fix(bridge): stall-based approval detection + known issues from deep debugging
- IDLE→stall detection: RUNNING+delta=0 for 6 polls (30s) - lastModifiedTime-based thinking filter (partial) - ResolveOutstandingSteps confirmed CANCELS steps (removed) - HandleCascadeUserInteraction always socket hang up (removed) - VS Code accept commands: silent success, no effect - Hybrid approval: focus+all commands sequential, no break - logToFile: console.log backup added - Known issues: 4 critical findings documented - better-antigravity reference added for future research
This commit is contained in:
@@ -18,12 +18,16 @@ import * as cp from 'child_process';
|
||||
|
||||
// ─── File-based logging (AI can read directly) ───
|
||||
function logToFile(msg: string) {
|
||||
const ts = new Date().toISOString().replace('T', ' ').substring(0, 19);
|
||||
const line = `${ts} ${msg}`;
|
||||
console.log(`Gravity Bridge: ${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 { }
|
||||
fs.appendFileSync(logFile, line + '\n', 'utf-8');
|
||||
} catch (e: any) {
|
||||
console.error(`Gravity Bridge LOG WRITE FAIL: ${e.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// antigravity-sdk embedded locally (src/sdk/)
|
||||
@@ -201,6 +205,299 @@ async function initSDK(context: vscode.ExtensionContext): Promise<boolean> {
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Approval Observer via SDK IntegrationManager ───
|
||||
|
||||
async function setupApprovalObserver() {
|
||||
if (!sdk) { logToFile('[OBSERVER] no SDK'); return; }
|
||||
try {
|
||||
const integration = sdk.integration;
|
||||
if (!integration) { logToFile('[OBSERVER] sdk.integration unavailable'); return; }
|
||||
|
||||
// 1. Start HTTP bridge server in Extension Host
|
||||
const bridgePort = await startObserverHttpBridge();
|
||||
if (!bridgePort) { logToFile('[OBSERVER] HTTP bridge failed'); return; }
|
||||
|
||||
// 2. Register a TOP_BAR button so build() works
|
||||
try {
|
||||
integration.register({
|
||||
id: 'gravity_bridge_status',
|
||||
point: 'topBar',
|
||||
icon: '🌉',
|
||||
tooltip: 'Gravity Bridge Active',
|
||||
});
|
||||
} catch { /* already registered */ }
|
||||
|
||||
// 3. Write renderer script with HTTP fetch() approach
|
||||
const observerJS = generateApprovalObserverScript(bridgePort);
|
||||
const patcher = (integration as any)._patcher;
|
||||
if (patcher && typeof patcher.getScriptPath === 'function') {
|
||||
let baseScript = '';
|
||||
try { baseScript = integration.build(); } catch { baseScript = ''; }
|
||||
const combinedScript = baseScript + '\n' + observerJS;
|
||||
const scriptPath = patcher.getScriptPath();
|
||||
fs.writeFileSync(scriptPath, combinedScript, 'utf8');
|
||||
logToFile(`[OBSERVER] script written → ${scriptPath} (port=${bridgePort})`);
|
||||
if (!integration.isInstalled()) {
|
||||
patcher.install(combinedScript);
|
||||
logToFile('[OBSERVER] workbench.html patched (needs reload)');
|
||||
}
|
||||
|
||||
// Also patch workbench-jetski-agent.html (Antigravity's actual entry point!)
|
||||
const scriptDir = path.dirname(scriptPath);
|
||||
const jetskiHtml = path.join(scriptDir, 'workbench-jetski-agent.html');
|
||||
const scriptBasename = path.basename(scriptPath);
|
||||
try {
|
||||
if (fs.existsSync(jetskiHtml)) {
|
||||
let html = fs.readFileSync(jetskiHtml, 'utf8');
|
||||
if (!html.includes(scriptBasename)) {
|
||||
html = html.replace('</html>',
|
||||
`\n<!-- AG SDK [variet-gravity-bridge] -->\n<script src="./${scriptBasename}"></script>\n<!-- /AG SDK [variet-gravity-bridge] -->\n</html>`);
|
||||
fs.writeFileSync(jetskiHtml, html, 'utf8');
|
||||
logToFile('[OBSERVER] workbench-jetski-agent.html PATCHED');
|
||||
} else {
|
||||
logToFile('[OBSERVER] workbench-jetski-agent.html already has script tag');
|
||||
}
|
||||
}
|
||||
} catch (e: any) {
|
||||
logToFile(`[OBSERVER] jetski patch error: ${e.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
try { integration.enableAutoRepair(); } catch { }
|
||||
setInterval(() => { try { integration.signalActive(); } catch { } }, 30_000);
|
||||
|
||||
logToFile(`[OBSERVER] setup complete (HTTP bridge on port ${bridgePort})`);
|
||||
console.log(`Gravity Bridge: ✅ Approval observer installed (port ${bridgePort})`);
|
||||
} catch (err: any) {
|
||||
logToFile(`[OBSERVER] setup error: ${err.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// ─── HTTP Bridge Server (Extension Host → Renderer communication) ───
|
||||
|
||||
let observerHttpServer: any = null;
|
||||
const pendingResponses = new Map<string, { approved: boolean } | null>();
|
||||
|
||||
function startObserverHttpBridge(): Promise<number> {
|
||||
return new Promise((resolve) => {
|
||||
try {
|
||||
const http = require('http');
|
||||
const server = http.createServer((req: any, res: any) => {
|
||||
// CORS headers for renderer fetch()
|
||||
res.setHeader('Access-Control-Allow-Origin', '*');
|
||||
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
|
||||
res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
|
||||
if (req.method === 'OPTIONS') { res.writeHead(200); res.end(); return; }
|
||||
|
||||
const url = new URL(req.url, `http://127.0.0.1`);
|
||||
|
||||
// POST /pending — renderer reports a detected approval button
|
||||
if (req.method === 'POST' && url.pathname === '/pending') {
|
||||
let body = '';
|
||||
req.on('data', (c: string) => body += c);
|
||||
req.on('end', () => {
|
||||
try {
|
||||
const data = JSON.parse(body);
|
||||
const rid = data.request_id || Date.now().toString();
|
||||
// Write pending file for Discord bot
|
||||
const pendingDir = path.join(bridgePath, 'pending');
|
||||
if (!fs.existsSync(pendingDir)) fs.mkdirSync(pendingDir, { recursive: true });
|
||||
const pending = {
|
||||
...data,
|
||||
request_id: rid,
|
||||
timestamp: Date.now() / 1000,
|
||||
status: 'pending',
|
||||
project_name: projectName,
|
||||
auto_detected: true,
|
||||
source: 'dom_observer',
|
||||
};
|
||||
fs.writeFileSync(path.join(pendingDir, `${rid}.json`), JSON.stringify(pending, null, 2));
|
||||
logToFile(`[HTTP] pending created: ${rid} cmd="${data.command}" ctx="${(data.description || '').substring(0, 50)}"`);
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ ok: true, request_id: rid }));
|
||||
} catch (e: any) {
|
||||
logToFile(`[HTTP] pending error: ${e.message}`);
|
||||
res.writeHead(400); res.end(JSON.stringify({ error: e.message }));
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// GET /response/:rid — renderer polls for Discord approval
|
||||
if (req.method === 'GET' && url.pathname.startsWith('/response/')) {
|
||||
const rid = url.pathname.split('/')[2];
|
||||
const respFile = path.join(bridgePath, 'response', `${rid}.json`);
|
||||
if (fs.existsSync(respFile)) {
|
||||
try {
|
||||
const data = JSON.parse(fs.readFileSync(respFile, 'utf8'));
|
||||
fs.unlinkSync(respFile);
|
||||
logToFile(`[HTTP] response sent: ${rid} approved=${data.approved}`);
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify(data));
|
||||
} catch {
|
||||
res.writeHead(200); res.end(JSON.stringify({ waiting: true }));
|
||||
}
|
||||
} else {
|
||||
res.writeHead(200); res.end(JSON.stringify({ waiting: true }));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// GET /ping — health check
|
||||
if (url.pathname === '/ping') {
|
||||
res.writeHead(200); res.end('pong');
|
||||
return;
|
||||
}
|
||||
|
||||
res.writeHead(404); res.end('not found');
|
||||
});
|
||||
|
||||
// Listen on random port
|
||||
server.listen(0, '127.0.0.1', () => {
|
||||
const port = server.address().port;
|
||||
observerHttpServer = server;
|
||||
logToFile(`[HTTP] bridge server started on port ${port}`);
|
||||
|
||||
// Write port to workbench dir so renderer can read it via XHR
|
||||
const patcher = (sdk.integration as any)?._patcher;
|
||||
if (patcher && typeof patcher.getWorkbenchDir === 'function') {
|
||||
const portFile = path.join(patcher.getWorkbenchDir(), 'ag-bridge-port');
|
||||
fs.writeFileSync(portFile, port.toString(), 'utf8');
|
||||
logToFile(`[HTTP] port written → ${portFile}`);
|
||||
}
|
||||
|
||||
resolve(port);
|
||||
});
|
||||
|
||||
server.on('error', (e: any) => {
|
||||
logToFile(`[HTTP] server error: ${e.message}`);
|
||||
resolve(0);
|
||||
});
|
||||
} catch (e: any) {
|
||||
logToFile(`[HTTP] server failed: ${e.message}`);
|
||||
resolve(0);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ─── Renderer Script (uses fetch() — no Node.js APIs) ───
|
||||
|
||||
function generateApprovalObserverScript(_port: number): string {
|
||||
// Port is NOT hardcoded — renderer reads it dynamically from ag-bridge-port file via XHR
|
||||
return `
|
||||
// ── Gravity Bridge: Approval Observer (renderer-side, dynamic port) ──
|
||||
(function(){
|
||||
'use strict';
|
||||
var BASE='',_lastTs=0,_obs=false,_sent={},_ready=false;
|
||||
|
||||
function log(m){console.log('[GB Observer] '+m);}
|
||||
log('Script loaded — discovering bridge port...');
|
||||
|
||||
// ── Dynamic Port Discovery (like SDK heartbeat) ──
|
||||
function discoverPort(cb){
|
||||
var attempts=0;
|
||||
var timer=setInterval(function(){
|
||||
attempts++;
|
||||
if(attempts>30){clearInterval(timer);log('Port discovery timeout');return;}
|
||||
try{
|
||||
var xhr=new XMLHttpRequest();
|
||||
xhr.open('GET','./ag-bridge-port?t='+Date.now(),false);
|
||||
xhr.send();
|
||||
if(xhr.status===200){
|
||||
var port=parseInt(xhr.responseText.trim(),10);
|
||||
if(port>0&&port<65536){
|
||||
clearInterval(timer);
|
||||
log('Port discovered: '+port);
|
||||
cb(port);
|
||||
}
|
||||
}
|
||||
}catch(e){}
|
||||
},2000);
|
||||
}
|
||||
|
||||
discoverPort(function(port){
|
||||
BASE='http://127.0.0.1:'+port;
|
||||
// Verify bridge is alive
|
||||
fetch(BASE+'/ping').then(function(r){return r.text();}).then(function(t){
|
||||
if(t==='pong'){log('Bridge connected on port '+port);_ready=true;startObserver();}
|
||||
else log('Bridge ping failed: '+t);
|
||||
}).catch(function(e){log('Bridge unreachable: '+e.message);});
|
||||
});
|
||||
|
||||
var PATS=[
|
||||
{sel:'button',re:/^Run$/i,type:'terminal_command'},
|
||||
{sel:'button',re:/^Accept/i,type:'agent_step'},
|
||||
{sel:'button',re:/^Allow/i,type:'permission'},
|
||||
{sel:'button',re:/^Continue$/i,type:'continue'},
|
||||
];
|
||||
|
||||
function ctx(b){
|
||||
var p=b.closest('[class*="step"]')||b.closest('[class*="action"]')||b.parentElement;
|
||||
if(!p)return '';
|
||||
var c=p.querySelector('pre,code,[class*="command"],[class*="terminal"]');
|
||||
if(c)return(c.textContent||'').trim().substring(0,200);
|
||||
return(p.textContent||'').replace((b.textContent||''),'').trim().substring(0,200);
|
||||
}
|
||||
|
||||
function scan(){
|
||||
if(!_ready)return;
|
||||
var now=Date.now();if(now-_lastTs<1000)return;
|
||||
var panel=document.querySelector('#jetski-agent-panel,.antigravity-agent-side-panel,[class*="agent-panel"]');
|
||||
if(!panel)return;
|
||||
for(var i=0;i<PATS.length;i++){
|
||||
var pat=PATS[i],btns=panel.querySelectorAll(pat.sel);
|
||||
for(var j=0;j<btns.length;j++){
|
||||
var b=btns[j],txt=(b.textContent||'').trim();
|
||||
if(!pat.re.test(txt)||b.disabled)continue;
|
||||
var bid=pat.type+'_'+txt+'_'+Math.round(b.getBoundingClientRect().top);
|
||||
if(_sent[bid])continue;
|
||||
var desc=ctx(b),rid=now.toString();
|
||||
_sent[bid]=rid;_lastTs=now;
|
||||
log('FOUND '+pat.type+': "'+txt+'" → sending to bridge');
|
||||
(function(rid2,b2,bid2){
|
||||
fetch(BASE+'/pending',{
|
||||
method:'POST',
|
||||
headers:{'Content-Type':'application/json'},
|
||||
body:JSON.stringify({request_id:rid2,command:txt,description:desc,step_type:pat.type})
|
||||
}).then(function(r){return r.json();}).then(function(d){
|
||||
log('Pending created: '+d.request_id);
|
||||
pollResp(d.request_id,b2,bid2);
|
||||
}).catch(function(e){log('POST error: '+e.message);delete _sent[bid2];});
|
||||
})(rid,b,bid);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function pollResp(rid,b,bid){
|
||||
var n=0,t=setInterval(function(){
|
||||
n++;if(n>600){clearInterval(t);delete _sent[bid];return;}
|
||||
fetch(BASE+'/response/'+rid).then(function(r){return r.json();}).then(function(d){
|
||||
if(d.waiting)return;
|
||||
clearInterval(t);
|
||||
if(d.approved){log('APPROVED '+rid+' → clicking');b.click();}
|
||||
else{
|
||||
log('REJECTED '+rid);
|
||||
var p=b.closest('[class*="step"]')||b.parentElement;
|
||||
if(p){var rb=p.querySelectorAll('button');
|
||||
for(var k=0;k<rb.length;k++){var rt=(rb[k].textContent||'').trim().toLowerCase();
|
||||
if(rt==='reject'||rt==='cancel'||rt==='deny'){rb[k].click();break;}}}
|
||||
}
|
||||
delete _sent[bid];
|
||||
}).catch(function(){});
|
||||
},500);
|
||||
}
|
||||
|
||||
function startObserver(){
|
||||
if(_obs)return;
|
||||
new MutationObserver(function(){scan();}).observe(document.body,{childList:true,subtree:true});
|
||||
setInterval(scan,2000);
|
||||
_obs=true;log('Observer active — watching for approval buttons');
|
||||
}
|
||||
})();
|
||||
`;
|
||||
}
|
||||
|
||||
// Track last seen step per session to avoid re-fetching
|
||||
const lastSeenStep = new Map<string, number>();
|
||||
const lastSnapshotText = new Map<string, string>();
|
||||
@@ -255,12 +552,22 @@ function setupMonitor() {
|
||||
let lastNotifyStepIndex = -1;
|
||||
let lastTaskStepIndex = -1;
|
||||
let lastPendingStepIndex = -1; // dedup: don't re-create pending for same step
|
||||
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
|
||||
let lastModTime = ''; // track lastModifiedTime to distinguish thinking vs approval
|
||||
|
||||
setInterval(async () => {
|
||||
pollCount++;
|
||||
if (pollCount <= 3 || pollCount % 12 === 0) {
|
||||
logToFile(`[POLL#${pollCount}] alive`);
|
||||
}
|
||||
try {
|
||||
const allTraj = await sdk.ls.rawRPC('GetAllCascadeTrajectories', {});
|
||||
if (!allTraj?.trajectorySummaries) return;
|
||||
if (!allTraj?.trajectorySummaries) {
|
||||
if (pollCount <= 3) logToFile('[POLL] no trajectorySummaries');
|
||||
return;
|
||||
}
|
||||
|
||||
let bestSession: any = null;
|
||||
let bestSessionId = '';
|
||||
@@ -299,106 +606,74 @@ function setupMonitor() {
|
||||
console.log(`Gravity Bridge: [POLL#${pollCount}] +${delta} steps (${currentCount}) "${currentTitle}"`);
|
||||
}
|
||||
|
||||
// ── IMMEDIATE PENDING DETECTION ──
|
||||
// On EVERY poll: check last 3 steps for non-DONE status
|
||||
// This catches: file review, file access permission, command approval
|
||||
if (isRunning) {
|
||||
try {
|
||||
const stepsResp = await sdk.ls.rawRPC('GetCascadeTrajectorySteps', { cascadeId: bestSessionId });
|
||||
const steps = stepsResp?.steps || [];
|
||||
if (steps.length > 0) {
|
||||
// Check last 3 steps (some may be in-flight)
|
||||
const checkCount = Math.min(3, steps.length);
|
||||
for (let i = steps.length - checkCount; i < steps.length; i++) {
|
||||
const step = steps[i];
|
||||
const stepStatus = (step.status || '').replace('CORTEX_STEP_STATUS_', '');
|
||||
const stepType = (step.type || '').replace('CORTEX_STEP_TYPE_', '');
|
||||
const stepIdx = step.metadata?.sourceTrajectoryStepInfo?.stepIndex ?? i;
|
||||
// Log session state on EVERY poll for diagnostics
|
||||
const statusStr = String(bestSession.status || 'UNKNOWN');
|
||||
if (pollCount <= 10 || pollCount % 6 === 0 || delta > 0) {
|
||||
logToFile(`[POLL#${pollCount}] status=${statusStr} steps=${currentCount} delta=${delta}`);
|
||||
}
|
||||
|
||||
// Skip already-handled steps
|
||||
if (stepIdx <= lastPendingStepIndex) continue;
|
||||
// Skip completed/rejected steps
|
||||
if (stepStatus === 'DONE' || stepStatus === 'REJECTED') continue;
|
||||
// ── Stall-based approval detection ──
|
||||
// INSIGHT: Both thinking and approval show RUNNING+delta=0.
|
||||
// DIFFERENTIATOR: lastModifiedTime
|
||||
// - Thinking: lastModifiedTime KEEPS CHANGING (server actively processing)
|
||||
// - Approval wait: lastModifiedTime FROZEN (server idle, waiting for user)
|
||||
|
||||
// ── Non-DONE step found! Create pending based on type ──
|
||||
let cmd = '';
|
||||
let desc = '';
|
||||
const toolName = step.metadata?.toolCall?.name || '';
|
||||
let argsJson = '';
|
||||
try { argsJson = step.metadata?.toolCall?.argumentsJson || ''; } catch { }
|
||||
// DEBUG: dump session keys on first poll to find modTime field
|
||||
if (pollCount === 1) {
|
||||
const keys = Object.keys(bestSession).filter(k => !['latestNotifyUserStep', 'latestTaskBoundaryStep', 'latestToolCallStep'].includes(k));
|
||||
logToFile(`[DEBUG] session keys: ${keys.join(', ')}`);
|
||||
logToFile(`[DEBUG] lastModifiedTime=${bestSession.lastModifiedTime}, lastModifiedTimestamp=${(bestSession as any).lastModifiedTimestamp}, modifiedTime=${(bestSession as any).modifiedTime}`);
|
||||
}
|
||||
|
||||
if (toolName === 'run_command' || toolName === 'send_command_input') {
|
||||
// Command execution approval
|
||||
try {
|
||||
const args = JSON.parse(argsJson || '{}');
|
||||
cmd = args.CommandLine || args.command || args.Input || toolName;
|
||||
} catch { cmd = toolName; }
|
||||
desc = `명령어 실행 승인 (${stepType})`;
|
||||
} else if (toolName === 'browser_subagent') {
|
||||
// Browser subagent
|
||||
try {
|
||||
const args = JSON.parse(argsJson || '{}');
|
||||
cmd = args.Task?.substring(0, 150) || 'browser task';
|
||||
} catch { cmd = 'browser_subagent'; }
|
||||
desc = `브라우저 서브에이전트 실행`;
|
||||
} else if (stepType === 'CODE_ACTION' || toolName === 'replace_file_content' || toolName === 'multi_replace_file_content' || toolName === 'write_to_file') {
|
||||
// File modification review
|
||||
try {
|
||||
const args = JSON.parse(argsJson || '{}');
|
||||
cmd = args.TargetFile || args.target_file || toolName;
|
||||
} catch { cmd = toolName; }
|
||||
desc = `파일 수정 검토 요청`;
|
||||
} else if (toolName === 'view_file' || toolName === 'view_file_outline' || toolName === 'view_code_item') {
|
||||
// File access (usually auto-approved, but handle if pending)
|
||||
try {
|
||||
const args = JSON.parse(argsJson || '{}');
|
||||
cmd = args.AbsolutePath || args.File || toolName;
|
||||
} catch { cmd = toolName; }
|
||||
desc = `파일 접근 권한 요청`;
|
||||
} else if (toolName === 'notify_user') {
|
||||
// AI asking for user feedback — this needs a different response
|
||||
try {
|
||||
const args = JSON.parse(argsJson || '{}');
|
||||
cmd = args.Message?.substring(0, 200) || 'notify_user';
|
||||
} catch { cmd = 'notify_user'; }
|
||||
desc = `사용자 피드백 요청`;
|
||||
} else if (toolName) {
|
||||
cmd = toolName;
|
||||
desc = `도구 실행: ${toolName}`;
|
||||
} else {
|
||||
cmd = stepType || 'unknown';
|
||||
desc = `${stepType} (${stepStatus})`;
|
||||
}
|
||||
const currentModTime = bestSession.lastModifiedTime || (bestSession as any).lastModifiedTimestamp || (bestSession as any).modifiedTime || '';
|
||||
const modTimeChanged = currentModTime !== lastModTime;
|
||||
const isStall = isRunning && delta === 0;
|
||||
|
||||
// Create pending for Discord
|
||||
const rid = Date.now().toString();
|
||||
const pending = {
|
||||
request_id: rid,
|
||||
conversation_id: bestSessionId,
|
||||
command: cmd.substring(0, 500),
|
||||
description: desc.substring(0, 200),
|
||||
timestamp: Date.now() / 1000,
|
||||
status: 'pending',
|
||||
project_name: projectName,
|
||||
step_index: stepIdx,
|
||||
step_type: stepType,
|
||||
step_status: stepStatus,
|
||||
tool_name: toolName,
|
||||
auto_detected: true,
|
||||
};
|
||||
const pendingDir = path.join(bridgePath, 'pending');
|
||||
fs.writeFileSync(path.join(pendingDir, `${rid}.json`), JSON.stringify(pending, null, 2));
|
||||
lastPendingStepIndex = stepIdx;
|
||||
logToFile(`[PENDING] step=${stepIdx} type=${stepType} status=${stepStatus} tool=${toolName} cmd=${cmd.substring(0, 60)}`);
|
||||
console.log(`Gravity Bridge: [POLL#${pollCount}] PENDING! step=${stepIdx} ${toolName || stepType} → pending/${rid}.json`);
|
||||
}
|
||||
}
|
||||
} catch (e: any) {
|
||||
// Only log occasionally to avoid spam
|
||||
if (pollCount % 10 === 0) {
|
||||
logToFile(`[PENDING] step query error: ${e.message}`);
|
||||
// Log modTime on stalls for debugging
|
||||
if (isStall && consecutiveIdleCount < 8) {
|
||||
logToFile(`[STALL-DBG] idle=${consecutiveIdleCount} modTime='${currentModTime}' changed=${modTimeChanged}`);
|
||||
}
|
||||
|
||||
if (delta > 0) {
|
||||
consecutiveIdleCount = 0;
|
||||
sawRunningAfterPending = true;
|
||||
lastModTime = currentModTime;
|
||||
} else if (isStall) {
|
||||
if (modTimeChanged) {
|
||||
// lastModifiedTime is still changing = AI is thinking, NOT approval
|
||||
consecutiveIdleCount = 0; // Reset!
|
||||
if (pollCount <= 10 || pollCount % 12 === 0) {
|
||||
logToFile(`[THINK] step=${currentCount} modTime changing → not stall`);
|
||||
}
|
||||
} else {
|
||||
// lastModifiedTime frozen = real stall (approval waiting)
|
||||
consecutiveIdleCount++;
|
||||
}
|
||||
lastModTime = currentModTime;
|
||||
|
||||
const now = Date.now();
|
||||
const cooldownOk = (now - lastPendingTime) > 60_000;
|
||||
|
||||
if (consecutiveIdleCount >= 6 && sawRunningAfterPending && cooldownOk) {
|
||||
// 6 polls × 5s = 30 seconds of FROZEN stall = approval waiting
|
||||
lastPendingStepIndex = currentCount;
|
||||
lastPendingTime = now;
|
||||
sawRunningAfterPending = false;
|
||||
|
||||
const command = `Stall at step ${currentCount}`;
|
||||
const description = `승인 대기 감지 (${consecutiveIdleCount * 5}초 정지), Title: "${currentTitle}"`;
|
||||
|
||||
logToFile(`[STALL] step=${currentCount} frozenCount=${consecutiveIdleCount} → pending`);
|
||||
writePendingApproval({ conversation_id: activeSessionId, command, description });
|
||||
} else if (consecutiveIdleCount === 6) {
|
||||
const reasons = [];
|
||||
if (!sawRunningAfterPending) reasons.push('needDelta>0');
|
||||
if (!cooldownOk) reasons.push(`cooldown(${Math.round((60000 - (now - lastPendingTime)) / 1000)}s)`);
|
||||
if (reasons.length > 0) logToFile(`[STALL] SKIP: ${reasons.join(', ')}`);
|
||||
}
|
||||
} else if (!isRunning) {
|
||||
consecutiveIdleCount = 0;
|
||||
lastModTime = currentModTime;
|
||||
}
|
||||
|
||||
// ── Process latestNotifyUserStep ──
|
||||
@@ -475,42 +750,72 @@ async function processResponseFile(filePath: string) {
|
||||
} catch { }
|
||||
}
|
||||
|
||||
if (resp.approved) {
|
||||
// Step 1: Focus Antigravity panel — webview MUST be active for commands to work
|
||||
// acceptAgentStep dispatches via postMessage to Chat Client webview
|
||||
try {
|
||||
await vscode.commands.executeCommand('antigravity.agentPanel.focus');
|
||||
logToFile('[RESPONSE] panel focused');
|
||||
} catch (e: any) {
|
||||
logToFile(`[RESPONSE] panel focus failed: ${e.message}`);
|
||||
}
|
||||
// Wait for webview to initialize
|
||||
await new Promise(r => setTimeout(r, 500));
|
||||
// ═══ APPROVAL STRATEGY (VS Code Commands Only) ═══
|
||||
// Phase 0 ResolveOutstandingSteps: REMOVED — confirmed it CANCELS steps!
|
||||
// Phase 1 HandleCascadeUserInteraction: REMOVED — always gets "socket hang up"
|
||||
// Phase 2: ALL VS Code commands sequentially (no break on "success")
|
||||
|
||||
// Step 2: Accept — only acceptAgentStep (the universal approval command)
|
||||
try {
|
||||
await vscode.commands.executeCommand('antigravity.agent.acceptAgentStep');
|
||||
logToFile('[RESPONSE] acceptAgentStep sent');
|
||||
} catch (e: any) {
|
||||
logToFile(`[RESPONSE] acceptAgentStep failed: ${e.message}`);
|
||||
}
|
||||
logToFile('[RESPONSE] approve done');
|
||||
} else {
|
||||
// REJECT — same pattern: focus first, then reject
|
||||
const approved = resp.approved;
|
||||
|
||||
// Focus panel with multiple attempts + longer delay
|
||||
for (let i = 0; i < 2; i++) {
|
||||
try {
|
||||
await vscode.commands.executeCommand('antigravity.agentPanel.focus');
|
||||
} catch { }
|
||||
await new Promise(r => setTimeout(r, 500));
|
||||
try {
|
||||
await vscode.commands.executeCommand('antigravity.agent.rejectAgentStep');
|
||||
logToFile('[RESPONSE] rejectAgentStep sent');
|
||||
if (i === 0) logToFile('[RESPONSE] panel focus attempt 1');
|
||||
} catch (e: any) {
|
||||
logToFile(`[RESPONSE] rejectAgentStep failed: ${e.message}`);
|
||||
logToFile(`[RESPONSE] panel focus attempt ${i + 1} failed: ${e.message}`);
|
||||
}
|
||||
logToFile('[RESPONSE] reject done');
|
||||
await new Promise(r => setTimeout(r, 500));
|
||||
}
|
||||
|
||||
// Phase 2: Sequential VS Code commands (MUST try ALL — no break!)
|
||||
// Focus panel first
|
||||
try {
|
||||
await vscode.commands.executeCommand('antigravity.agentPanel.focus');
|
||||
logToFile('[RESPONSE] panel focused');
|
||||
} catch (e: any) {
|
||||
logToFile(`[RESPONSE] panel focus failed: ${e.message}`);
|
||||
}
|
||||
await new Promise(r => setTimeout(r, 500));
|
||||
|
||||
if (approved) {
|
||||
const approveCommands = [
|
||||
'antigravity.interactiveCascade.acceptSuggestedAction',
|
||||
'antigravity.terminalCommand.run',
|
||||
'antigravity.terminalCommand.accept',
|
||||
'antigravity.command.accept',
|
||||
'antigravity.agent.acceptAgentStep',
|
||||
];
|
||||
for (const cmd of approveCommands) {
|
||||
try {
|
||||
await vscode.commands.executeCommand(cmd);
|
||||
logToFile(`[RESPONSE] cmd OK: ${cmd}`);
|
||||
} catch (e: any) {
|
||||
logToFile(`[RESPONSE] cmd FAIL: ${cmd} → ${e.message}`);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const rejectCommands = [
|
||||
'antigravity.interactiveCascade.rejectSuggestedAction',
|
||||
'antigravity.terminalCommand.reject',
|
||||
'antigravity.command.reject',
|
||||
'antigravity.agent.rejectAgentStep',
|
||||
];
|
||||
for (const cmd of rejectCommands) {
|
||||
try {
|
||||
await vscode.commands.executeCommand(cmd);
|
||||
logToFile(`[RESPONSE] cmd OK: ${cmd}`);
|
||||
} catch (e: any) {
|
||||
logToFile(`[RESPONSE] cmd FAIL: ${cmd} → ${e.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
logToFile(`[RESPONSE] ${approved ? 'approve' : 'reject'} done`);
|
||||
|
||||
// Cleanup
|
||||
try { fs.unlinkSync(filePath); } catch { }
|
||||
try { if (fs.existsSync(pendingFile)) fs.unlinkSync(pendingFile); } catch { }
|
||||
} catch (e: any) {
|
||||
const log = `[RESPONSE] error: ${e.message}`;
|
||||
console.log(`Gravity Bridge: ${log}`);
|
||||
@@ -638,8 +943,9 @@ export async function activate(context: vscode.ExtensionContext) {
|
||||
|
||||
if (sdkReady) {
|
||||
setupMonitor(); // Now just logs that monitor is disabled
|
||||
setupApprovalObserver(); // DOM observer via SDK IntegrationManager
|
||||
statusBar.text = '$(check) Bridge';
|
||||
statusBar.tooltip = `Gravity Bridge: ${projectName} (POLL active)`;
|
||||
statusBar.tooltip = `Gravity Bridge: ${projectName} (POLL + Observer active)`;
|
||||
|
||||
// Register SDK-powered commands
|
||||
context.subscriptions.push(
|
||||
|
||||
Reference in New Issue
Block a user