fix(bridge): approval flow tuning — dedup + text cleanup + stall fallback removal + safe reject #task-256
This commit is contained in:
@@ -980,6 +980,9 @@ function generateApprovalObserverScript(_port) {
|
||||
|
||||
var txt=(b.textContent||'').trim();
|
||||
if(!txt)continue;
|
||||
// Strip keyboard shortcut suffixes (e.g. "RunAlt+↵" → "Run")
|
||||
txt=txt.replace(/(Alt|Ctrl|Shift|Meta)\+.*/,'').trim();
|
||||
if(!txt)continue;
|
||||
|
||||
// Match against patterns
|
||||
var matchedType=null;
|
||||
@@ -1480,28 +1483,8 @@ function setupMonitor() {
|
||||
logToFile(`[STEP-PROBE] error: ${e.message}`);
|
||||
}
|
||||
}
|
||||
const now = Date.now();
|
||||
const cooldownOk = (now - lastPendingTime) > 60_000;
|
||||
const fallbackThreshold = (currentCount > 770) ? 4 : 8; // 775-limit: faster fallback
|
||||
if (consecutiveIdleCount >= fallbackThreshold && sawRunningAfterPending && cooldownOk) {
|
||||
// Dynamic fallback: 20s (>770 steps) or 40s (normal)
|
||||
lastPendingStepIndex = currentCount;
|
||||
lastPendingTime = now;
|
||||
sawRunningAfterPending = false;
|
||||
const command = `Stall at step ${currentCount} (fallback)`;
|
||||
const description = `승인 대기 감지 — fallback (${consecutiveIdleCount * 5}초 정지), Title: "${currentTitle}"`;
|
||||
logToFile(`[STALL-FALLBACK] step=${currentCount} frozenCount=${consecutiveIdleCount} → pending`);
|
||||
writePendingApproval({ conversation_id: activeSessionId, command, description, source: 'stall_fallback' });
|
||||
}
|
||||
else if (consecutiveIdleCount === fallbackThreshold) {
|
||||
const reasons = [];
|
||||
if (!sawRunningAfterPending)
|
||||
reasons.push('needDelta>0');
|
||||
if (!cooldownOk)
|
||||
reasons.push(`cooldown(${Math.round((60000 - (now - lastPendingTime)) / 1000)}s)`);
|
||||
if (reasons.length > 0)
|
||||
logToFile(`[STALL] SKIP: ${reasons.join(', ')}`);
|
||||
}
|
||||
// Stall fallback REMOVED — step probe is sole fallback source
|
||||
// (stall fallback was generating false positives and is now redundant)
|
||||
}
|
||||
else if (!isRunning) {
|
||||
consecutiveIdleCount = 0;
|
||||
@@ -1614,19 +1597,22 @@ async function processResponseFile(filePath) {
|
||||
logToFile(`[RESPONSE] renderer-handled approval (rid=${resp.request_id})`);
|
||||
}
|
||||
else {
|
||||
// Step probe / stall path: try all approval strategies
|
||||
logToFile(`[RESPONSE] step_probe/stall → trying multi-strategy approval`);
|
||||
const approvalResult = await tryApprovalStrategies(approved, sessionId);
|
||||
logToFile(`[RESPONSE] approval result: ${approvalResult}`);
|
||||
// Step probe path: approve → trigger renderer click, reject → log only
|
||||
if (approved) {
|
||||
logToFile(`[RESPONSE] step_probe → approve via trigger-click`);
|
||||
clickTrigger = { action: 'approve', timestamp: Date.now() };
|
||||
}
|
||||
else {
|
||||
// SAFE: reject logs only — NO ResolveOutstandingSteps (it cancels AI work!)
|
||||
logToFile(`[RESPONSE] step_probe → reject (log only, no destructive action)`);
|
||||
}
|
||||
}
|
||||
logToFile(`[RESPONSE] ${approved ? 'approve' : 'reject'} done (${isDomObserver ? 'dom' : 'step_probe'})`);
|
||||
// Cleanup response file — BUT NOT for DOM observer!
|
||||
if (!isDomObserver) {
|
||||
try {
|
||||
fs.unlinkSync(filePath);
|
||||
}
|
||||
catch { }
|
||||
// Cleanup response file
|
||||
try {
|
||||
fs.unlinkSync(filePath);
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
catch (e) {
|
||||
const log = `[RESPONSE] error: ${e.message}`;
|
||||
@@ -1762,13 +1748,33 @@ function writePendingApproval(data) {
|
||||
if (!fs.existsSync(pendingDir)) {
|
||||
fs.mkdirSync(pendingDir, { recursive: true });
|
||||
}
|
||||
const id = Date.now().toString();
|
||||
// ── Dedup: skip if DOM observer already created a pending for same action recently ──
|
||||
const nowMs = Date.now();
|
||||
const DEDUP_WINDOW_MS = 15_000; // 15 second dedup window
|
||||
try {
|
||||
const existingFiles = fs.readdirSync(pendingDir).filter((f) => f.endsWith('.json'));
|
||||
for (const ef of existingFiles) {
|
||||
const efPath = path.join(pendingDir, ef);
|
||||
const existing = JSON.parse(fs.readFileSync(efPath, 'utf-8'));
|
||||
if (existing.source === 'dom_observer' && existing.status === 'pending') {
|
||||
const age = nowMs - (existing.timestamp * 1000);
|
||||
if (age < DEDUP_WINDOW_MS && age >= 0) {
|
||||
logToFile(`[DEDUP] skip step_probe pending — DOM observer pending exists: ${ef} (${Math.round(age / 1000)}s ago)`);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (dedupErr) {
|
||||
logToFile(`[DEDUP] check error (non-fatal): ${dedupErr.message}`);
|
||||
}
|
||||
const id = nowMs.toString();
|
||||
const payload = {
|
||||
request_id: id,
|
||||
conversation_id: data.conversation_id,
|
||||
command: data.command,
|
||||
description: data.description,
|
||||
timestamp: Date.now() / 1000,
|
||||
timestamp: nowMs / 1000,
|
||||
status: 'pending',
|
||||
discord_message_id: 0,
|
||||
project_name: projectName,
|
||||
|
||||
Reference in New Issue
Block a user