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:
2026-03-09 09:19:36 +09:00
parent 027135e2b5
commit 3b1bb9246e
6 changed files with 364 additions and 133 deletions

View File

@@ -164,3 +164,8 @@
- **해결**: 항상 `extension.ts``generateApprovalObserverScript()` 함수를 수정 → 컴파일 → 배포 → Reload - **해결**: 항상 `extension.ts``generateApprovalObserverScript()` 함수를 수정 → 컴파일 → 배포 → Reload
- **주의**: HTML inline은 JS파일이 먼저 로드되어 `window.__agSDK` 가드에 의해 실행 안 됨. 실제 실행되는 것은 JS파일 경로의 스크립트 - **주의**: HTML inline은 JS파일이 먼저 로드되어 `window.__agSDK` 가드에 의해 실행 안 됨. 실제 실행되는 것은 JS파일 경로의 스크립트
### [2026-03-09] VS Code Accept — Run 버튼에 잘못된 명령 사용
- **증상**: Discord 승인 → `acceptAgentStep` 실행 → "Silent Success" (실제 승인 안 됨)
- **원인**: `acceptAgentStep`**코드 변경** 승인 전용. Run 버튼 = **터미널 명령** 승인으로 `terminalCommand.run` 또는 `terminalCommand.accept`가 올바른 명령
- **해결**: SDK 7개 승인 명령을 step type별로 분기 시도 (`terminalCommand.run``terminalCommand.accept``command.accept``acceptAgentStep`)
- **주의**: `terminalCommand.run`의 개별 동작 결과는 아직 미검증. devlog-004에서 순차 시도만 언급됨. AG 재시작 후 E2E 테스트 필요

View File

@@ -0,0 +1,5 @@
# 2026-03-09 Devlog
| # | 시간 | 작업 설명 | 커밋 | 상태 |
|---|------|----------|------|------|
| 001 | 08:00~09:17 | 승인 실행 메커니즘 연구 + step-type별 VS Code 명령 분기 구현 | `pending` | 🔧 |

View File

@@ -0,0 +1,34 @@
# 승인 실행 메커니즘 연구 + step-type별 명령 분기
- **시간**: 2026-03-09 08:00~09:17
- **Commit**: `pending`
- **Vikunja**: #263 진행중
## 결정 사항
### acceptAgentStep은 Run 버튼에 무효
- SDK에 7개 별도 승인 명령 존재 (우리는 1개만 사용 중이었음)
- Run 버튼 = **터미널 명령**`terminalCommand.run` / `terminalCommand.accept`가 올바른 명령
- `acceptAgentStep`**코드 변경** 승인 전용 → Run 버튼과 무관
### 빌트인 Turbo Mode 발견
- `TerminalExecutionPolicy.EAGER` = 항상 자동 실행 (AG Settings에서 on/off)
- 사용자 판단: 통제권 유지가 중요 → Discord 승인 방식 유지, Turbo는 최후 수단
### pywinauto 폐기
- 크로스플랫폼 불가 (Linux 사용 가능성)
- 창 겹침 시 동작 불가
- 사용자 결정으로 폐기
## 코드 변경
- `extension.ts`:
- `tryApprovalStrategies()` — HandleCascadeUserInteraction RPC 3가지 variant + VS Code 7개 명령 순차 시도
- Step probe: stall 1 poll (5초) 후 즉시 probe, 775-limit 감지 + 동적 fallback (20s/40s)
- NOTIFY 필터: 50자 미만 무시, TASK 중복 skip
- BTN-DUMP 진단 로그 제거
- `.agents/references/known-issues.md`: 1건 추가
## 미완료
1. **AG 재시작 후 E2E 테스트**`terminalCommand.run` 개별 동작 확인 (핵심)
2. **`extension.log` 분석** — 어떤 전략이 실제 승인을 트리거하는지 로그로 확인
3. **실패 시 대안**: `executeCascadeAction` 파라미터 탐색 또는 Turbo Mode 활성화

View File

