fix(bridge): hybrid approval — SDK rawRPC + VS Code commands
Root cause: VS Code commands (acceptAgentStep, terminalCommand.run etc) return undefined silently but don't actually accept WAITING steps. LS requires HTTPS + CSRF token for RPC calls. New approach: Phase 1 tries SDK rawRPC (has CSRF auth) with HandleCascadeUserInteraction + ResolveOutstandingSteps. Phase 2 tries all VS Code commands as fallback. All results logged to bridge/extension.log for debugging. Also removed stall detection (fundamentally broken — stepCount keeps incrementing from other tool calls during WAITING).
This commit is contained in:
@@ -16,6 +16,16 @@ import * as path from 'path';
|
||||
import * as os from 'os';
|
||||
import * as cp from 'child_process';
|
||||
|
||||
// ─── File-based logging (AI can read directly) ───
|
||||
function logToFile(msg: string) {
|
||||
try {
|
||||
if (!bridgePath) return;
|
||||
const logFile = path.join(bridgePath, 'extension.log');
|
||||
const ts = new Date().toISOString().replace('T', ' ').substring(0, 19);
|
||||
fs.appendFileSync(logFile, `${ts} ${msg}\n`, 'utf-8');
|
||||
} catch { }
|
||||
}
|
||||
|
||||
// antigravity-sdk embedded locally (src/sdk/)
|
||||
let AntigravitySDK: any;
|
||||
let sdk: any;
|
||||
@@ -244,10 +254,6 @@ function setupMonitor() {
|
||||
let lastKnownStepCount = 0;
|
||||
let lastNotifyStepIndex = -1;
|
||||
let lastTaskStepIndex = -1;
|
||||
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++;
|
||||
@@ -273,7 +279,6 @@ 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) {
|
||||
@@ -282,45 +287,17 @@ function setupMonitor() {
|
||||
lastKnownStepCount = currentCount;
|
||||
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 ──
|
||||
// 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 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,
|
||||
command: `⏳ 사용자 확인 대기 중`,
|
||||
description: `⏳ **AI가 사용자 확인을 기다리고 있습니다**\n\n세션: ${currentTitle}\nstep: ${currentCount}\n\n✅ 승인 또는 ❌ 거부를 선택하세요.`,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// Reset stall if anything changed
|
||||
if (stallCount > 0) {
|
||||
console.log(`Gravity Bridge: [POLL#${pollCount}] stall cleared (was ${stallCount})`);
|
||||
}
|
||||
stallCount = 0;
|
||||
}
|
||||
|
||||
// No new steps? (but process notify/task even without delta)
|
||||
// No change?
|
||||
if (currentCount <= lastKnownStepCount && pollCount > 1) {
|
||||
// Still process notify/task changes even without step count change
|
||||
// (they might arrive as updates to existing steps)
|
||||
if (pollCount % 30 === 0) {
|
||||
console.log(`Gravity Bridge: [POLL#${pollCount}] idle steps=${currentCount}`);
|
||||
}
|
||||
// Still process notify/task below (stepIndex may update without stepCount change)
|
||||
}
|
||||
|
||||
const delta = currentCount - lastKnownStepCount;
|
||||
@@ -388,7 +365,9 @@ async function processResponseFile(filePath: string) {
|
||||
try {
|
||||
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}`);
|
||||
const msg = `[RESPONSE] rid=${resp.request_id} approved=${resp.approved}`;
|
||||
console.log(`Gravity Bridge: ${msg}`);
|
||||
logToFile(msg);
|
||||
|
||||
// Find matching pending request for session_id
|
||||
const pendingDir = path.join(bridgePath, 'pending');
|
||||
@@ -402,56 +381,74 @@ async function processResponseFile(filePath: string) {
|
||||
}
|
||||
|
||||
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' },
|
||||
// STRATEGY: Try SDK rawRPC first (has CSRF auth), then VS Code commands
|
||||
// Phase 1: SDK rawRPC (requires active SDK connection with CSRF)
|
||||
if (sdk && sessionId) {
|
||||
const rpcMethods = [
|
||||
{ method: 'HandleCascadeUserInteraction', params: { cascadeId: sessionId, approved: true } },
|
||||
{ method: 'ResolveOutstandingSteps', params: { cascadeId: sessionId } },
|
||||
];
|
||||
for (const { method, params } of rpcMethods) {
|
||||
try {
|
||||
const result = await sdk.ls.rawRPC(method, params);
|
||||
const log = `[RESPONSE] RPC OK ${method}: ${JSON.stringify(result).substring(0, 100)}`;
|
||||
logToFile(log);
|
||||
} catch (e: any) {
|
||||
const log = `[RESPONSE] RPC FAIL ${method}: ${e.message}`;
|
||||
logToFile(log);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
logToFile(`[RESPONSE] SDK unavailable (sdk=${!!sdk}, session=${sessionId})`);
|
||||
}
|
||||
|
||||
// Phase 2: VS Code commands (may or may not work depending on UI focus)
|
||||
const cmds = [
|
||||
'antigravity.terminalCommand.run',
|
||||
'antigravity.terminalCommand.accept',
|
||||
'antigravity.command.accept',
|
||||
'antigravity.agent.acceptAgentStep',
|
||||
];
|
||||
let approved = false;
|
||||
for (const { cmd, label } of approvalCommands) {
|
||||
for (const cmd of cmds) {
|
||||
try {
|
||||
await vscode.commands.executeCommand(cmd);
|
||||
console.log(`Gravity Bridge: [RESPONSE] ✅ approved via ${label}`);
|
||||
approved = true;
|
||||
break;
|
||||
logToFile(`[RESPONSE] CMD OK ${cmd}`);
|
||||
} catch (e: any) {
|
||||
console.log(`Gravity Bridge: [RESPONSE] ${label} failed: ${e.message}`);
|
||||
logToFile(`[RESPONSE] CMD FAIL ${cmd}: ${e.message}`);
|
||||
}
|
||||
}
|
||||
if (!approved) {
|
||||
console.log('Gravity Bridge: [RESPONSE] ⚠️ all approval methods failed');
|
||||
}
|
||||
logToFile('[RESPONSE] all approve attempts done');
|
||||
} else {
|
||||
// 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) {
|
||||
// REJECT
|
||||
if (sdk && sessionId) {
|
||||
try {
|
||||
await vscode.commands.executeCommand(cmd);
|
||||
console.log(`Gravity Bridge: [RESPONSE] ❌ rejected via ${label}`);
|
||||
rejected = true;
|
||||
break;
|
||||
await sdk.ls.rawRPC('HandleCascadeUserInteraction', { cascadeId: sessionId, approved: false });
|
||||
logToFile('[RESPONSE] RPC reject OK');
|
||||
} catch (e: any) {
|
||||
console.log(`Gravity Bridge: [RESPONSE] ${label} failed: ${e.message}`);
|
||||
logToFile(`[RESPONSE] RPC reject FAIL: ${e.message}`);
|
||||
}
|
||||
}
|
||||
if (!rejected) {
|
||||
console.log('Gravity Bridge: [RESPONSE] ⚠️ all reject methods failed');
|
||||
const cmds = [
|
||||
'antigravity.terminalCommand.reject',
|
||||
'antigravity.command.reject',
|
||||
'antigravity.agent.rejectAgentStep',
|
||||
];
|
||||
for (const cmd of cmds) {
|
||||
try {
|
||||
await vscode.commands.executeCommand(cmd);
|
||||
logToFile(`[RESPONSE] CMD OK ${cmd}`);
|
||||
} catch (e: any) {
|
||||
logToFile(`[RESPONSE] CMD FAIL ${cmd}: ${e.message}`);
|
||||
}
|
||||
}
|
||||
logToFile('[RESPONSE] all reject attempts done');
|
||||
}
|
||||
|
||||
try { fs.unlinkSync(filePath); } catch { }
|
||||
} catch (e: any) {
|
||||
console.log(`Gravity Bridge: [RESPONSE] error: ${e.message}`);
|
||||
const log = `[RESPONSE] error: ${e.message}`;
|
||||
console.log(`Gravity Bridge: ${log}`);
|
||||
logToFile(log);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user