fix(bridge): response file race condition + Run button regex + known issues
- Fix: processResponseFile no longer deletes response files for DOM observer approvals, allowing renderer pollResponse to find and serve them via HTTP - Fix: Run button regex ^Run$ → ^Run to match 'Run Alt+⏎' button text - Fix: BTN-DUMP diagnostic added to generateApprovalObserverScript (source) - Doc: 2 new known issues (race condition, renderer script 3-location confusion) - Doc: devlog entry #19
This commit is contained in:
@@ -130,11 +130,18 @@
|
||||
- **해결**: `scan()` 검색 범위를 `document.body` 전체로 확장, `Accept all` / `Reject all` 패턴 추가
|
||||
- **주의**: 패널+body 이중 검색 시 dedupe 필요 (같은 버튼이 두 번 잡힐 수 있음)
|
||||
|
||||
### [2026-03-08] latestToolCallStep — 즉시 승인 감지 (stall 30초 대체)
|
||||
- **증상**: 이전 stall-based 감지는 RUNNING+delta=0+modTime frozen 30초 후에야 pending 생성
|
||||
- **원인**: `GetAllCascadeTrajectories` 응답의 `latestToolCallStep` 필드를 전혀 활용하지 않았음
|
||||
- **해결**: `latestToolCallStep.step`의 status 필드에서 `WAITING` 여부를 직접 체크 → 5초 poll 1회에 즉시 감지. Stall detection은 100초 fallback으로 유지
|
||||
- **주의**: `latestToolCallStep`의 정확한 protobuf 구조(status 필드 위치)는 런타임 덤프로 확정 필요 — 첫 실행 시 `[TOOL-STEP]` 로그 확인
|
||||
### [2026-03-08] GetCascadeTrajectorySteps — cascadeId 파라미터 발견
|
||||
- **증상**: `GetCascadeTrajectorySteps`에 `trajectoryId` 파라미터 → 500 "trajectory not found"
|
||||
- **원인**: 파라미터명이 `trajectoryId`가 아니라 **`cascadeId`**. 값은 `GetAllCascadeTrajectories.trajectorySummaries`의 맵 키(세션 ID)
|
||||
- **해결**: `{ cascadeId: sessionId }`로 호출 → 전체 step 배열 반환 성공
|
||||
- **주의**: `latestToolCallStep` 필드는 `GetAllCascadeTrajectories` 응답에 **존재하지 않음** (KI 오류)
|
||||
|
||||
### [2026-03-08] Step 구조 — CORTEX_STEP_STATUS_WAITING 즉시 감지
|
||||
- **증상**: stall-based 감지(100초)가 너무 느림
|
||||
- **원인**: 이제 `GetCascadeTrajectorySteps`로 최신 step의 status를 직접 확인 가능
|
||||
- **해결**: stall 5초 후 step probe → `CORTEX_STEP_STATUS_WAITING` 확인 → 즉시 pending 생성
|
||||
- **Step 구조**: `{type: "CORTEX_STEP_TYPE_RUN_COMMAND", status: "CORTEX_STEP_STATUS_WAITING", metadata: {toolCall: {name, argumentsJson}}, runCommand, requestedInteraction}`
|
||||
- **주의**: 775-step 하드 리밋은 여전히 존재. 긴 세션에서는 fallback(40초) 사용
|
||||
|
||||
### [2026-03-08] Extension 재설치 안전성 — 자동 패치 메커니즘
|
||||
- **증상**: Antigravity 삭제 후 재설치 시 렌더러 스크립트가 동작 안 함
|
||||
@@ -144,3 +151,16 @@
|
||||
2. `product.json` SHA256 체크섬 자동 업데이트
|
||||
3. HTTP bridge 서버 시작 + 결정론적 포트
|
||||
- **주의**: 패치 후 반드시 **Antigravity 풀 재시작** 필요 (Reload Window 불가). Extension VSIX만 설치하면 수동 패치 불필요
|
||||
|
||||
### [2026-03-08] Response 파일 Race Condition — DOM Observer 승인 실패
|
||||
- **증상**: Discord에서 승인 → `[RESPONSE] renderer-handled approval` 로그 출력 → 실제 버튼 클릭 안 됨
|
||||
- **원인**: `processResponseFile` (파일 감시자)이 response 파일을 즉시 삭제 → renderer의 `pollResponse`가 HTTP `GET /response/:rid`로 조회 시 파일 이미 없음
|
||||
- **해결**: DOM observer 소스일 때는 response 파일을 삭제하지 않도록 수정. HTTP endpoint가 renderer에게 서빙한 후 삭제
|
||||
- **주의**: non-DOM (stall/step_probe relay)는 watcher에서 삭제해도 됨
|
||||
|
||||
### [2026-03-08] Renderer 스크립트 소스 혼동 — 3곳의 코드
|
||||
- **증상**: `extension.ts`에 BTN-DUMP 추가 → Reload 2번 → 콘솔에 안 나옴
|
||||
- **원인**: renderer 코드가 **3곳**에 존재: (1) `extension.ts`의 `generateApprovalObserverScript()` (소스), (2) `ag-sdk-variet-gravity-bridge.js` (배포됨, Reload시 소스에서 재생성), (3) `workbench-jetski-agent.html` inline (HTML, JS파일과 중복로드 방지됨). 직접 JS파일 패치는 Reload시 소스에서 재생성되어 **덮어씌워짐**
|
||||
- **해결**: 항상 `extension.ts`의 `generateApprovalObserverScript()` 함수를 수정 → 컴파일 → 배포 → Reload
|
||||
- **주의**: HTML inline은 JS파일이 먼저 로드되어 `window.__agSDK` 가드에 의해 실행 안 됨. 실제 실행되는 것은 JS파일 경로의 스크립트
|
||||
|
||||
|
||||
@@ -20,3 +20,4 @@
|
||||
| 16 | 19:00~19:48 | 렌더러 스크립트 로딩 디버깅: sync XHR→async fetch 변환, 설치경로 불일치 발견, vscode-file:// 커스텀 파일 서빙 불가 확인, Electron 풀 재시작 필요 발견 | - | 🔧 |
|
||||
| 17 | 19:53~20:00 | **AG 재시작 성공**: GB Observer Bridge connected (port 34332), Allow Once/Allow This Conversation 감지 정상 동작 확인 | - | ✅ |
|
||||
| 18 | 20:00~20:15 | **승인 감지 최적화**: latestToolCallStep 즉시 감지 (30초→5초), DOM scan 범위 확장 (Accept all/Reject all), stall→100초 fallback | - | 🔧 |
|
||||
| 19 | 21:30~22:55 | **E2E 디버깅**: response 파일 race condition 수정, Run 버튼 regex 패턴 수정(`^Run$`→`^Run`), renderer 스크립트 소스 혼동 발견(3곳), Run 버튼은 webview iframe 내부로 DOM observer 접근 불가 확인 | - | 🔧 |
|
||||
|
||||
@@ -447,6 +447,7 @@ function startObserverHttpBridge() {
|
||||
const pending = {
|
||||
...data,
|
||||
request_id: rid,
|
||||
conversation_id: activeSessionId || '',
|
||||
timestamp: Date.now() / 1000,
|
||||
status: 'pending',
|
||||
project_name: projectName,
|
||||
@@ -604,7 +605,7 @@ function generateApprovalObserverScript(_port) {
|
||||
|
||||
// ── Button patterns to detect (order matters: first match wins per scan) ──
|
||||
var PATS=[
|
||||
{re:/^Run$/i, type:'terminal_command'},
|
||||
{re:/^Run/i, type:'terminal_command'},
|
||||
{re:/^Accept all$/i, type:'diff_review'},
|
||||
{re:/^Accept$/i, type:'agent_step'},
|
||||
{re:/^Allow/i, type:'permission'},
|
||||
@@ -688,6 +689,23 @@ 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');
|
||||
@@ -907,18 +925,17 @@ function setupMonitor() {
|
||||
// stepIndex on each → perfect for dedup
|
||||
// ══════════════════════════════════════════════════════════════════════
|
||||
let pollCount = 0;
|
||||
// activeSessionId is module-level (for writeChatSnapshot lazy registration)
|
||||
// activeSessionId is module-level (used by writeChatSnapshot for lazy registration)
|
||||
let activeSessionTitle = '';
|
||||
let lastKnownStepCount = 0;
|
||||
let lastNotifyStepIndex = -1;
|
||||
let lastTaskStepIndex = -1;
|
||||
let lastToolStepIndex = -1; // track latestToolCallStep for instant detection
|
||||
let toolStepDumped = false; // dump structure once for debugging
|
||||
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
|
||||
let stallProbed = false; // prevent repeated step probes during same stall
|
||||
setInterval(async () => {
|
||||
pollCount++;
|
||||
if (pollCount <= 3 || pollCount % 12 === 0) {
|
||||
@@ -971,8 +988,16 @@ function setupMonitor() {
|
||||
lastNotifyStepIndex = bestSession.latestNotifyUserStep?.stepIndex ?? -1;
|
||||
lastTaskStepIndex = bestSession.latestTaskBoundaryStep?.stepIndex ?? -1;
|
||||
lastPendingStepIndex = -1;
|
||||
stallProbed = false;
|
||||
// Don't register here — registration happens lazily in writeChatSnapshot/writePendingApproval
|
||||
// to avoid race conditions between multiple extension instances
|
||||
// Dump session keys + trajectoryMetadata on session change
|
||||
const allKeys = Object.keys(bestSession);
|
||||
logToFile(`[SESSION-INIT] id=${activeSessionId.substring(0, 8)} keys=[${allKeys.join(',')}]`);
|
||||
const trajMeta = bestSession.trajectoryMetadata;
|
||||
if (trajMeta) {
|
||||
logToFile(`[SESSION-INIT] trajectoryMetadata=${JSON.stringify(trajMeta).substring(0, 500)}`);
|
||||
}
|
||||
console.log(`Gravity Bridge: [POLL#${pollCount}] session: ${activeSessionId.substring(0, 8)} "${currentTitle}" steps=${currentCount} ${isRunning ? 'RUNNING' : 'idle'}`);
|
||||
return;
|
||||
}
|
||||
@@ -986,57 +1011,11 @@ function setupMonitor() {
|
||||
if (pollCount <= 10 || pollCount % 6 === 0 || delta > 0) {
|
||||
logToFile(`[POLL#${pollCount}] status=${statusStr} steps=${currentCount} delta=${delta}`);
|
||||
}
|
||||
// ── PRIMARY: Instant tool-call-based approval detection ──
|
||||
// latestToolCallStep contains the most recent tool call with its status.
|
||||
// If status includes WAITING, the AI is waiting for user approval.
|
||||
const toolStep = bestSession.latestToolCallStep;
|
||||
if (toolStep && toolStep.stepIndex > lastToolStepIndex) {
|
||||
const stepData = toolStep.step || {};
|
||||
// Dump structure once for debugging (keys + first 800 chars)
|
||||
if (!toolStepDumped) {
|
||||
logToFile(`[TOOL-STEP] keys: ${JSON.stringify(Object.keys(toolStep))}`);
|
||||
logToFile(`[TOOL-STEP] step keys: ${JSON.stringify(Object.keys(stepData))}`);
|
||||
logToFile(`[TOOL-STEP] full: ${JSON.stringify(toolStep).substring(0, 800)}`);
|
||||
toolStepDumped = true;
|
||||
}
|
||||
// Check for WAITING status in various possible field locations
|
||||
const stepStatus = String(stepData.status || toolStep.status || stepData.stepStatus || '').toLowerCase();
|
||||
const isWaiting = stepStatus.includes('waiting') || stepStatus.includes('pending');
|
||||
if (isWaiting && toolStep.stepIndex !== lastPendingStepIndex) {
|
||||
// INSTANT detection! Extract tool call details
|
||||
const command = extractToolCommand(stepData);
|
||||
const description = extractToolDescription(stepData, currentTitle, toolStep.stepIndex);
|
||||
logToFile(`[TOOL-DETECT] INSTANT! step=${toolStep.stepIndex} status='${stepStatus}' cmd='${command}'`);
|
||||
lastPendingStepIndex = toolStep.stepIndex;
|
||||
lastPendingTime = Date.now();
|
||||
sawRunningAfterPending = false;
|
||||
writePendingApproval({
|
||||
conversation_id: activeSessionId,
|
||||
command,
|
||||
description,
|
||||
step_type: 'tool_call',
|
||||
step_index: toolStep.stepIndex,
|
||||
});
|
||||
}
|
||||
else if (!isWaiting) {
|
||||
// Tool call exists but not waiting — log for diagnostics
|
||||
if (toolStep.stepIndex > lastToolStepIndex) {
|
||||
logToFile(`[TOOL-STEP] step=${toolStep.stepIndex} status='${stepStatus}' (not waiting)`);
|
||||
}
|
||||
}
|
||||
lastToolStepIndex = toolStep.stepIndex;
|
||||
}
|
||||
// ── FALLBACK: Stall-based approval detection ──
|
||||
// Kept as safety net for edge cases where latestToolCallStep doesn't report WAITING.
|
||||
// Threshold raised from 6 (30s) to 20 (100s) since primary detection above handles most cases.
|
||||
// DEBUG: dump session keys on first poll
|
||||
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}`);
|
||||
if (toolStep)
|
||||
logToFile(`[DEBUG] toolStep present, stepIndex=${toolStep.stepIndex}`);
|
||||
}
|
||||
// ── 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.
|
||||
// ── STALL-BASED approval detection with step probe ──
|
||||
const currentModTime = bestSession.lastModifiedTime || bestSession.lastModifiedTimestamp || bestSession.modifiedTime || '';
|
||||
const modTimeChanged = currentModTime !== lastModTime;
|
||||
const isStall = isRunning && delta === 0;
|
||||
@@ -1047,12 +1026,14 @@ function setupMonitor() {
|
||||
if (delta > 0) {
|
||||
consecutiveIdleCount = 0;
|
||||
sawRunningAfterPending = true;
|
||||
stallProbed = false; // allow re-probe on next stall
|
||||
lastModTime = currentModTime;
|
||||
}
|
||||
else if (isStall) {
|
||||
if (modTimeChanged) {
|
||||
// lastModifiedTime is still changing = AI is thinking, NOT approval
|
||||
consecutiveIdleCount = 0; // Reset!
|
||||
stallProbed = false;
|
||||
if (pollCount <= 10 || pollCount % 12 === 0) {
|
||||
logToFile(`[THINK] step=${currentCount} modTime changing → not stall`);
|
||||
}
|
||||
@@ -1062,19 +1043,90 @@ function setupMonitor() {
|
||||
consecutiveIdleCount++;
|
||||
}
|
||||
lastModTime = currentModTime;
|
||||
// ── 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) {
|
||||
try {
|
||||
const stepsResp = await sdk.ls.rawRPC('GetCascadeTrajectorySteps', {
|
||||
cascadeId: bestSessionId,
|
||||
});
|
||||
if (stepsResp?.steps?.length > 0) {
|
||||
const steps = stepsResp.steps;
|
||||
// Diagnostic: compare returned steps vs trajectory stepCount
|
||||
logToFile(`[STEP-PROBE] returned=${steps.length} vs trajectory.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--) {
|
||||
const step = steps[si];
|
||||
const stepStatus = step?.status || '';
|
||||
const stepType = step?.type || '';
|
||||
if (stepStatus === 'CORTEX_STEP_STATUS_WAITING') {
|
||||
foundWaiting = true;
|
||||
// Extract command from metadata.toolCall or direct fields
|
||||
const toolCall = step?.metadata?.toolCall;
|
||||
const toolName = toolCall?.name || stepType.replace('CORTEX_STEP_TYPE_', '').toLowerCase();
|
||||
let command = toolName;
|
||||
// Parse argumentsJson for command details
|
||||
if (toolCall?.argumentsJson) {
|
||||
try {
|
||||
const args = JSON.parse(toolCall.argumentsJson);
|
||||
if (args.CommandLine) {
|
||||
command = `${toolName}: ${args.CommandLine.substring(0, 150)}`;
|
||||
}
|
||||
else if (args.TargetFile) {
|
||||
command = `${toolName}: ${args.TargetFile.split(/[\\/]/).pop()}`;
|
||||
}
|
||||
else {
|
||||
command = `${toolName}: ${Object.keys(args).join(', ')}`;
|
||||
}
|
||||
}
|
||||
catch {
|
||||
command = toolName;
|
||||
}
|
||||
}
|
||||
const description = `Step #${si} (${stepType.replace('CORTEX_STEP_TYPE_', '')})`;
|
||||
logToFile(`[STEP-PROBE] ★ WAITING! step=${si} type=${stepType} cmd='${command}'`);
|
||||
if (si !== lastPendingStepIndex) {
|
||||
stallProbed = true; // found WAITING — stop retrying
|
||||
lastPendingStepIndex = si;
|
||||
lastPendingTime = Date.now();
|
||||
sawRunningAfterPending = false;
|
||||
writePendingApproval({
|
||||
conversation_id: activeSessionId,
|
||||
command,
|
||||
description,
|
||||
step_type: toolName,
|
||||
step_index: si,
|
||||
source: 'step_probe',
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!foundWaiting) {
|
||||
const lastStep = steps[steps.length - 1];
|
||||
logToFile(`[STEP-PROBE] lastStep status=${lastStep?.status} type=${lastStep?.type} (not waiting)`);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (e) {
|
||||
logToFile(`[STEP-PROBE] error: ${e.message}`);
|
||||
}
|
||||
}
|
||||
const now = Date.now();
|
||||
const cooldownOk = (now - lastPendingTime) > 60_000;
|
||||
if (consecutiveIdleCount >= 20 && sawRunningAfterPending && cooldownOk) {
|
||||
// 20 polls × 5s = 100 seconds — fallback only
|
||||
if (consecutiveIdleCount >= 8 && sawRunningAfterPending && cooldownOk) {
|
||||
// 8 polls × 5s = 40 seconds — fallback (reduced from 100s)
|
||||
lastPendingStepIndex = currentCount;
|
||||
lastPendingTime = now;
|
||||
sawRunningAfterPending = false;
|
||||
const command = `Stall at step ${currentCount} (fallback)`;
|
||||
const description = `승인 대기 감지 — fallback (${consecutiveIdleCount * 5}초 정지), Title: "${currentTitle}"`;
|
||||
logToFile(`[STALL-FALLBACK] step=${currentCount} frozenCount=${consecutiveIdleCount} → pending`);
|
||||
writePendingApproval({ conversation_id: activeSessionId, command, description });
|
||||
writePendingApproval({ conversation_id: activeSessionId, command, description, source: 'stall_fallback' });
|
||||
}
|
||||
else if (consecutiveIdleCount === 20) {
|
||||
else if (consecutiveIdleCount === 8) {
|
||||
const reasons = [];
|
||||
if (!sawRunningAfterPending)
|
||||
reasons.push('needDelta>0');
|
||||
@@ -1168,7 +1220,8 @@ async function processResponseFile(filePath) {
|
||||
try {
|
||||
const pending = JSON.parse(fs.readFileSync(pendingFile, 'utf-8'));
|
||||
sessionId = pending.conversation_id || '';
|
||||
isDomObserver = pending.auto_detected === true && pending.source === 'dom_observer';
|
||||
isDomObserver = pending.auto_detected === true
|
||||
|| pending.source === 'dom_observer';
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
@@ -1177,64 +1230,53 @@ async function processResponseFile(filePath) {
|
||||
// Stall-detection approvals: use VS Code commands as fallback (focus-dependent)
|
||||
const approved = resp.approved;
|
||||
if (isDomObserver) {
|
||||
// DOM observer path: renderer polls /response/:rid and clicks the button directly
|
||||
logToFile(`[RESPONSE] DOM observer approval — renderer will handle click (rid=${resp.request_id})`);
|
||||
// DOM observer path: renderer polls /response/:rid and clicks directly
|
||||
logToFile(`[RESPONSE] renderer-handled approval (rid=${resp.request_id})`);
|
||||
}
|
||||
else {
|
||||
// Stall-detection path: use VS Code commands (legacy, focus-dependent)
|
||||
logToFile(`[RESPONSE] Stall-detection approval — using VS Code commands`);
|
||||
// Focus panel
|
||||
for (let i = 0; i < 2; i++) {
|
||||
try {
|
||||
await vscode.commands.executeCommand('antigravity.agentPanel.focus');
|
||||
if (i === 0)
|
||||
logToFile('[RESPONSE] panel focus attempt 1');
|
||||
}
|
||||
catch (e) {
|
||||
logToFile(`[RESPONSE] panel focus attempt ${i + 1} failed: ${e.message}`);
|
||||
}
|
||||
await new Promise(r => setTimeout(r, 500));
|
||||
}
|
||||
if (approved) {
|
||||
const approveCommands = [
|
||||
'antigravity.terminalCommand.run',
|
||||
'antigravity.terminalCommand.accept',
|
||||
'antigravity.command.accept',
|
||||
'antigravity.agent.acceptAgentStep',
|
||||
];
|
||||
for (const cmd of approveCommands) {
|
||||
// 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 {
|
||||
await vscode.commands.executeCommand(cmd);
|
||||
logToFile(`[RESPONSE] cmd OK: ${cmd}`);
|
||||
}
|
||||
catch (e) {
|
||||
logToFile(`[RESPONSE] cmd FAIL: ${cmd} → ${e.message}`);
|
||||
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 { }
|
||||
}
|
||||
}
|
||||
else {
|
||||
const rejectCommands = [
|
||||
'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) {
|
||||
logToFile(`[RESPONSE] cmd FAIL: ${cmd} → ${e.message}`);
|
||||
}
|
||||
}
|
||||
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'})`);
|
||||
// Cleanup response file (but NOT pending — renderer still polls it)
|
||||
try {
|
||||
fs.unlinkSync(filePath);
|
||||
// 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 { }
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
catch (e) {
|
||||
const log = `[RESPONSE] error: ${e.message}`;
|
||||
@@ -1382,6 +1424,7 @@ function writePendingApproval(data) {
|
||||
project_name: projectName,
|
||||
...(data.step_type ? { step_type: data.step_type } : {}),
|
||||
...(data.step_index !== undefined ? { step_index: data.step_index } : {}),
|
||||
...(data.source ? { source: data.source } : {}),
|
||||
};
|
||||
fs.writeFileSync(path.join(pendingDir, `${id}.json`), JSON.stringify(payload, null, 2), 'utf-8');
|
||||
console.log(`Gravity Bridge: pending approval written → ${id}.json`);
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -418,6 +418,7 @@ function startObserverHttpBridge(): Promise<number> {
|
||||
const pending = {
|
||||
...data,
|
||||
request_id: rid,
|
||||
conversation_id: activeSessionId || '',
|
||||
timestamp: Date.now() / 1000,
|
||||
status: 'pending',
|
||||
project_name: projectName,
|
||||
@@ -575,7 +576,7 @@ function generateApprovalObserverScript(_port: number): string {
|
||||
|
||||
// ── Button patterns to detect (order matters: first match wins per scan) ──
|
||||
var PATS=[
|
||||
{re:/^Run$/i, type:'terminal_command'},
|
||||
{re:/^Run/i, type:'terminal_command'},
|
||||
{re:/^Accept all$/i, type:'diff_review'},
|
||||
{re:/^Accept$/i, type:'agent_step'},
|
||||
{re:/^Allow/i, type:'permission'},
|
||||
@@ -659,6 +660,23 @@ 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');
|
||||
@@ -877,18 +895,17 @@ function setupMonitor() {
|
||||
// stepIndex on each → perfect for dedup
|
||||
// ══════════════════════════════════════════════════════════════════════
|
||||
let pollCount = 0;
|
||||
// activeSessionId is module-level (for writeChatSnapshot lazy registration)
|
||||
// activeSessionId is module-level (used by writeChatSnapshot for lazy registration)
|
||||
let activeSessionTitle = '';
|
||||
let lastKnownStepCount = 0;
|
||||
let lastNotifyStepIndex = -1;
|
||||
let lastTaskStepIndex = -1;
|
||||
let lastToolStepIndex = -1; // track latestToolCallStep for instant detection
|
||||
let toolStepDumped = false; // dump structure once for debugging
|
||||
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
|
||||
let stallProbed = false; // prevent repeated step probes during same stall
|
||||
|
||||
setInterval(async () => {
|
||||
pollCount++;
|
||||
@@ -943,8 +960,16 @@ function setupMonitor() {
|
||||
lastNotifyStepIndex = bestSession.latestNotifyUserStep?.stepIndex ?? -1;
|
||||
lastTaskStepIndex = bestSession.latestTaskBoundaryStep?.stepIndex ?? -1;
|
||||
lastPendingStepIndex = -1;
|
||||
stallProbed = false;
|
||||
// Don't register here — registration happens lazily in writeChatSnapshot/writePendingApproval
|
||||
// to avoid race conditions between multiple extension instances
|
||||
// Dump session keys + trajectoryMetadata on session change
|
||||
const allKeys = Object.keys(bestSession);
|
||||
logToFile(`[SESSION-INIT] id=${activeSessionId.substring(0, 8)} keys=[${allKeys.join(',')}]`);
|
||||
const trajMeta = bestSession.trajectoryMetadata;
|
||||
if (trajMeta) {
|
||||
logToFile(`[SESSION-INIT] trajectoryMetadata=${JSON.stringify(trajMeta).substring(0, 500)}`);
|
||||
}
|
||||
console.log(`Gravity Bridge: [POLL#${pollCount}] session: ${activeSessionId.substring(0, 8)} "${currentTitle}" steps=${currentCount} ${isRunning ? 'RUNNING' : 'idle'}`);
|
||||
return;
|
||||
}
|
||||
@@ -962,63 +987,12 @@ function setupMonitor() {
|
||||
logToFile(`[POLL#${pollCount}] status=${statusStr} steps=${currentCount} delta=${delta}`);
|
||||
}
|
||||
|
||||
// ── PRIMARY: Instant tool-call-based approval detection ──
|
||||
// latestToolCallStep contains the most recent tool call with its status.
|
||||
// If status includes WAITING, the AI is waiting for user approval.
|
||||
const toolStep = bestSession.latestToolCallStep;
|
||||
if (toolStep && toolStep.stepIndex > lastToolStepIndex) {
|
||||
const stepData = toolStep.step || {};
|
||||
// Dump structure once for debugging (keys + first 800 chars)
|
||||
if (!toolStepDumped) {
|
||||
logToFile(`[TOOL-STEP] keys: ${JSON.stringify(Object.keys(toolStep))}`);
|
||||
logToFile(`[TOOL-STEP] step keys: ${JSON.stringify(Object.keys(stepData))}`);
|
||||
logToFile(`[TOOL-STEP] full: ${JSON.stringify(toolStep).substring(0, 800)}`);
|
||||
toolStepDumped = true;
|
||||
}
|
||||
// ── 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.
|
||||
|
||||
// Check for WAITING status in various possible field locations
|
||||
const stepStatus = String(
|
||||
stepData.status || toolStep.status || stepData.stepStatus || ''
|
||||
).toLowerCase();
|
||||
const isWaiting = stepStatus.includes('waiting') || stepStatus.includes('pending');
|
||||
|
||||
if (isWaiting && toolStep.stepIndex !== lastPendingStepIndex) {
|
||||
// INSTANT detection! Extract tool call details
|
||||
const command = extractToolCommand(stepData);
|
||||
const description = extractToolDescription(stepData, currentTitle, toolStep.stepIndex);
|
||||
logToFile(`[TOOL-DETECT] INSTANT! step=${toolStep.stepIndex} status='${stepStatus}' cmd='${command}'`);
|
||||
|
||||
lastPendingStepIndex = toolStep.stepIndex;
|
||||
lastPendingTime = Date.now();
|
||||
sawRunningAfterPending = false;
|
||||
|
||||
writePendingApproval({
|
||||
conversation_id: activeSessionId,
|
||||
command,
|
||||
description,
|
||||
step_type: 'tool_call',
|
||||
step_index: toolStep.stepIndex,
|
||||
});
|
||||
} else if (!isWaiting) {
|
||||
// Tool call exists but not waiting — log for diagnostics
|
||||
if (toolStep.stepIndex > lastToolStepIndex) {
|
||||
logToFile(`[TOOL-STEP] step=${toolStep.stepIndex} status='${stepStatus}' (not waiting)`);
|
||||
}
|
||||
}
|
||||
lastToolStepIndex = toolStep.stepIndex;
|
||||
}
|
||||
|
||||
// ── FALLBACK: Stall-based approval detection ──
|
||||
// Kept as safety net for edge cases where latestToolCallStep doesn't report WAITING.
|
||||
// Threshold raised from 6 (30s) to 20 (100s) since primary detection above handles most cases.
|
||||
|
||||
// DEBUG: dump session keys on first poll
|
||||
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}`);
|
||||
if (toolStep) logToFile(`[DEBUG] toolStep present, stepIndex=${toolStep.stepIndex}`);
|
||||
}
|
||||
// ── STALL-BASED approval detection with step probe ──
|
||||
|
||||
const currentModTime = bestSession.lastModifiedTime || (bestSession as any).lastModifiedTimestamp || (bestSession as any).modifiedTime || '';
|
||||
const modTimeChanged = currentModTime !== lastModTime;
|
||||
@@ -1032,11 +1006,13 @@ function setupMonitor() {
|
||||
if (delta > 0) {
|
||||
consecutiveIdleCount = 0;
|
||||
sawRunningAfterPending = true;
|
||||
stallProbed = false; // allow re-probe on next stall
|
||||
lastModTime = currentModTime;
|
||||
} else if (isStall) {
|
||||
if (modTimeChanged) {
|
||||
// lastModifiedTime is still changing = AI is thinking, NOT approval
|
||||
consecutiveIdleCount = 0; // Reset!
|
||||
stallProbed = false;
|
||||
if (pollCount <= 10 || pollCount % 12 === 0) {
|
||||
logToFile(`[THINK] step=${currentCount} modTime changing → not stall`);
|
||||
}
|
||||
@@ -1046,11 +1022,82 @@ function setupMonitor() {
|
||||
}
|
||||
lastModTime = currentModTime;
|
||||
|
||||
// ── 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) {
|
||||
try {
|
||||
const stepsResp = await sdk.ls.rawRPC('GetCascadeTrajectorySteps', {
|
||||
cascadeId: bestSessionId,
|
||||
});
|
||||
if (stepsResp?.steps?.length > 0) {
|
||||
const steps = stepsResp.steps;
|
||||
// Diagnostic: compare returned steps vs trajectory stepCount
|
||||
logToFile(`[STEP-PROBE] returned=${steps.length} vs trajectory.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--) {
|
||||
const step = steps[si];
|
||||
const stepStatus = step?.status || '';
|
||||
const stepType = step?.type || '';
|
||||
|
||||
if (stepStatus === 'CORTEX_STEP_STATUS_WAITING') {
|
||||
foundWaiting = true;
|
||||
// Extract command from metadata.toolCall or direct fields
|
||||
const toolCall = step?.metadata?.toolCall;
|
||||
const toolName = toolCall?.name || stepType.replace('CORTEX_STEP_TYPE_', '').toLowerCase();
|
||||
let command = toolName;
|
||||
|
||||
// Parse argumentsJson for command details
|
||||
if (toolCall?.argumentsJson) {
|
||||
try {
|
||||
const args = JSON.parse(toolCall.argumentsJson);
|
||||
if (args.CommandLine) {
|
||||
command = `${toolName}: ${args.CommandLine.substring(0, 150)}`;
|
||||
} else if (args.TargetFile) {
|
||||
command = `${toolName}: ${args.TargetFile.split(/[\\/]/).pop()}`;
|
||||
} else {
|
||||
command = `${toolName}: ${Object.keys(args).join(', ')}`;
|
||||
}
|
||||
} catch { command = toolName; }
|
||||
}
|
||||
|
||||
const description = `Step #${si} (${stepType.replace('CORTEX_STEP_TYPE_', '')})`;
|
||||
logToFile(`[STEP-PROBE] ★ WAITING! step=${si} type=${stepType} cmd='${command}'`);
|
||||
|
||||
if (si !== lastPendingStepIndex) {
|
||||
stallProbed = true; // found WAITING — stop retrying
|
||||
lastPendingStepIndex = si;
|
||||
lastPendingTime = Date.now();
|
||||
sawRunningAfterPending = false;
|
||||
writePendingApproval({
|
||||
conversation_id: activeSessionId,
|
||||
command,
|
||||
description,
|
||||
step_type: toolName,
|
||||
step_index: si,
|
||||
source: 'step_probe',
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!foundWaiting) {
|
||||
const lastStep = steps[steps.length - 1];
|
||||
logToFile(`[STEP-PROBE] lastStep status=${lastStep?.status} type=${lastStep?.type} (not waiting)`);
|
||||
}
|
||||
}
|
||||
} catch (e: any) {
|
||||
logToFile(`[STEP-PROBE] error: ${e.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
const now = Date.now();
|
||||
const cooldownOk = (now - lastPendingTime) > 60_000;
|
||||
|
||||
if (consecutiveIdleCount >= 20 && sawRunningAfterPending && cooldownOk) {
|
||||
// 20 polls × 5s = 100 seconds — fallback only
|
||||
if (consecutiveIdleCount >= 8 && sawRunningAfterPending && cooldownOk) {
|
||||
// 8 polls × 5s = 40 seconds — fallback (reduced from 100s)
|
||||
lastPendingStepIndex = currentCount;
|
||||
lastPendingTime = now;
|
||||
sawRunningAfterPending = false;
|
||||
@@ -1059,8 +1106,8 @@ function setupMonitor() {
|
||||
const description = `승인 대기 감지 — fallback (${consecutiveIdleCount * 5}초 정지), Title: "${currentTitle}"`;
|
||||
|
||||
logToFile(`[STALL-FALLBACK] step=${currentCount} frozenCount=${consecutiveIdleCount} → pending`);
|
||||
writePendingApproval({ conversation_id: activeSessionId, command, description });
|
||||
} else if (consecutiveIdleCount === 20) {
|
||||
writePendingApproval({ conversation_id: activeSessionId, command, description, source: 'stall_fallback' });
|
||||
} else if (consecutiveIdleCount === 8) {
|
||||
const reasons = [];
|
||||
if (!sawRunningAfterPending) reasons.push('needDelta>0');
|
||||
if (!cooldownOk) reasons.push(`cooldown(${Math.round((60000 - (now - lastPendingTime)) / 1000)}s)`);
|
||||
@@ -1155,7 +1202,8 @@ async function processResponseFile(filePath: string) {
|
||||
try {
|
||||
const pending = JSON.parse(fs.readFileSync(pendingFile, 'utf-8'));
|
||||
sessionId = pending.conversation_id || '';
|
||||
isDomObserver = pending.auto_detected === true && pending.source === 'dom_observer';
|
||||
isDomObserver = pending.auto_detected === true
|
||||
|| pending.source === 'dom_observer';
|
||||
} catch { }
|
||||
}
|
||||
|
||||
@@ -1166,59 +1214,53 @@ async function processResponseFile(filePath: string) {
|
||||
const approved = resp.approved;
|
||||
|
||||
if (isDomObserver) {
|
||||
// DOM observer path: renderer polls /response/:rid and clicks the button directly
|
||||
logToFile(`[RESPONSE] DOM observer approval — renderer will handle click (rid=${resp.request_id})`);
|
||||
// DOM observer path: renderer polls /response/:rid and clicks directly
|
||||
logToFile(`[RESPONSE] renderer-handled approval (rid=${resp.request_id})`);
|
||||
} else {
|
||||
// Stall-detection path: use VS Code commands (legacy, focus-dependent)
|
||||
logToFile(`[RESPONSE] Stall-detection approval — using VS Code commands`);
|
||||
// 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`);
|
||||
|
||||
// Focus panel
|
||||
for (let i = 0; i < 2; i++) {
|
||||
try {
|
||||
await vscode.commands.executeCommand('antigravity.agentPanel.focus');
|
||||
if (i === 0) logToFile('[RESPONSE] panel focus attempt 1');
|
||||
} catch (e: any) {
|
||||
logToFile(`[RESPONSE] panel focus attempt ${i + 1} failed: ${e.message}`);
|
||||
}
|
||||
await new Promise(r => setTimeout(r, 500));
|
||||
}
|
||||
const pendingDir = path.join(bridgePath, 'pending');
|
||||
const responseDir = path.join(bridgePath, 'response');
|
||||
if (!fs.existsSync(responseDir)) fs.mkdirSync(responseDir, { recursive: true });
|
||||
|
||||
if (approved) {
|
||||
const approveCommands = [
|
||||
'antigravity.terminalCommand.run',
|
||||
'antigravity.terminalCommand.accept',
|
||||
'antigravity.command.accept',
|
||||
'antigravity.agent.acceptAgentStep',
|
||||
];
|
||||
for (const cmd of approveCommands) {
|
||||
let relayCount = 0;
|
||||
try {
|
||||
const files = fs.readdirSync(pendingDir).filter((f: string) => f.endsWith('.json'));
|
||||
for (const f of files) {
|
||||
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.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}`);
|
||||
}
|
||||
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'})`);
|
||||
|
||||
// Cleanup response file (but NOT pending — renderer still polls it)
|
||||
try { fs.unlinkSync(filePath); } catch { }
|
||||
// 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 { }
|
||||
}
|
||||
} catch (e: any) {
|
||||
const log = `[RESPONSE] error: ${e.message}`;
|
||||
console.log(`Gravity Bridge: ${log}`);
|
||||
@@ -1340,7 +1382,7 @@ function extractToolDescription(stepData: any, sessionTitle: string, stepIndex:
|
||||
}
|
||||
|
||||
/** Write a pending approval file matching Bot's ApprovalRequest dataclass. */
|
||||
function writePendingApproval(data: { conversation_id: string; command: string; description: string; step_type?: string; step_index?: number }) {
|
||||
function writePendingApproval(data: { conversation_id: string; command: string; description: string; step_type?: string; step_index?: number; source?: string }) {
|
||||
try {
|
||||
const pendingDir = path.join(bridgePath, 'pending');
|
||||
if (!fs.existsSync(pendingDir)) { fs.mkdirSync(pendingDir, { recursive: true }); }
|
||||
@@ -1356,6 +1398,7 @@ function writePendingApproval(data: { conversation_id: string; command: string;
|
||||
project_name: projectName,
|
||||
...(data.step_type ? { step_type: data.step_type } : {}),
|
||||
...(data.step_index !== undefined ? { step_index: data.step_index } : {}),
|
||||
...(data.source ? { source: data.source } : {}),
|
||||
};
|
||||
fs.writeFileSync(path.join(pendingDir, `${id}.json`), JSON.stringify(payload, null, 2), 'utf-8');
|
||||
console.log(`Gravity Bridge: pending approval written → ${id}.json`);
|
||||
|
||||
Reference in New Issue
Block a user