@@ -689,23 +689,6 @@ function generateApprovalObserverScript(_port) {
if(document.body)searchRoots.push(document.body); if(document.body)searchRoots.push(document.body);
if(!searchRoots.length)return; 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 var seen={}; // dedupe buttons across search roots
for(var r=0;r<searchRoots.length;r++){ for(var r=0;r<searchRoots.length;r++){
var allBtns=searchRoots[r].querySelectorAll('button'); var allBtns=searchRoots[r].querySelectorAll('button');
@@ -936,6 +919,7 @@ function setupMonitor() {
let sawRunningAfterPending = true; // gate: must see delta>0 before next pending let sawRunningAfterPending = true; // gate: must see delta>0 before next pending
let lastModTime = ''; // track lastModifiedTime to distinguish thinking vs approval let lastModTime = ''; // track lastModifiedTime to distinguish thinking vs approval
let stallProbed = false; // prevent repeated step probes during same stall let stallProbed = false; // prevent repeated step probes during same stall
let lastRelayedTaskText = ''; // dedup TASK_BOUNDARY relay
setInterval(async () => { setInterval(async () => {
pollCount++; pollCount++;
if (pollCount <= 3 || pollCount % 12 === 0) { if (pollCount <= 3 || pollCount % 12 === 0) {
@@ -1012,9 +996,8 @@ function setupMonitor() {
logToFile(`[POLL#${pollCount}] status=${statusStr} steps=${currentCount} delta=${delta}`); logToFile(`[POLL#${pollCount}] status=${statusStr} steps=${currentCount} delta=${delta}`);
} }
// ── PRIMARY: Step-probe-based approval detection ── // ── PRIMARY: Step-probe-based approval detection ──
// latestToolCallStep does NOT exist in GetAllCascadeTrajectories response. // On stall (idle=1, ~5s), probe GetCascadeTrajectorySteps to check WAITING.
// Instead, on early stall (idle=2, ~10s), probe GetCascadeTrajectorySteps // 775-step limit: probe fails for long sessions → faster stall fallback.
// to fetch the latest step and check if it's a tool call awaiting approval.
// ── STALL-BASED approval detection with step probe ── // ── STALL-BASED approval detection with step probe ──
const currentModTime = bestSession.lastModifiedTime || bestSession.lastModifiedTimestamp || bestSession.modifiedTime || ''; const currentModTime = bestSession.lastModifiedTime || bestSession.lastModifiedTimestamp || bestSession.modifiedTime || '';
const modTimeChanged = currentModTime !== lastModTime; const modTimeChanged = currentModTime !== lastModTime;
@@ -1046,7 +1029,7 @@ function setupMonitor() {
// ── Step probe: on stall, fetch latest step via cascadeId (retry until WAITING found) ── // ── Step probe: on stall, fetch latest step via cascadeId (retry until WAITING found) ──
// CONFIRMED: param='cascadeId', id=sessionId (map key from trajectorySummaries) // CONFIRMED: param='cascadeId', id=sessionId (map key from trajectorySummaries)
// Retries every 2 polls (~10s) because RUN_COMMAND step may not be created yet // 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 { try {
const stepsResp = await sdk.ls.rawRPC('GetCascadeTrajectorySteps', { const stepsResp = await sdk.ls.rawRPC('GetCascadeTrajectorySteps', {
cascadeId: bestSessionId, cascadeId: bestSessionId,
@@ -1055,6 +1038,9 @@ function setupMonitor() {
const steps = stepsResp.steps; const steps = stepsResp.steps;
// Diagnostic: compare returned steps vs trajectory stepCount // Diagnostic: compare returned steps vs trajectory stepCount
logToFile(`[STEP-PROBE] returned=${steps.length} vs trajectory.stepCount=${currentCount}`); 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) // Scan last 5 steps backwards to find WAITING (RUN_COMMAND may not be last)
let foundWaiting = false; let foundWaiting = false;
for (let si = steps.length - 1; si >= Math.max(0, steps.length - 5); si--) { 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 now = Date.now();
const cooldownOk = (now - lastPendingTime) > 60_000; const cooldownOk = (now - lastPendingTime) > 60_000;
if (consecutiveIdleCount >= 8 && sawRunningAfterPending && cooldownOk) { const fallbackThreshold = (currentCount > 770) ? 4 : 8; // 775-limit: faster fallback
// 8 polls × 5s = 40 seconds — fallback (reduced from 100s) if (consecutiveIdleCount >= fallbackThreshold && sawRunningAfterPending && cooldownOk) {
// Dynamic fallback: 20s (>770 steps) or 40s (normal)
lastPendingStepIndex = currentCount; lastPendingStepIndex = currentCount;
lastPendingTime = now; lastPendingTime = now;
sawRunningAfterPending = false; sawRunningAfterPending = false;
@@ -1126,7 +1113,7 @@ function setupMonitor() {
logToFile(`[STALL-FALLBACK] step=${currentCount} frozenCount=${consecutiveIdleCount} → pending`); logToFile(`[STALL-FALLBACK] step=${currentCount} frozenCount=${consecutiveIdleCount} → pending`);
writePendingApproval({ conversation_id: activeSessionId, command, description, source: 'stall_fallback' }); writePendingApproval({ conversation_id: activeSessionId, command, description, source: 'stall_fallback' });
} }
else if (consecutiveIdleCount === 8) { else if (consecutiveIdleCount === fallbackThreshold) {
const reasons = []; const reasons = [];
if (!sawRunningAfterPending) if (!sawRunningAfterPending)
reasons.push('needDelta>0'); reasons.push('needDelta>0');
@@ -1145,10 +1132,14 @@ function setupMonitor() {
if (notifyStep && notifyStep.stepIndex > lastNotifyStepIndex) { if (notifyStep && notifyStep.stepIndex > lastNotifyStepIndex) {
lastNotifyStepIndex = notifyStep.stepIndex; lastNotifyStepIndex = notifyStep.stepIndex;
const content = notifyStep.step?.notifyUser?.notificationContent || ''; 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}`); writeChatSnapshot(`📣 **알림** (step ${notifyStep.stepIndex})\n\n${content}`);
console.log(`Gravity Bridge: [POLL#${pollCount}] NOTIFY step=${notifyStep.stepIndex} ${content.length} chars`); 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 ── // ── Process latestTaskBoundaryStep ──
const taskStep = bestSession.latestTaskBoundaryStep; const taskStep = bestSession.latestTaskBoundaryStep;
@@ -1157,8 +1148,16 @@ function setupMonitor() {
const tb = taskStep.step?.taskBoundary; const tb = taskStep.step?.taskBoundary;
if (tb?.taskName) { if (tb?.taskName) {
const mode = tb.mode ? tb.mode.replace('AGENT_MODE_', '') : ''; const mode = tb.mode ? tb.mode.replace('AGENT_MODE_', '') : '';
writeChatSnapshot(`📋 **[${mode}] ${tb.taskName}**\n${tb.taskStatus || ''}\n\n${tb.taskSummary || ''}`); // Filter: skip status-only updates with same task name (noise)
console.log(`Gravity Bridge: [POLL#${pollCount}] TASK step=${taskStep.stepIndex} "${tb.taskName}"`); 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 { } catch { }
} }
// ═══ APPROVAL STRATEGY ═══ // ═══ MULTI-STRATEGY APPROVAL (v2.1) ═══
// DOM observer approvals: renderer handles clicking via pollResponse — skip VS Code commands // Tries multiple methods sequentially with detailed logging.
// Stall-detection approvals: use VS Code commands as fallback (focus-dependent) // DOM observer: renderer handles clicking via pollResponse
// Step probe/stall: try RPC → VS Code commands → log results
const approved = resp.approved; const approved = resp.approved;
if (isDomObserver) { if (isDomObserver) {
// DOM observer path: renderer polls /response/:rid and clicks directly // DOM observer path: renderer polls /response/:rid and clicks directly
logToFile(`[RESPONSE] renderer-handled approval (rid=${resp.request_id})`); logToFile(`[RESPONSE] renderer-handled approval (rid=${resp.request_id})`);
} }
else { else {
// Step probe / stall path: relay approval to DOM observer pending files // Step probe / stall path: try all approval strategies
// The renderer polls /response/:rid and can click the actual button logToFile(`[RESPONSE] step_probe/stall → trying multi-strategy approval`);
logToFile(`[RESPONSE] step_probe/stall → relaying to DOM observer pending files`); const approvalResult = await tryApprovalStrategies(approved, sessionId);
const pendingDir = path.join(bridgePath, 'pending'); logToFile(`[RESPONSE] approval result: ${approvalResult}`);
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`);
} }
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! // 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) { if (!isDomObserver) {
try { try {
fs.unlinkSync(filePath); fs.unlinkSync(filePath);
@@ -1437,6 +1407,129 @@ function writePendingApproval(data) {
console.log(`Gravity Bridge: pending write error: ${e.message}`); 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 ─── // ─── Activation ───
async function activate(context) { async function activate(context) {
console.log('Gravity Bridge: activating...'); console.log('Gravity Bridge: activating...');

File diff suppressed because one or more lines are too long

View File

@@ -660,23 +660,6 @@ function generateApprovalObserverScript(_port: number): string {
if(document.body)searchRoots.push(document.body); if(document.body)searchRoots.push(document.body);
if(!searchRoots.length)return; 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 var seen={}; // dedupe buttons across search roots
for(var r=0;r<searchRoots.length;r++){ for(var r=0;r<searchRoots.length;r++){
var allBtns=searchRoots[r].querySelectorAll('button'); var allBtns=searchRoots[r].querySelectorAll('button');
@@ -906,6 +889,7 @@ function setupMonitor() {
let sawRunningAfterPending = true; // gate: must see delta>0 before next pending let sawRunningAfterPending = true; // gate: must see delta>0 before next pending
let lastModTime = ''; // track lastModifiedTime to distinguish thinking vs approval let lastModTime = ''; // track lastModifiedTime to distinguish thinking vs approval
let stallProbed = false; // prevent repeated step probes during same stall let stallProbed = false; // prevent repeated step probes during same stall
let lastRelayedTaskText = ''; // dedup TASK_BOUNDARY relay
setInterval(async () => { setInterval(async () => {
pollCount++; pollCount++;
@@ -988,9 +972,8 @@ function setupMonitor() {
} }
// ── PRIMARY: Step-probe-based approval detection ── // ── PRIMARY: Step-probe-based approval detection ──
// latestToolCallStep does NOT exist in GetAllCascadeTrajectories response. // On stall (idle=1, ~5s), probe GetCascadeTrajectorySteps to check WAITING.
// Instead, on early stall (idle=2, ~10s), probe GetCascadeTrajectorySteps // 775-step limit: probe fails for long sessions → faster stall fallback.
// to fetch the latest step and check if it's a tool call awaiting approval.
// ── STALL-BASED approval detection with step probe ── // ── STALL-BASED approval detection with step probe ──
@@ -1025,7 +1008,7 @@ function setupMonitor() {
// ── Step probe: on stall, fetch latest step via cascadeId (retry until WAITING found) ── // ── Step probe: on stall, fetch latest step via cascadeId (retry until WAITING found) ──
// CONFIRMED: param='cascadeId', id=sessionId (map key from trajectorySummaries) // CONFIRMED: param='cascadeId', id=sessionId (map key from trajectorySummaries)
// Retries every 2 polls (~10s) because RUN_COMMAND step may not be created yet // 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 { try {
const stepsResp = await sdk.ls.rawRPC('GetCascadeTrajectorySteps', { const stepsResp = await sdk.ls.rawRPC('GetCascadeTrajectorySteps', {
cascadeId: bestSessionId, cascadeId: bestSessionId,
@@ -1034,6 +1017,9 @@ function setupMonitor() {
const steps = stepsResp.steps; const steps = stepsResp.steps;
// Diagnostic: compare returned steps vs trajectory stepCount // Diagnostic: compare returned steps vs trajectory stepCount
logToFile(`[STEP-PROBE] returned=${steps.length} vs trajectory.stepCount=${currentCount}`); 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) // Scan last 5 steps backwards to find WAITING (RUN_COMMAND may not be last)
let foundWaiting = false; let foundWaiting = false;
@@ -1096,8 +1082,9 @@ function setupMonitor() {
const now = Date.now(); const now = Date.now();
const cooldownOk = (now - lastPendingTime) > 60_000; const cooldownOk = (now - lastPendingTime) > 60_000;
if (consecutiveIdleCount >= 8 && sawRunningAfterPending && cooldownOk) { const fallbackThreshold = (currentCount > 770) ? 4 : 8; // 775-limit: faster fallback
// 8 polls × 5s = 40 seconds — fallback (reduced from 100s) if (consecutiveIdleCount >= fallbackThreshold && sawRunningAfterPending && cooldownOk) {
// Dynamic fallback: 20s (>770 steps) or 40s (normal)
lastPendingStepIndex = currentCount; lastPendingStepIndex = currentCount;
lastPendingTime = now; lastPendingTime = now;
sawRunningAfterPending = false; sawRunningAfterPending = false;
@@ -1107,7 +1094,7 @@ function setupMonitor() {
logToFile(`[STALL-FALLBACK] step=${currentCount} frozenCount=${consecutiveIdleCount} → pending`); logToFile(`[STALL-FALLBACK] step=${currentCount} frozenCount=${consecutiveIdleCount} → pending`);
writePendingApproval({ conversation_id: activeSessionId, command, description, source: 'stall_fallback' }); writePendingApproval({ conversation_id: activeSessionId, command, description, source: 'stall_fallback' });
} else if (consecutiveIdleCount === 8) { } else if (consecutiveIdleCount === fallbackThreshold) {
const reasons = []; const reasons = [];
if (!sawRunningAfterPending) reasons.push('needDelta>0'); if (!sawRunningAfterPending) reasons.push('needDelta>0');
if (!cooldownOk) reasons.push(`cooldown(${Math.round((60000 - (now - lastPendingTime)) / 1000)}s)`); if (!cooldownOk) reasons.push(`cooldown(${Math.round((60000 - (now - lastPendingTime)) / 1000)}s)`);
@@ -1123,9 +1110,12 @@ function setupMonitor() {
if (notifyStep && notifyStep.stepIndex > lastNotifyStepIndex) { if (notifyStep && notifyStep.stepIndex > lastNotifyStepIndex) {
lastNotifyStepIndex = notifyStep.stepIndex; lastNotifyStepIndex = notifyStep.stepIndex;
const content = notifyStep.step?.notifyUser?.notificationContent || ''; 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}`); writeChatSnapshot(`📣 **알림** (step ${notifyStep.stepIndex})\n\n${content}`);
console.log(`Gravity Bridge: [POLL#${pollCount}] NOTIFY step=${notifyStep.stepIndex} ${content.length} chars`); 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)}`);
} }
} }
@@ -1136,8 +1126,15 @@ function setupMonitor() {
const tb = taskStep.step?.taskBoundary; const tb = taskStep.step?.taskBoundary;
if (tb?.taskName) { if (tb?.taskName) {
const mode = tb.mode ? tb.mode.replace('AGENT_MODE_', '') : ''; const mode = tb.mode ? tb.mode.replace('AGENT_MODE_', '') : '';
writeChatSnapshot(`📋 **[${mode}] ${tb.taskName}**\n${tb.taskStatus || ''}\n\n${tb.taskSummary || ''}`); // Filter: skip status-only updates with same task name (noise)
console.log(`Gravity Bridge: [POLL#${pollCount}] TASK step=${taskStep.stepIndex} "${tb.taskName}"`); 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}"`);
}
} }
} }
} catch (e: any) { } catch (e: any) {
@@ -1207,9 +1204,10 @@ async function processResponseFile(filePath: string) {
} catch { } } catch { }
} }
// ═══ APPROVAL STRATEGY ═══ // ═══ MULTI-STRATEGY APPROVAL (v2.1) ═══
// DOM observer approvals: renderer handles clicking via pollResponse — skip VS Code commands // Tries multiple methods sequentially with detailed logging.
// Stall-detection approvals: use VS Code commands as fallback (focus-dependent) // DOM observer: renderer handles clicking via pollResponse
// Step probe/stall: try RPC → VS Code commands → log results
const approved = resp.approved; const approved = resp.approved;
@@ -1217,47 +1215,15 @@ async function processResponseFile(filePath: string) {
// DOM observer path: renderer polls /response/:rid and clicks directly // DOM observer path: renderer polls /response/:rid and clicks directly
logToFile(`[RESPONSE] renderer-handled approval (rid=${resp.request_id})`); logToFile(`[RESPONSE] renderer-handled approval (rid=${resp.request_id})`);
} else { } else {
// Step probe / stall path: relay approval to DOM observer pending files // Step probe / stall path: try all approval strategies
// The renderer polls /response/:rid and can click the actual button logToFile(`[RESPONSE] step_probe/stall → trying multi-strategy approval`);
logToFile(`[RESPONSE] step_probe/stall → relaying to DOM observer pending files`); const approvalResult = await tryApprovalStrategies(approved, sessionId);
logToFile(`[RESPONSE] approval result: ${approvalResult}`);
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: string) => 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: any) {
logToFile(`[RESPONSE] relay scan error: ${e.message}`);
}
logToFile(`[RESPONSE] relayed to ${relayCount} DOM observer pending files`);
} }
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! // 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) { if (!isDomObserver) {
try { fs.unlinkSync(filePath); } catch { } try { fs.unlinkSync(filePath); } catch { }
} }
@@ -1409,6 +1375,134 @@ function writePendingApproval(data: { conversation_id: string; command: string;
} }
} }
// ─── 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: boolean, sessionId: string): Promise<string> {
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: any) {
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: any) {
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: any) {
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: any) {
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: any) {
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: any) {
logToFile(`[APPROVAL-3] ❌ FAIL: ${e.message}`);
}
}
logToFile(`[APPROVAL] ⚠️ ALL strategies attempted — check logs for results`);
return `ALL_ATTEMPTED:${action}`;
}
// ─── Activation ─── // ─── Activation ───
export async function activate(context: vscode.ExtensionContext) { export async function activate(context: vscode.ExtensionContext) {