fix(bridge): v0.3.12 approval state management — sawRunningAfterPending gate + approval-flow.md system doc
- processResponseFile: set sawRunningAfterPending=true instead of removing resets (prevents infinite pending loop AND known-issues L479 auto_resolve regression) - Hoist sawRunningAfterPending to module level for cross-function access - Add recentPendingSteps memory dedup Map (60s TTL) for file-deletion resilience - Create docs/approval-flow.md: complete system flow guide with state diagram - Update known-issues.md: 2 new entries (state reset fix, memory dedup)
This commit is contained in:
@@ -82,6 +82,11 @@ let deterministicPort = 0; // derived from projectName, consistent across restar
|
||||
let watcher = null;
|
||||
let commandsWatcher = null;
|
||||
const sentPendingIds = new Set();
|
||||
// Memory-based dedup: tracks recently created pending step_indexes to prevent
|
||||
// regeneration after pending file deletion (by Collector/Bot response cycle).
|
||||
// Map<string, number> = `${conversationId}:${stepIndex}` → creation timestamp
|
||||
const recentPendingSteps = new Map();
|
||||
const PENDING_MEMORY_TTL_MS = 60_000; // 60 seconds memory retention
|
||||
// ─── Project Detection ───
|
||||
function detectProjectName() {
|
||||
const config = vscode.workspace.getConfiguration('gravityBridge');
|
||||
@@ -706,6 +711,7 @@ let clickTrigger = null;
|
||||
let sessionStalled = false; // true when session is stalled waiting for approval
|
||||
let lastPendingStepIndex = -1; // dedup: don't re-create pending for same step (module-level for tryApprovalStrategies)
|
||||
let stallProbed = false; // prevent repeated step probes during same stall (module-level for processResponseFile reset)
|
||||
let sawRunningAfterPending = true; // gate: must see delta>0 before next pending (module-level for processResponseFile)
|
||||
// Deep inspect trigger: curl sets this, renderer picks it up and POSTs results back
|
||||
let deepInspectRequested = false;
|
||||
let deepInspectResult = null;
|
||||
@@ -1733,7 +1739,7 @@ function setupMonitor() {
|
||||
// lastPendingStepIndex is module-level (above sessionStalled)
|
||||
let consecutiveIdleCount = 0; // debounce: require N consecutive stall polls
|
||||
let lastPendingTime = 0; // cooldown: minimum gap between pendings
|
||||
let sawRunningAfterPending = true; // gate: must see delta>0 before next pending
|
||||
// sawRunningAfterPending is module-level (used by processResponseFile to close auto_resolve gate)
|
||||
let lastModTime = ''; // track lastModifiedTime to distinguish thinking vs approval
|
||||
// stallProbed is module-level (used by processResponseFile to reset after approval)
|
||||
let lastRelayedTaskText = ''; // dedup TASK_BOUNDARY relay
|
||||
@@ -1969,6 +1975,11 @@ function setupMonitor() {
|
||||
writeChatSnapshot(`✅ **AG에서 직접 진행됨** (step ${lastPendingStepIndex})\n\n\`${primaryCommand.substring(0, 200)}\``);
|
||||
}
|
||||
lastPendingStepIndex = -1;
|
||||
// Clear memory dedup for this session (step progressed, new WAITING steps are allowed)
|
||||
for (const k of recentPendingSteps.keys()) {
|
||||
if (k.startsWith(activeSessionId + ':'))
|
||||
recentPendingSteps.delete(k);
|
||||
}
|
||||
}
|
||||
consecutiveIdleCount = 0;
|
||||
sawRunningAfterPending = true;
|
||||
@@ -2628,9 +2639,21 @@ async function processResponseFile(filePath) {
|
||||
logToFile(`[RESPONSE] strategy result: ${strategyResult}`);
|
||||
}
|
||||
logToFile(`[RESPONSE] ${approved ? 'approve' : 'reject'} done (${isDomObserver ? 'dom' : 'step_probe'})`);
|
||||
// Reset stall probe gate so next WAITING step is detected immediately
|
||||
stallProbed = false;
|
||||
lastPendingStepIndex = -1;
|
||||
// FIX v2 (2026-03-16): Correct state management after response processing.
|
||||
//
|
||||
// HISTORY: processResponseFile originally reset lastPendingStepIndex=-1 and stallProbed=false.
|
||||
// v0.3.11 removed both resets entirely → caused infinite pending loop (step_probe re-detects
|
||||
// same WAITING step because lastPendingStepIndex=-1 makes si!=lastPendingStepIndex true).
|
||||
// v0.3.12 attempt removed resets entirely → conflicts with known-issues.md L479
|
||||
// (auto_resolve duplicate notification on delta>0 because sawRunningAfterPending is false).
|
||||
//
|
||||
// CORRECT FIX: Set sawRunningAfterPending=true to close the auto_resolve gate.
|
||||
// - lastPendingStepIndex: KEEP (prevents step_probe from re-creating pending for same step)
|
||||
// - stallProbed: KEEP (prevents re-probe during same stall)
|
||||
// - sawRunningAfterPending = true: CRITICAL — tells auto_resolve "Discord handled this,
|
||||
// don't generate duplicate 직접 진행됨 notification" (prevents known-issues L479 regression)
|
||||
// All three reset naturally at delta > 0 (L1972-1980) when step actually progresses.
|
||||
sawRunningAfterPending = true;
|
||||
// Cleanup response file
|
||||
// CRITICAL: DOM observer responses must NOT be deleted here!
|
||||
// The renderer polls GET /response/:rid to discover the approval.
|
||||
@@ -2780,6 +2803,22 @@ function writePendingApproval(data) {
|
||||
// ── Dedup: if DOM observer already created a "Run"-only pending, MERGE detailed info into it ──
|
||||
const nowMs = Date.now();
|
||||
const DEDUP_WINDOW_MS = 15_000; // 15 second dedup window
|
||||
// ── FIX: Memory-based dedup (survives pending file deletion by Collector/Bot) ──
|
||||
// Pending files are deleted when Bot writes a response (bridge.py L461, collector.py L259).
|
||||
// File-based dedup alone fails after deletion → same step_index creates new pending → loop.
|
||||
if (data.step_index !== undefined && data.conversation_id) {
|
||||
const memKey = `${data.conversation_id}:${data.step_index}`;
|
||||
const prevTs = recentPendingSteps.get(memKey);
|
||||
if (prevTs && (nowMs - prevTs) < PENDING_MEMORY_TTL_MS) {
|
||||
logToFile(`[DEDUP-MEM] skip: step_index ${data.step_index} already created ${Math.round((nowMs - prevTs) / 1000)}s ago`);
|
||||
return;
|
||||
}
|
||||
// Cleanup stale entries (keep map small)
|
||||
for (const [k, ts] of recentPendingSteps) {
|
||||
if (nowMs - ts > PENDING_MEMORY_TTL_MS)
|
||||
recentPendingSteps.delete(k);
|
||||
}
|
||||
}
|
||||
try {
|
||||
const existingFiles = fs.readdirSync(pendingDir).filter((f) => f.endsWith('.json'));
|
||||
for (const ef of existingFiles) {
|
||||
@@ -2799,6 +2838,10 @@ function writePendingApproval(data) {
|
||||
existing.source = 'dom_observer+step_probe'; // mark as merged
|
||||
fs.writeFileSync(efPath, JSON.stringify(existing, null, 2), 'utf-8');
|
||||
logToFile(`[DEDUP] MERGED step_probe info into DOM pending: ${ef} cmd="${data.command.substring(0, 60)}"`);
|
||||
// Record in memory dedup
|
||||
if (data.step_index !== undefined && data.conversation_id) {
|
||||
recentPendingSteps.set(`${data.conversation_id}:${data.step_index}`, nowMs);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -2845,6 +2888,10 @@ function writePendingApproval(data) {
|
||||
};
|
||||
fs.writeFileSync(path.join(pendingDir, `${id}.json`), JSON.stringify(payload, null, 2), 'utf-8');
|
||||
console.log(`Gravity Bridge: pending approval written → ${id}.json`);
|
||||
// Record in memory dedup cache (survives file deletion by Collector/Bot)
|
||||
if (data.step_index !== undefined && data.conversation_id) {
|
||||
recentPendingSteps.set(`${data.conversation_id}:${data.step_index}`, nowMs);
|
||||
}
|
||||
// Register session → project mapping (correct because projectName is per-window)
|
||||
if (data.conversation_id) {
|
||||
writeRegistration(data.conversation_id);
|
||||
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user