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 lastNotifyStepIndex = -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 lastPendingStepCount = -1; // step count when last pending was written (prevent retrigger)
|
||||
let prevModTime = ''; // track lastModifiedTime changes
|
||||
setInterval(async () => {
|
||||
pollCount++;
|
||||
try {
|
||||
@@ -301,6 +303,7 @@ function setupMonitor() {
|
||||
const currentCount = bestSession.stepCount || 0;
|
||||
const currentTitle = (bestSession.summary || 'Untitled').substring(0, 50);
|
||||
const isRunning = String(bestSession.status || '').includes('RUNNING');
|
||||
const currentModTime = bestSession.lastModifiedTime || '';
|
||||
// Session changed?
|
||||
if (bestSessionId !== activeSessionId) {
|
||||
activeSessionId = bestSessionId;
|
||||
@@ -309,17 +312,23 @@ function setupMonitor() {
|
||||
lastNotifyStepIndex = bestSession.latestNotifyUserStep?.stepIndex ?? -1;
|
||||
lastTaskStepIndex = bestSession.latestTaskBoundaryStep?.stepIndex ?? -1;
|
||||
stallCount = 0;
|
||||
prevModTime = currentModTime;
|
||||
writeRegistration(activeSessionId);
|
||||
console.log(`Gravity Bridge: [POLL#${pollCount}] session: ${activeSessionId.substring(0, 8)} "${currentTitle}" steps=${currentCount} ${isRunning ? 'RUNNING' : 'idle'}`);
|
||||
return;
|
||||
}
|
||||
// ── STALL DETECTION for WAITING steps ──
|
||||
// If stepCount is frozen while status is RUNNING → AI is likely waiting for user approval
|
||||
if (currentCount === lastKnownStepCount && isRunning) {
|
||||
// TRUE WAITING: stepCount frozen AND lastModifiedTime frozen AND status RUNNING
|
||||
// FALSE STALL: stepCount frozen but lastModifiedTime still changing (AI thinking/processing)
|
||||
const timeAlsoFrozen = currentModTime === prevModTime;
|
||||
prevModTime = currentModTime;
|
||||
if (currentCount === lastKnownStepCount && isRunning && timeAlsoFrozen) {
|
||||
stallCount++;
|
||||
// After 2 consecutive stalls (~10s), write pending approval (once per stall period)
|
||||
if (stallCount === 2 && (Date.now() - lastPendingAt) > 30000) {
|
||||
// After 6 consecutive stalls (~30s), write pending approval
|
||||
// Only if: cooldown elapsed AND not same step count as last pending
|
||||
if (stallCount === 6 && (Date.now() - lastPendingAt) > 60000 && currentCount !== lastPendingStepCount) {
|
||||
lastPendingAt = Date.now();
|
||||
lastPendingStepCount = currentCount;
|
||||
console.log(`Gravity Bridge: [POLL#${pollCount}] STALL detected (${stallCount} polls, steps=${currentCount}) → writing pending`);
|
||||
writePendingApproval({
|
||||
conversation_id: activeSessionId,
|
||||
@@ -327,12 +336,9 @@ function setupMonitor() {
|
||||
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 {
|
||||
// Step count changed or not running — reset stall
|
||||
// Reset stall if anything changed
|
||||
if (stallCount > 0) {
|
||||
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 resp = JSON.parse(content);
|
||||
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
|
||||
const pendingDir = path.join(bridgePath, 'pending');
|
||||
const pendingFile = path.join(pendingDir, `${resp.request_id}.json`);
|
||||
@@ -419,27 +421,55 @@ async function processResponseFile(filePath) {
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
if (sessionId && resp.approved) {
|
||||
try {
|
||||
await sdk.ls.rawRPC('HandleCascadeUserInteraction', {
|
||||
cascadeId: sessionId,
|
||||
approved: true,
|
||||
});
|
||||
console.log('Gravity Bridge: [RESPONSE] ✅ approved via HandleCascadeUserInteraction');
|
||||
}
|
||||
catch (e) {
|
||||
console.log(`Gravity Bridge: [RESPONSE] HandleCascadeUserInteraction failed: ${e.message}`);
|
||||
if (resp.approved) {
|
||||
// TRY ALL APPROVAL METHODS sequentially until one works
|
||||
// These are the CORRECT VS Code commands from the SDK:
|
||||
// 1. antigravity.agent.acceptAgentStep — generic agent step (file edits, etc.)
|
||||
// 2. antigravity.command.accept — non-terminal command acceptance
|
||||
// 3. antigravity.terminalCommand.run — terminal command execution
|
||||
const approvalCommands = [
|
||||
{ cmd: 'antigravity.agent.acceptAgentStep', label: 'acceptAgentStep' },
|
||||
{ cmd: 'antigravity.command.accept', label: 'command.accept' },
|
||||
{ cmd: 'antigravity.terminalCommand.run', label: 'terminalCommand.run' },
|
||||
];
|
||||
let approved = false;
|
||||
for (const { cmd, label } of approvalCommands) {
|
||||
try {
|
||||
await sdk.ls.rawRPC('ResolveOutstandingSteps', { cascadeId: sessionId });
|
||||
console.log('Gravity Bridge: [RESPONSE] ✅ approved via ResolveOutstandingSteps');
|
||||
await vscode.commands.executeCommand(cmd);
|
||||
console.log(`Gravity Bridge: [RESPONSE] ✅ approved via ${label}`);
|
||||
approved = true;
|
||||
break;
|
||||
}
|
||||
catch (e2) {
|
||||
console.log(`Gravity Bridge: [RESPONSE] ResolveOutstandingSteps also failed: ${e2.message}`);
|
||||
catch (e) {
|
||||
console.log(`Gravity Bridge: [RESPONSE] ${label} failed: ${e.message}`);
|
||||
}
|
||||
}
|
||||
if (!approved) {
|
||||
console.log('Gravity Bridge: [RESPONSE] ⚠️ all approval methods failed');
|
||||
}
|
||||
}
|
||||
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 {
|
||||
fs.unlinkSync(filePath);
|
||||
|
||||
Reference in New Issue
Block a user