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:
@@ -164,3 +164,8 @@
|
||||
- **해결**: 항상 `extension.ts`의 `generateApprovalObserverScript()` 함수를 수정 → 컴파일 → 배포 → Reload
|
||||
- **주의**: 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 테스트 필요
|
||||
|
||||
5
docs/devlog/2026-03-09.md
Normal file
5
docs/devlog/2026-03-09.md
Normal file
@@ -0,0 +1,5 @@
|
||||
# 2026-03-09 Devlog
|
||||
|
||||
| # | 시간 | 작업 설명 | 커밋 | 상태 |
|
||||
|---|------|----------|------|------|
|
||||
| 001 | 08:00~09:17 | 승인 실행 메커니즘 연구 + step-type별 VS Code 명령 분기 구현 | `pending` | 🔧 |
|
||||
34
docs/devlog/entries/20260309-001.md
Normal file
34
docs/devlog/entries/20260309-001.md
Normal 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 활성화
|
||||
@@ -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...');
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -660,23 +660,6 @@ function generateApprovalObserverScript(_port: number): string {
|
||||
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');
|
||||
@@ -906,6 +889,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++;
|
||||
@@ -988,9 +972,8 @@ function setupMonitor() {
|
||||
}
|
||||
|
||||
// ── 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 ──
|
||||
|
||||
@@ -1025,7 +1008,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,
|
||||
@@ -1034,6 +1017,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;
|
||||
@@ -1096,8 +1082,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;
|
||||
@@ -1107,7 +1094,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');
|
||||
if (!cooldownOk) reasons.push(`cooldown(${Math.round((60000 - (now - lastPendingTime)) / 1000)}s)`);
|
||||
@@ -1123,9 +1110,12 @@ 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)}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1136,8 +1126,15 @@ 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}"`);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e: any) {
|
||||
@@ -1207,9 +1204,10 @@ async function processResponseFile(filePath: string) {
|
||||
} 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;
|
||||
|
||||
@@ -1217,47 +1215,15 @@ async function processResponseFile(filePath: string) {
|
||||
// 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: 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`);
|
||||
// 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); } 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 ───
|
||||
|
||||
export async function activate(context: vscode.ExtensionContext) {
|
||||
|
||||
Reference in New Issue
Block a user