fix(bridge): safer stall detection + VS Code command-based approval
Stall detection fixes: - Threshold 2→6 polls (30s minimum stall before triggering) - Added lastModifiedTime tracking (both stepCount AND modTime must freeze) - Cooldown 30s→60s between pending writes - Track lastPendingStepCount to prevent retrigger for same stall Approval handler fixes: - Replace HandleCascadeUserInteraction RPC with VS Code commands - Sequential fallback: acceptAgentStep → command.accept → terminalCommand.run - Same pattern for reject: rejectAgentStep → command.reject → terminalCommand.reject - Removed SDK dependency check (VS Code commands work without SDK)
This commit is contained in:
@@ -275,8 +275,10 @@ function setupMonitor() {
|
|||||||
let lastKnownStepCount = 0;
|
let lastKnownStepCount = 0;
|
||||||
let lastNotifyStepIndex = -1;
|
let lastNotifyStepIndex = -1;
|
||||||
let lastTaskStepIndex = -1;
|
let lastTaskStepIndex = -1;
|
||||||
let stallCount = 0; // consecutive polls with no step change while RUNNING
|
let stallCount = 0; // consecutive polls with frozen stepCount + lastModifiedTime
|
||||||
let lastPendingAt = 0; // timestamp of last pending approval write (dedup)
|
let lastPendingAt = 0; // timestamp of last pending approval write (dedup)
|
||||||
|
let lastPendingStepCount = -1; // step count when last pending was written (prevent retrigger)
|
||||||
|
let prevModTime = ''; // track lastModifiedTime changes
|
||||||
setInterval(async () => {
|
setInterval(async () => {
|
||||||
pollCount++;
|
pollCount++;
|
||||||
try {
|
try {
|
||||||
@@ -301,6 +303,7 @@ function setupMonitor() {
|
|||||||
const currentCount = bestSession.stepCount || 0;
|
const currentCount = bestSession.stepCount || 0;
|
||||||
const currentTitle = (bestSession.summary || 'Untitled').substring(0, 50);
|
const currentTitle = (bestSession.summary || 'Untitled').substring(0, 50);
|
||||||
const isRunning = String(bestSession.status || '').includes('RUNNING');
|
const isRunning = String(bestSession.status || '').includes('RUNNING');
|
||||||
|
const currentModTime = bestSession.lastModifiedTime || '';
|
||||||
// Session changed?
|
// Session changed?
|
||||||
if (bestSessionId !== activeSessionId) {
|
if (bestSessionId !== activeSessionId) {
|
||||||
activeSessionId = bestSessionId;
|
activeSessionId = bestSessionId;
|
||||||
@@ -309,17 +312,23 @@ function setupMonitor() {
|
|||||||
lastNotifyStepIndex = bestSession.latestNotifyUserStep?.stepIndex ?? -1;
|
lastNotifyStepIndex = bestSession.latestNotifyUserStep?.stepIndex ?? -1;
|
||||||
lastTaskStepIndex = bestSession.latestTaskBoundaryStep?.stepIndex ?? -1;
|
lastTaskStepIndex = bestSession.latestTaskBoundaryStep?.stepIndex ?? -1;
|
||||||
stallCount = 0;
|
stallCount = 0;
|
||||||
|
prevModTime = currentModTime;
|
||||||
writeRegistration(activeSessionId);
|
writeRegistration(activeSessionId);
|
||||||
console.log(`Gravity Bridge: [POLL#${pollCount}] session: ${activeSessionId.substring(0, 8)} "${currentTitle}" steps=${currentCount} ${isRunning ? 'RUNNING' : 'idle'}`);
|
console.log(`Gravity Bridge: [POLL#${pollCount}] session: ${activeSessionId.substring(0, 8)} "${currentTitle}" steps=${currentCount} ${isRunning ? 'RUNNING' : 'idle'}`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// ── STALL DETECTION for WAITING steps ──
|
// ── STALL DETECTION for WAITING steps ──
|
||||||
// If stepCount is frozen while status is RUNNING → AI is likely waiting for user approval
|
// TRUE WAITING: stepCount frozen AND lastModifiedTime frozen AND status RUNNING
|
||||||
if (currentCount === lastKnownStepCount && isRunning) {
|
// FALSE STALL: stepCount frozen but lastModifiedTime still changing (AI thinking/processing)
|
||||||
|
const timeAlsoFrozen = currentModTime === prevModTime;
|
||||||
|
prevModTime = currentModTime;
|
||||||
|
if (currentCount === lastKnownStepCount && isRunning && timeAlsoFrozen) {
|
||||||
stallCount++;
|
stallCount++;
|
||||||
// After 2 consecutive stalls (~10s), write pending approval (once per stall period)
|
// After 6 consecutive stalls (~30s), write pending approval
|
||||||
if (stallCount === 2 && (Date.now() - lastPendingAt) > 30000) {
|
// Only if: cooldown elapsed AND not same step count as last pending
|
||||||
|
if (stallCount === 6 && (Date.now() - lastPendingAt) > 60000 && currentCount !== lastPendingStepCount) {
|
||||||
lastPendingAt = Date.now();
|
lastPendingAt = Date.now();
|
||||||
|
lastPendingStepCount = currentCount;
|
||||||
console.log(`Gravity Bridge: [POLL#${pollCount}] STALL detected (${stallCount} polls, steps=${currentCount}) → writing pending`);
|
console.log(`Gravity Bridge: [POLL#${pollCount}] STALL detected (${stallCount} polls, steps=${currentCount}) → writing pending`);
|
||||||
writePendingApproval({
|
writePendingApproval({
|
||||||
conversation_id: activeSessionId,
|
conversation_id: activeSessionId,
|
||||||
@@ -327,12 +336,9 @@ function setupMonitor() {
|
|||||||
description: `⏳ **AI가 사용자 확인을 기다리고 있습니다**\n\n세션: ${currentTitle}\nstep: ${currentCount}\n\n✅ 승인 또는 ❌ 거부를 선택하세요.`,
|
description: `⏳ **AI가 사용자 확인을 기다리고 있습니다**\n\n세션: ${currentTitle}\nstep: ${currentCount}\n\n✅ 승인 또는 ❌ 거부를 선택하세요.`,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (pollCount % 20 === 0) {
|
|
||||||
console.log(`Gravity Bridge: [POLL#${pollCount}] stall=${stallCount} steps=${currentCount} RUNNING`);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
// Step count changed or not running — reset stall
|
// Reset stall if anything changed
|
||||||
if (stallCount > 0) {
|
if (stallCount > 0) {
|
||||||
console.log(`Gravity Bridge: [POLL#${pollCount}] stall cleared (was ${stallCount})`);
|
console.log(`Gravity Bridge: [POLL#${pollCount}] stall cleared (was ${stallCount})`);
|
||||||
}
|
}
|
||||||
@@ -404,10 +410,6 @@ async function processResponseFile(filePath) {
|
|||||||
const content = fs.readFileSync(filePath, 'utf-8');
|
const content = fs.readFileSync(filePath, 'utf-8');
|
||||||
const resp = JSON.parse(content);
|
const resp = JSON.parse(content);
|
||||||
console.log(`Gravity Bridge: [RESPONSE] request_id=${resp.request_id} approved=${resp.approved}`);
|
console.log(`Gravity Bridge: [RESPONSE] request_id=${resp.request_id} approved=${resp.approved}`);
|
||||||
if (!sdk) {
|
|
||||||
console.log('Gravity Bridge: [RESPONSE] SDK not available');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// Find matching pending request for session_id
|
// Find matching pending request for session_id
|
||||||
const pendingDir = path.join(bridgePath, 'pending');
|
const pendingDir = path.join(bridgePath, 'pending');
|
||||||
const pendingFile = path.join(pendingDir, `${resp.request_id}.json`);
|
const pendingFile = path.join(pendingDir, `${resp.request_id}.json`);
|
||||||
@@ -419,27 +421,55 @@ async function processResponseFile(filePath) {
|
|||||||
}
|
}
|
||||||
catch { }
|
catch { }
|
||||||
}
|
}
|
||||||
if (sessionId && resp.approved) {
|
if (resp.approved) {
|
||||||
try {
|
// TRY ALL APPROVAL METHODS sequentially until one works
|
||||||
await sdk.ls.rawRPC('HandleCascadeUserInteraction', {
|
// These are the CORRECT VS Code commands from the SDK:
|
||||||
cascadeId: sessionId,
|
// 1. antigravity.agent.acceptAgentStep — generic agent step (file edits, etc.)
|
||||||
approved: true,
|
// 2. antigravity.command.accept — non-terminal command acceptance
|
||||||
});
|
// 3. antigravity.terminalCommand.run — terminal command execution
|
||||||
console.log('Gravity Bridge: [RESPONSE] ✅ approved via HandleCascadeUserInteraction');
|
const approvalCommands = [
|
||||||
}
|
{ cmd: 'antigravity.agent.acceptAgentStep', label: 'acceptAgentStep' },
|
||||||
catch (e) {
|
{ cmd: 'antigravity.command.accept', label: 'command.accept' },
|
||||||
console.log(`Gravity Bridge: [RESPONSE] HandleCascadeUserInteraction failed: ${e.message}`);
|
{ cmd: 'antigravity.terminalCommand.run', label: 'terminalCommand.run' },
|
||||||
|
];
|
||||||
|
let approved = false;
|
||||||
|
for (const { cmd, label } of approvalCommands) {
|
||||||
try {
|
try {
|
||||||
await sdk.ls.rawRPC('ResolveOutstandingSteps', { cascadeId: sessionId });
|
await vscode.commands.executeCommand(cmd);
|
||||||
console.log('Gravity Bridge: [RESPONSE] ✅ approved via ResolveOutstandingSteps');
|
console.log(`Gravity Bridge: [RESPONSE] ✅ approved via ${label}`);
|
||||||
|
approved = true;
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
catch (e2) {
|
catch (e) {
|
||||||
console.log(`Gravity Bridge: [RESPONSE] ResolveOutstandingSteps also failed: ${e2.message}`);
|
console.log(`Gravity Bridge: [RESPONSE] ${label} failed: ${e.message}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (!approved) {
|
||||||
|
console.log('Gravity Bridge: [RESPONSE] ⚠️ all approval methods failed');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
console.log(`Gravity Bridge: [RESPONSE] ${resp.approved ? '✅' : '❌'} (session=${sessionId || 'unknown'})`);
|
// REJECT — try all reject commands
|
||||||
|
const rejectCommands = [
|
||||||
|
{ cmd: 'antigravity.agent.rejectAgentStep', label: 'rejectAgentStep' },
|
||||||
|
{ cmd: 'antigravity.command.reject', label: 'command.reject' },
|
||||||
|
{ cmd: 'antigravity.terminalCommand.reject', label: 'terminalCommand.reject' },
|
||||||
|
];
|
||||||
|
let rejected = false;
|
||||||
|
for (const { cmd, label } of rejectCommands) {
|
||||||
|
try {
|
||||||
|
await vscode.commands.executeCommand(cmd);
|
||||||
|
console.log(`Gravity Bridge: [RESPONSE] ❌ rejected via ${label}`);
|
||||||
|
rejected = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
console.log(`Gravity Bridge: [RESPONSE] ${label} failed: ${e.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!rejected) {
|
||||||
|
console.log('Gravity Bridge: [RESPONSE] ⚠️ all reject methods failed');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
fs.unlinkSync(filePath);
|
fs.unlinkSync(filePath);
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -244,8 +244,10 @@ function setupMonitor() {
|
|||||||
let lastKnownStepCount = 0;
|
let lastKnownStepCount = 0;
|
||||||
let lastNotifyStepIndex = -1;
|
let lastNotifyStepIndex = -1;
|
||||||
let lastTaskStepIndex = -1;
|
let lastTaskStepIndex = -1;
|
||||||
let stallCount = 0; // consecutive polls with no step change while RUNNING
|
let stallCount = 0; // consecutive polls with frozen stepCount + lastModifiedTime
|
||||||
let lastPendingAt = 0; // timestamp of last pending approval write (dedup)
|
let lastPendingAt = 0; // timestamp of last pending approval write (dedup)
|
||||||
|
let lastPendingStepCount = -1; // step count when last pending was written (prevent retrigger)
|
||||||
|
let prevModTime = ''; // track lastModifiedTime changes
|
||||||
|
|
||||||
setInterval(async () => {
|
setInterval(async () => {
|
||||||
pollCount++;
|
pollCount++;
|
||||||
@@ -271,6 +273,7 @@ function setupMonitor() {
|
|||||||
const currentCount = bestSession.stepCount || 0;
|
const currentCount = bestSession.stepCount || 0;
|
||||||
const currentTitle = (bestSession.summary || 'Untitled').substring(0, 50);
|
const currentTitle = (bestSession.summary || 'Untitled').substring(0, 50);
|
||||||
const isRunning = String(bestSession.status || '').includes('RUNNING');
|
const isRunning = String(bestSession.status || '').includes('RUNNING');
|
||||||
|
const currentModTime = bestSession.lastModifiedTime || '';
|
||||||
|
|
||||||
// Session changed?
|
// Session changed?
|
||||||
if (bestSessionId !== activeSessionId) {
|
if (bestSessionId !== activeSessionId) {
|
||||||
@@ -280,18 +283,25 @@ function setupMonitor() {
|
|||||||
lastNotifyStepIndex = bestSession.latestNotifyUserStep?.stepIndex ?? -1;
|
lastNotifyStepIndex = bestSession.latestNotifyUserStep?.stepIndex ?? -1;
|
||||||
lastTaskStepIndex = bestSession.latestTaskBoundaryStep?.stepIndex ?? -1;
|
lastTaskStepIndex = bestSession.latestTaskBoundaryStep?.stepIndex ?? -1;
|
||||||
stallCount = 0;
|
stallCount = 0;
|
||||||
|
prevModTime = currentModTime;
|
||||||
writeRegistration(activeSessionId);
|
writeRegistration(activeSessionId);
|
||||||
console.log(`Gravity Bridge: [POLL#${pollCount}] session: ${activeSessionId.substring(0, 8)} "${currentTitle}" steps=${currentCount} ${isRunning ? 'RUNNING' : 'idle'}`);
|
console.log(`Gravity Bridge: [POLL#${pollCount}] session: ${activeSessionId.substring(0, 8)} "${currentTitle}" steps=${currentCount} ${isRunning ? 'RUNNING' : 'idle'}`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── STALL DETECTION for WAITING steps ──
|
// ── STALL DETECTION for WAITING steps ──
|
||||||
// If stepCount is frozen while status is RUNNING → AI is likely waiting for user approval
|
// TRUE WAITING: stepCount frozen AND lastModifiedTime frozen AND status RUNNING
|
||||||
if (currentCount === lastKnownStepCount && isRunning) {
|
// FALSE STALL: stepCount frozen but lastModifiedTime still changing (AI thinking/processing)
|
||||||
|
const timeAlsoFrozen = currentModTime === prevModTime;
|
||||||
|
prevModTime = currentModTime;
|
||||||
|
|
||||||
|
if (currentCount === lastKnownStepCount && isRunning && timeAlsoFrozen) {
|
||||||
stallCount++;
|
stallCount++;
|
||||||
// After 2 consecutive stalls (~10s), write pending approval (once per stall period)
|
// After 6 consecutive stalls (~30s), write pending approval
|
||||||
if (stallCount === 2 && (Date.now() - lastPendingAt) > 30000) {
|
// Only if: cooldown elapsed AND not same step count as last pending
|
||||||
|
if (stallCount === 6 && (Date.now() - lastPendingAt) > 60000 && currentCount !== lastPendingStepCount) {
|
||||||
lastPendingAt = Date.now();
|
lastPendingAt = Date.now();
|
||||||
|
lastPendingStepCount = currentCount;
|
||||||
console.log(`Gravity Bridge: [POLL#${pollCount}] STALL detected (${stallCount} polls, steps=${currentCount}) → writing pending`);
|
console.log(`Gravity Bridge: [POLL#${pollCount}] STALL detected (${stallCount} polls, steps=${currentCount}) → writing pending`);
|
||||||
writePendingApproval({
|
writePendingApproval({
|
||||||
conversation_id: activeSessionId,
|
conversation_id: activeSessionId,
|
||||||
@@ -299,11 +309,8 @@ function setupMonitor() {
|
|||||||
description: `⏳ **AI가 사용자 확인을 기다리고 있습니다**\n\n세션: ${currentTitle}\nstep: ${currentCount}\n\n✅ 승인 또는 ❌ 거부를 선택하세요.`,
|
description: `⏳ **AI가 사용자 확인을 기다리고 있습니다**\n\n세션: ${currentTitle}\nstep: ${currentCount}\n\n✅ 승인 또는 ❌ 거부를 선택하세요.`,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (pollCount % 20 === 0) {
|
|
||||||
console.log(`Gravity Bridge: [POLL#${pollCount}] stall=${stallCount} steps=${currentCount} RUNNING`);
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
// Step count changed or not running — reset stall
|
// Reset stall if anything changed
|
||||||
if (stallCount > 0) {
|
if (stallCount > 0) {
|
||||||
console.log(`Gravity Bridge: [POLL#${pollCount}] stall cleared (was ${stallCount})`);
|
console.log(`Gravity Bridge: [POLL#${pollCount}] stall cleared (was ${stallCount})`);
|
||||||
}
|
}
|
||||||
@@ -383,11 +390,6 @@ async function processResponseFile(filePath: string) {
|
|||||||
const resp = JSON.parse(content);
|
const resp = JSON.parse(content);
|
||||||
console.log(`Gravity Bridge: [RESPONSE] request_id=${resp.request_id} approved=${resp.approved}`);
|
console.log(`Gravity Bridge: [RESPONSE] request_id=${resp.request_id} approved=${resp.approved}`);
|
||||||
|
|
||||||
if (!sdk) {
|
|
||||||
console.log('Gravity Bridge: [RESPONSE] SDK not available');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Find matching pending request for session_id
|
// Find matching pending request for session_id
|
||||||
const pendingDir = path.join(bridgePath, 'pending');
|
const pendingDir = path.join(bridgePath, 'pending');
|
||||||
const pendingFile = path.join(pendingDir, `${resp.request_id}.json`);
|
const pendingFile = path.join(pendingDir, `${resp.request_id}.json`);
|
||||||
@@ -399,24 +401,52 @@ async function processResponseFile(filePath: string) {
|
|||||||
} catch { }
|
} catch { }
|
||||||
}
|
}
|
||||||
|
|
||||||
if (sessionId && resp.approved) {
|
if (resp.approved) {
|
||||||
try {
|
// TRY ALL APPROVAL METHODS sequentially until one works
|
||||||
await sdk.ls.rawRPC('HandleCascadeUserInteraction', {
|
// These are the CORRECT VS Code commands from the SDK:
|
||||||
cascadeId: sessionId,
|
// 1. antigravity.agent.acceptAgentStep — generic agent step (file edits, etc.)
|
||||||
approved: true,
|
// 2. antigravity.command.accept — non-terminal command acceptance
|
||||||
});
|
// 3. antigravity.terminalCommand.run — terminal command execution
|
||||||
console.log('Gravity Bridge: [RESPONSE] ✅ approved via HandleCascadeUserInteraction');
|
const approvalCommands = [
|
||||||
} catch (e: any) {
|
{ cmd: 'antigravity.agent.acceptAgentStep', label: 'acceptAgentStep' },
|
||||||
console.log(`Gravity Bridge: [RESPONSE] HandleCascadeUserInteraction failed: ${e.message}`);
|
{ cmd: 'antigravity.command.accept', label: 'command.accept' },
|
||||||
|
{ cmd: 'antigravity.terminalCommand.run', label: 'terminalCommand.run' },
|
||||||
|
];
|
||||||
|
let approved = false;
|
||||||
|
for (const { cmd, label } of approvalCommands) {
|
||||||
try {
|
try {
|
||||||
await sdk.ls.rawRPC('ResolveOutstandingSteps', { cascadeId: sessionId });
|
await vscode.commands.executeCommand(cmd);
|
||||||
console.log('Gravity Bridge: [RESPONSE] ✅ approved via ResolveOutstandingSteps');
|
console.log(`Gravity Bridge: [RESPONSE] ✅ approved via ${label}`);
|
||||||
} catch (e2: any) {
|
approved = true;
|
||||||
console.log(`Gravity Bridge: [RESPONSE] ResolveOutstandingSteps also failed: ${e2.message}`);
|
break;
|
||||||
|
} catch (e: any) {
|
||||||
|
console.log(`Gravity Bridge: [RESPONSE] ${label} failed: ${e.message}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (!approved) {
|
||||||
|
console.log('Gravity Bridge: [RESPONSE] ⚠️ all approval methods failed');
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
console.log(`Gravity Bridge: [RESPONSE] ${resp.approved ? '✅' : '❌'} (session=${sessionId || 'unknown'})`);
|
// REJECT — try all reject commands
|
||||||
|
const rejectCommands = [
|
||||||
|
{ cmd: 'antigravity.agent.rejectAgentStep', label: 'rejectAgentStep' },
|
||||||
|
{ cmd: 'antigravity.command.reject', label: 'command.reject' },
|
||||||
|
{ cmd: 'antigravity.terminalCommand.reject', label: 'terminalCommand.reject' },
|
||||||
|
];
|
||||||
|
let rejected = false;
|
||||||
|
for (const { cmd, label } of rejectCommands) {
|
||||||
|
try {
|
||||||
|
await vscode.commands.executeCommand(cmd);
|
||||||
|
console.log(`Gravity Bridge: [RESPONSE] ❌ rejected via ${label}`);
|
||||||
|
rejected = true;
|
||||||
|
break;
|
||||||
|
} catch (e: any) {
|
||||||
|
console.log(`Gravity Bridge: [RESPONSE] ${label} failed: ${e.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!rejected) {
|
||||||
|
console.log('Gravity Bridge: [RESPONSE] ⚠️ all reject methods failed');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
try { fs.unlinkSync(filePath); } catch { }
|
try { fs.unlinkSync(filePath); } catch { }
|
||||||
|
|||||||
Reference in New Issue
Block a user