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:
2026-03-08 09:29:40 +09:00
parent c98b6432f8
commit e7bc4046a4
3 changed files with 144 additions and 146 deletions

View File

@@ -51,6 +51,17 @@ const fs = __importStar(require("fs"));
const path = __importStar(require("path"));
const os = __importStar(require("os"));
const cp = __importStar(require("child_process"));
// ─── File-based logging (AI can read directly) ───
function logToFile(msg) {
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;
let sdk;
@@ -275,10 +286,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++;
try {
@@ -303,7 +310,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) {
activeSessionId = bestSessionId;
@@ -311,43 +317,16 @@ 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;
lastKnownStepCount = currentCount;
@@ -409,7 +388,9 @@ async function processResponseFile(filePath) {
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');
const pendingFile = path.join(pendingDir, `${resp.request_id}.json`);
@@ -422,54 +403,72 @@ async function processResponseFile(filePath) {
catch { }
}
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) {
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) {
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) {
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) {
logToFile(`[RESPONSE] CMD FAIL ${cmd}: ${e.message}`);
}
}
logToFile('[RESPONSE] all reject attempts done');
}
try {
fs.unlinkSync(filePath);
@@ -477,7 +476,9 @@ async function processResponseFile(filePath) {
catch { }
}
catch (e) {
console.log(`Gravity Bridge: [RESPONSE] error: ${e.message}`);
const log = `[RESPONSE] error: ${e.message}`;
console.log(`Gravity Bridge: ${log}`);
logToFile(log);
}
}
/**

File diff suppressed because one or more lines are too long

View File

@@ -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);
}
}