feat(bridge): step-type-specific approval commands + SDK research
- tryApprovalStrategies: terminalCommand.run > terminalCommand.accept > command.accept > acceptAgentStep - Step probe: immediate on first stall (5s), 775-limit detection with dynamic fallback - NOTIFY filter: skip <50 chars, TASK dedup by taskName+taskStatus - BTN-DUMP diagnostic removed from renderer - Focus: agentPanel.focus + agentSidePanel.focus (verified SDK commands) - known-issues: add step-type command mismatch finding
This commit is contained in:
@@ -689,23 +689,6 @@ function generateApprovalObserverScript(_port) {
|
||||
if(document.body)searchRoots.push(document.body);
|
||||
if(!searchRoots.length)return;
|
||||
|
||||
// ── DIAGNOSTIC: dump ALL button-like elements EVERY scan cycle ──
|
||||
{
|
||||
var dumpBtns=[];
|
||||
var totalChecked=0;
|
||||
for(var dr=0;dr<searchRoots.length;dr++){
|
||||
var dbs=searchRoots[dr].querySelectorAll('button,[role="button"]');
|
||||
totalChecked+=dbs.length;
|
||||
for(var di=0;di<dbs.length;di++){
|
||||
var db=dbs[di];
|
||||
var dt=(db.textContent||'').trim();
|
||||
var dtShort=dt.replace(/\\s+/g,' ').substring(0,50);
|
||||
dumpBtns.push(db.tagName+'['+dt.length+']:'+dtShort);
|
||||
}
|
||||
}
|
||||
log('[BTN-DUMP] roots='+searchRoots.length+' total='+totalChecked+' btns='+dumpBtns.length+': '+JSON.stringify(dumpBtns.slice(0,15)));
|
||||
}
|
||||
|
||||
var seen={}; // dedupe buttons across search roots
|
||||
for(var r=0;r<searchRoots.length;r++){
|
||||
var allBtns=searchRoots[r].querySelectorAll('button');
|
||||
@@ -936,6 +919,7 @@ function setupMonitor() {
|
||||
let sawRunningAfterPending = true; // gate: must see delta>0 before next pending
|
||||
let lastModTime = ''; // track lastModifiedTime to distinguish thinking vs approval
|
||||
let stallProbed = false; // prevent repeated step probes during same stall
|
||||
let lastRelayedTaskText = ''; // dedup TASK_BOUNDARY relay
|
||||
setInterval(async () => {
|
||||
pollCount++;
|
||||
if (pollCount <= 3 || pollCount % 12 === 0) {
|
||||
@@ -1012,9 +996,8 @@ function setupMonitor() {
|
||||
logToFile(`[POLL#${pollCount}] status=${statusStr} steps=${currentCount} delta=${delta}`);
|
||||
}
|
||||
// ── PRIMARY: Step-probe-based approval detection ──
|
||||
// latestToolCallStep does NOT exist in GetAllCascadeTrajectories response.
|
||||
// Instead, on early stall (idle=2, ~10s), probe GetCascadeTrajectorySteps
|
||||
// to fetch the latest step and check if it's a tool call awaiting approval.
|
||||
// On stall (idle=1, ~5s), probe GetCascadeTrajectorySteps to check WAITING.
|
||||
// 775-step limit: probe fails for long sessions → faster stall fallback.
|
||||
// ── STALL-BASED approval detection with step probe ──
|
||||
const currentModTime = bestSession.lastModifiedTime || bestSession.lastModifiedTimestamp || bestSession.modifiedTime || '';
|
||||
const modTimeChanged = currentModTime !== lastModTime;
|
||||
@@ -1046,7 +1029,7 @@ function setupMonitor() {
|
||||
// ── Step probe: on stall, fetch latest step via cascadeId (retry until WAITING found) ──
|
||||
// CONFIRMED: param='cascadeId', id=sessionId (map key from trajectorySummaries)
|
||||
// Retries every 2 polls (~10s) because RUN_COMMAND step may not be created yet
|
||||
if (consecutiveIdleCount >= 1 && consecutiveIdleCount % 2 === 1 && !stallProbed) {
|
||||
if (consecutiveIdleCount >= 1 && !stallProbed) {
|
||||
try {
|
||||
const stepsResp = await sdk.ls.rawRPC('GetCascadeTrajectorySteps', {
|
||||
cascadeId: bestSessionId,
|
||||
@@ -1055,6 +1038,9 @@ function setupMonitor() {
|
||||
const steps = stepsResp.steps;
|
||||
// Diagnostic: compare returned steps vs trajectory stepCount
|
||||
logToFile(`[STEP-PROBE] returned=${steps.length} vs trajectory.stepCount=${currentCount}`);
|
||||
if (steps.length < currentCount) {
|
||||
logToFile(`[STEP-PROBE] ⚠️ 775-limit hit! steps=${steps.length} < stepCount=${currentCount}`);
|
||||
}
|
||||
// Scan last 5 steps backwards to find WAITING (RUN_COMMAND may not be last)
|
||||
let foundWaiting = false;
|
||||
for (let si = steps.length - 1; si >= Math.max(0, steps.length - 5); si--) {
|
||||
@@ -1116,8 +1102,9 @@ function setupMonitor() {
|
||||
}
|
||||
const now = Date.now();
|
||||
const cooldownOk = (now - lastPendingTime) > 60_000;
|
||||
if (consecutiveIdleCount >= 8 && sawRunningAfterPending && cooldownOk) {
|
||||
// 8 polls × 5s = 40 seconds — fallback (reduced from 100s)
|
||||
const fallbackThreshold = (currentCount > 770) ? 4 : 8; // 775-limit: faster fallback
|
||||
if (consecutiveIdleCount >= fallbackThreshold && sawRunningAfterPending && cooldownOk) {
|
||||
// Dynamic fallback: 20s (>770 steps) or 40s (normal)
|
||||
lastPendingStepIndex = currentCount;
|
||||
lastPendingTime = now;
|
||||
sawRunningAfterPending = false;
|
||||
@@ -1126,7 +1113,7 @@ function setupMonitor() {
|
||||
logToFile(`[STALL-FALLBACK] step=${currentCount} frozenCount=${consecutiveIdleCount} → pending`);
|
||||
writePendingApproval({ conversation_id: activeSessionId, command, description, source: 'stall_fallback' });
|
||||
}
|
||||
else if (consecutiveIdleCount === 8) {
|
||||
else if (consecutiveIdleCount === fallbackThreshold) {
|
||||
const reasons = [];
|
||||
if (!sawRunningAfterPending)
|
||||
reasons.push('needDelta>0');
|
||||
@@ -1145,10 +1132,14 @@ function setupMonitor() {
|
||||
if (notifyStep && notifyStep.stepIndex > lastNotifyStepIndex) {
|
||||
lastNotifyStepIndex = notifyStep.stepIndex;
|
||||
const content = notifyStep.step?.notifyUser?.notificationContent || '';
|
||||
if (content.length > 10) {
|
||||
// Filter: only relay meaningful notifications (skip trivial ones)
|
||||
if (content.length > 50) {
|
||||
writeChatSnapshot(`📣 **알림** (step ${notifyStep.stepIndex})\n\n${content}`);
|
||||
console.log(`Gravity Bridge: [POLL#${pollCount}] NOTIFY step=${notifyStep.stepIndex} ${content.length} chars`);
|
||||
}
|
||||
else if (content.length > 0) {
|
||||
logToFile(`[POLL] NOTIFY skipped (too short: ${content.length} chars): ${content.substring(0, 80)}`);
|
||||
}
|
||||
}
|
||||
// ── Process latestTaskBoundaryStep ──
|
||||
const taskStep = bestSession.latestTaskBoundaryStep;
|
||||
@@ -1157,8 +1148,16 @@ function setupMonitor() {
|
||||
const tb = taskStep.step?.taskBoundary;
|
||||
if (tb?.taskName) {
|
||||
const mode = tb.mode ? tb.mode.replace('AGENT_MODE_', '') : '';
|
||||
writeChatSnapshot(`📋 **[${mode}] ${tb.taskName}**\n${tb.taskStatus || ''}\n\n${tb.taskSummary || ''}`);
|
||||
console.log(`Gravity Bridge: [POLL#${pollCount}] TASK step=${taskStep.stepIndex} "${tb.taskName}"`);
|
||||
// Filter: skip status-only updates with same task name (noise)
|
||||
const taskText = `${tb.taskName}|${tb.taskStatus || ''}`;
|
||||
if (taskText !== lastRelayedTaskText) {
|
||||
lastRelayedTaskText = taskText;
|
||||
writeChatSnapshot(`📋 **[${mode}] ${tb.taskName}**\n${tb.taskStatus || ''}\n\n${tb.taskSummary || ''}`);
|
||||
console.log(`Gravity Bridge: [POLL#${pollCount}] TASK step=${taskStep.stepIndex} "${tb.taskName}"`);
|
||||
}
|
||||
else {
|
||||
logToFile(`[POLL] TASK skipped (duplicate): "${tb.taskName}"`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1225,52 +1224,23 @@ async function processResponseFile(filePath) {
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
// ═══ APPROVAL STRATEGY ═══
|
||||
// DOM observer approvals: renderer handles clicking via pollResponse — skip VS Code commands
|
||||
// Stall-detection approvals: use VS Code commands as fallback (focus-dependent)
|
||||
// ═══ MULTI-STRATEGY APPROVAL (v2.1) ═══
|
||||
// Tries multiple methods sequentially with detailed logging.
|
||||
// DOM observer: renderer handles clicking via pollResponse
|
||||
// Step probe/stall: try RPC → VS Code commands → log results
|
||||
const approved = resp.approved;
|
||||
if (isDomObserver) {
|
||||
// DOM observer path: renderer polls /response/:rid and clicks directly
|
||||
logToFile(`[RESPONSE] renderer-handled approval (rid=${resp.request_id})`);
|
||||
}
|
||||
else {
|
||||
// Step probe / stall path: relay approval to DOM observer pending files
|
||||
// The renderer polls /response/:rid and can click the actual button
|
||||
logToFile(`[RESPONSE] step_probe/stall → relaying to DOM observer pending files`);
|
||||
const pendingDir = path.join(bridgePath, 'pending');
|
||||
const responseDir = path.join(bridgePath, 'response');
|
||||
if (!fs.existsSync(responseDir))
|
||||
fs.mkdirSync(responseDir, { recursive: true });
|
||||
let relayCount = 0;
|
||||
try {
|
||||
const files = fs.readdirSync(pendingDir).filter((f) => f.endsWith('.json'));
|
||||
for (const f of files) {
|
||||
try {
|
||||
const pd = JSON.parse(fs.readFileSync(path.join(pendingDir, f), 'utf-8'));
|
||||
if (pd.source === 'dom_observer' && pd.status === 'pending') {
|
||||
// Write response file for this DOM observer pending
|
||||
const responsePayload = {
|
||||
request_id: pd.request_id,
|
||||
approved,
|
||||
timestamp: Date.now() / 1000,
|
||||
};
|
||||
fs.writeFileSync(path.join(responseDir, `${pd.request_id}.json`), JSON.stringify(responsePayload, null, 2), 'utf-8');
|
||||
relayCount++;
|
||||
logToFile(`[RESPONSE] relayed to DOM pending: ${pd.request_id} approved=${approved}`);
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
}
|
||||
catch (e) {
|
||||
logToFile(`[RESPONSE] relay scan error: ${e.message}`);
|
||||
}
|
||||
logToFile(`[RESPONSE] relayed to ${relayCount} DOM observer pending files`);
|
||||
// Step probe / stall path: try all approval strategies
|
||||
logToFile(`[RESPONSE] step_probe/stall → trying multi-strategy approval`);
|
||||
const approvalResult = await tryApprovalStrategies(approved, sessionId);
|
||||
logToFile(`[RESPONSE] approval result: ${approvalResult}`);
|
||||
}
|
||||
logToFile(`[RESPONSE] ${approved ? 'approve' : 'reject'} done (${isDomObserver ? 'dom' : 'stall'})`);
|
||||
logToFile(`[RESPONSE] ${approved ? 'approve' : 'reject'} done (${isDomObserver ? 'dom' : 'step_probe'})`);
|
||||
// Cleanup response file — BUT NOT for DOM observer!
|
||||
// DOM observer: renderer's pollResponse needs to find it via HTTP GET /response/:rid
|
||||
// Non-DOM (stall/step_probe relay): watcher already handled it, safe to delete
|
||||
if (!isDomObserver) {
|
||||
try {
|
||||
fs.unlinkSync(filePath);
|
||||
@@ -1437,6 +1407,129 @@ function writePendingApproval(data) {
|
||||
console.log(`Gravity Bridge: pending write error: ${e.message}`);
|
||||
}
|
||||
}
|
||||
// ─── Multi-Strategy Approval Execution ───
|
||||
/**
|
||||
* Try multiple approval methods sequentially.
|
||||
* Returns a string describing which method succeeded (or all failed).
|
||||
*
|
||||
* Strategy order (most reliable first):
|
||||
* 1. HandleCascadeUserInteraction RPC (cross-platform, no focus)
|
||||
* 2. VS Code accept/reject commands (focus-dependent)
|
||||
* 3. Log failure for manual intervention
|
||||
*/
|
||||
async function tryApprovalStrategies(approved, sessionId) {
|
||||
const action = approved ? 'APPROVE' : 'REJECT';
|
||||
logToFile(`[APPROVAL] Starting ${action} strategies for session ${sessionId.substring(0, 8)}`);
|
||||
// ── Strategy 1: HandleCascadeUserInteraction RPC ──
|
||||
if (sdk) {
|
||||
// Try variant A: { cascadeId, approved }
|
||||
try {
|
||||
logToFile(`[APPROVAL-1A] HandleCascadeUserInteraction { cascadeId, approved: ${approved} }`);
|
||||
const rpcResult = await sdk.ls.rawRPC('HandleCascadeUserInteraction', {
|
||||
cascadeId: sessionId,
|
||||
approved: approved,
|
||||
});
|
||||
logToFile(`[APPROVAL-1A] ✅ SUCCESS: ${JSON.stringify(rpcResult).substring(0, 200)}`);
|
||||
return `RPC-1A:HandleCascadeUserInteraction(approved=${approved})`;
|
||||
}
|
||||
catch (e) {
|
||||
logToFile(`[APPROVAL-1A] ❌ FAIL: ${e.message}`);
|
||||
}
|
||||
// Try variant B: { cascadeId, stepAction }
|
||||
try {
|
||||
const stepAction = approved ? 'STEP_ACTION_ACCEPT' : 'STEP_ACTION_REJECT';
|
||||
logToFile(`[APPROVAL-1B] HandleCascadeUserInteraction { cascadeId, stepAction: '${stepAction}' }`);
|
||||
const rpcResult = await sdk.ls.rawRPC('HandleCascadeUserInteraction', {
|
||||
cascadeId: sessionId,
|
||||
stepAction: stepAction,
|
||||
});
|
||||
logToFile(`[APPROVAL-1B] ✅ SUCCESS: ${JSON.stringify(rpcResult).substring(0, 200)}`);
|
||||
return `RPC-1B:HandleCascadeUserInteraction(stepAction=${stepAction})`;
|
||||
}
|
||||
catch (e) {
|
||||
logToFile(`[APPROVAL-1B] ❌ FAIL: ${e.message}`);
|
||||
}
|
||||
// Try variant C: { cascadeId, userAction } (experimental)
|
||||
try {
|
||||
const userAction = approved ? 'USER_ACTION_APPROVED' : 'USER_ACTION_REJECTED';
|
||||
logToFile(`[APPROVAL-1C] HandleCascadeUserInteraction { cascadeId, userAction: '${userAction}' }`);
|
||||
const rpcResult = await sdk.ls.rawRPC('HandleCascadeUserInteraction', {
|
||||
cascadeId: sessionId,
|
||||
userAction: userAction,
|
||||
});
|
||||
logToFile(`[APPROVAL-1C] ✅ SUCCESS: ${JSON.stringify(rpcResult).substring(0, 200)}`);
|
||||
return `RPC-1C:HandleCascadeUserInteraction(userAction=${userAction})`;
|
||||
}
|
||||
catch (e) {
|
||||
logToFile(`[APPROVAL-1C] ❌ FAIL: ${e.message}`);
|
||||
}
|
||||
}
|
||||
// ── Strategy 2: VS Code Commands — step-type-specific (focus-dependent) ──
|
||||
// Per SDK research (2026-03-09):
|
||||
// Run button = terminal command → terminalCommand.run / terminalCommand.accept
|
||||
// Code changes = agent step → agent.acceptAgentStep
|
||||
// General commands = command.accept
|
||||
// Previously only tried acceptAgentStep (Silent Success) — now tries ALL 7
|
||||
// Try to focus the panel first (required for command.accept / acceptAgentStep)
|
||||
try {
|
||||
logToFile(`[APPROVAL-2] focusing panel...`);
|
||||
await vscode.commands.executeCommand('antigravity.agentPanel.focus');
|
||||
await new Promise(resolve => setTimeout(resolve, 300));
|
||||
await vscode.commands.executeCommand('antigravity.agentSidePanel.focus');
|
||||
await new Promise(resolve => setTimeout(resolve, 300));
|
||||
logToFile(`[APPROVAL-2] panel focus attempted (agentPanel + agentSidePanel)`);
|
||||
}
|
||||
catch (e) {
|
||||
logToFile(`[APPROVAL-2] panel focus failed: ${e.message}`);
|
||||
}
|
||||
// All 7 approval commands in priority order (terminal first for Run button)
|
||||
const commands = approved
|
||||
? [
|
||||
// Terminal commands (Run button) — UNTESTED INDIVIDUALLY (devlog-004)
|
||||
'antigravity.terminalCommand.run', // SDK: TERMINAL_RUN
|
||||
'antigravity.terminalCommand.accept', // SDK: TERMINAL_ACCEPT
|
||||
// General command approval
|
||||
'antigravity.command.accept', // SDK: COMMAND_ACCEPT
|
||||
// Agent step approval (known: Silent Success with focus)
|
||||
'antigravity.agent.acceptAgentStep', // SDK: ACCEPT_AGENT_STEP
|
||||
// Cascade action (experimental)
|
||||
// 'antigravity.executeCascadeAction', // SDK: needs action param — skip
|
||||
]
|
||||
: [
|
||||
'antigravity.terminalCommand.reject', // SDK: TERMINAL_REJECT
|
||||
'antigravity.command.reject', // SDK: COMMAND_REJECT
|
||||
'antigravity.agent.rejectAgentStep', // SDK: REJECT_AGENT_STEP
|
||||
];
|
||||
for (let i = 0; i < commands.length; i++) {
|
||||
const cmd = commands[i];
|
||||
try {
|
||||
const t0 = Date.now();
|
||||
logToFile(`[APPROVAL-2${String.fromCharCode(65 + i)}] executing: ${cmd}`);
|
||||
const result = await vscode.commands.executeCommand(cmd);
|
||||
const dt = Date.now() - t0;
|
||||
logToFile(`[APPROVAL-2${String.fromCharCode(65 + i)}] returned: ${JSON.stringify(result)} (${dt}ms)`);
|
||||
}
|
||||
catch (e) {
|
||||
logToFile(`[APPROVAL-2${String.fromCharCode(65 + i)}] ❌ FAIL: ${e.message}`);
|
||||
}
|
||||
}
|
||||
// ── Strategy 3: ResolveOutstandingSteps (REJECT ONLY — this CANCELS!) ──
|
||||
if (!approved && sdk) {
|
||||
try {
|
||||
logToFile(`[APPROVAL-3] ResolveOutstandingSteps (REJECT/CANCEL only!)`);
|
||||
const rpcResult = await sdk.ls.rawRPC('ResolveOutstandingSteps', {
|
||||
cascadeId: sessionId,
|
||||
});
|
||||
logToFile(`[APPROVAL-3] ✅ SUCCESS: ${JSON.stringify(rpcResult).substring(0, 200)}`);
|
||||
return `RPC-3:ResolveOutstandingSteps(cancel)`;
|
||||
}
|
||||
catch (e) {
|
||||
logToFile(`[APPROVAL-3] ❌ FAIL: ${e.message}`);
|
||||
}
|
||||
}
|
||||
logToFile(`[APPROVAL] ⚠️ ALL strategies attempted — check logs for results`);
|
||||
return `ALL_ATTEMPTED:${action}`;
|
||||
}
|
||||
// ─── Activation ───
|
||||
async function activate(context) {
|
||||
console.log('Gravity Bridge: activating...');
|
||||
|
||||
Reference in New Issue
Block a user