Compare commits
11 Commits
98646fed27
...
563fbadd5a
| Author | SHA1 | Date | |
|---|---|---|---|
| 563fbadd5a | |||
| 2958bdc950 | |||
| 9b047c0c7d | |||
| 7ed2db90df | |||
| 1089c6ce61 | |||
| e586bb6d41 | |||
| 8c6d25c6d4 | |||
| 628b5ae2fa | |||
| 2361aa7558 | |||
| 0e3a896c86 | |||
| 1f63f60280 |
49
bot.py
49
bot.py
@@ -165,6 +165,7 @@ class GravityBot(commands.Bot):
|
|||||||
self.session_status_messages: dict[str, int] = {} # conv_id → msg_id
|
self.session_status_messages: dict[str, int] = {} # conv_id → msg_id
|
||||||
self._sent_approval_ids: set[str] = set()
|
self._sent_approval_ids: set[str] = set()
|
||||||
self._deferred_ids: dict[str, int] = {} # request_id → defer count
|
self._deferred_ids: dict[str, int] = {} # request_id → defer count
|
||||||
|
self._sent_commands: dict[str, str] = {} # request_id → command text (for MERGE edit detection)
|
||||||
self._ready_event = asyncio.Event()
|
self._ready_event = asyncio.Event()
|
||||||
self._channel_lock = asyncio.Lock()
|
self._channel_lock = asyncio.Lock()
|
||||||
self.bridge = BridgeProtocol()
|
self.bridge = BridgeProtocol()
|
||||||
@@ -542,6 +543,7 @@ class GravityBot(commands.Bot):
|
|||||||
channel = await self._get_channel(project)
|
channel = await self._get_channel(project)
|
||||||
if channel:
|
if channel:
|
||||||
self._sent_approval_ids.add(req.request_id)
|
self._sent_approval_ids.add(req.request_id)
|
||||||
|
self._sent_commands[req.request_id] = req.command
|
||||||
await self._send_approval_request(channel, req)
|
await self._send_approval_request(channel, req)
|
||||||
|
|
||||||
# ── Check for auto_resolved pendings (approved directly in AG) ──
|
# ── Check for auto_resolved pendings (approved directly in AG) ──
|
||||||
@@ -567,6 +569,53 @@ class GravityBot(commands.Bot):
|
|||||||
pass
|
pass
|
||||||
f.unlink()
|
f.unlink()
|
||||||
self._deferred_ids.pop(data.get("request_id", ""), None)
|
self._deferred_ids.pop(data.get("request_id", ""), None)
|
||||||
|
self._sent_commands.pop(data.get("request_id", ""), None)
|
||||||
|
except (json.JSONDecodeError, OSError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
# ── Check for MERGE updates (step_probe updated command in already-sent pending) ──
|
||||||
|
for f in self.bridge.pending_dir.glob("*.json"):
|
||||||
|
try:
|
||||||
|
data = json.loads(f.read_text(encoding="utf-8-sig"))
|
||||||
|
rid = data.get("request_id", "")
|
||||||
|
if rid not in self._sent_approval_ids:
|
||||||
|
continue
|
||||||
|
if data.get("status") != "pending":
|
||||||
|
continue
|
||||||
|
msg_id = data.get("discord_message_id", 0)
|
||||||
|
if not msg_id:
|
||||||
|
continue
|
||||||
|
# Check if command was updated via MERGE
|
||||||
|
new_cmd = data.get("command", "")
|
||||||
|
old_cmd = self._sent_commands.get(rid, "")
|
||||||
|
if new_cmd and new_cmd != old_cmd and len(new_cmd) > len(old_cmd):
|
||||||
|
# MERGE detected — edit Discord message
|
||||||
|
self._sent_commands[rid] = new_cmd
|
||||||
|
project = data.get("project_name", Config.PROJECT_NAME)
|
||||||
|
channel = await self._get_channel(project)
|
||||||
|
if channel:
|
||||||
|
try:
|
||||||
|
msg = await channel.fetch_message(msg_id)
|
||||||
|
# Rebuild embed with full command
|
||||||
|
buttons = data.get("buttons")
|
||||||
|
desc_parts = [f"**명령어:**\n```\n{new_cmd[:1000]}\n```"]
|
||||||
|
if buttons and len(buttons) > 1:
|
||||||
|
btn_names = [b.get("text", "?") for b in buttons]
|
||||||
|
desc_parts.append(f"**선택지:** {' / '.join(btn_names)}")
|
||||||
|
desc = data.get("description", "")
|
||||||
|
if desc:
|
||||||
|
desc_parts.append(desc[:500])
|
||||||
|
embed = discord.Embed(
|
||||||
|
title="⚠️ 승인 요청",
|
||||||
|
description="\n".join(desc_parts),
|
||||||
|
color=discord.Color.orange(),
|
||||||
|
timestamp=datetime.now(timezone.utc),
|
||||||
|
)
|
||||||
|
embed.set_footer(text=f"ID: {rid}")
|
||||||
|
await msg.edit(embed=embed)
|
||||||
|
logger.info(f"MERGE edit: {rid[:12]} cmd='{new_cmd[:60]}'")
|
||||||
|
except discord.NotFound:
|
||||||
|
pass
|
||||||
except (json.JSONDecodeError, OSError):
|
except (json.JSONDecodeError, OSError):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|||||||
30
docs/devlog/entries/20260310-002.md
Normal file
30
docs/devlog/entries/20260310-002.md
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
---
|
||||||
|
id: "20260310-002"
|
||||||
|
date: "2026-03-10"
|
||||||
|
session_start: "07:55"
|
||||||
|
session_end: "10:40"
|
||||||
|
tags: [approval-flow, discord-relay, proto-rpc, step-offset]
|
||||||
|
---
|
||||||
|
|
||||||
|
# 승인 플로우 완성 + Discord 실시간 릴레이
|
||||||
|
|
||||||
|
## 요약
|
||||||
|
Proto RPC 기반 승인 플로우의 치명적 버그 2건 수정 + Discord 실시간 AI 응답 릴레이 구현.
|
||||||
|
|
||||||
|
## 주요 성과
|
||||||
|
|
||||||
|
### 1. 자동 취소 버그 수정 (`2361aa7`)
|
||||||
|
- `ResolveOutstandingSteps` RPC 비활성화 (reject 시 AG 작업 전체 취소 방지)
|
||||||
|
|
||||||
|
### 2. 775-step API limit 해결 (`628b5ae`)
|
||||||
|
- `GetCascadeTrajectorySteps` proto에서 `step_offset` 파라미터 발견/활용
|
||||||
|
- 장시간 세션(step 527+)에서도 최신 WAITING step 감지
|
||||||
|
|
||||||
|
### 3. Discord 실시간 AI 응답 릴레이 (`e586bb6` ~ `2958bdc`)
|
||||||
|
- `plannerResponse.modifiedResponse` + `verbosity: 1` (DEBUG)
|
||||||
|
- RUNNING 중 `delta > 0` 마다 RT 캡쳐 → 중간 메시지도 Discord에 표시
|
||||||
|
- notify_user/task_boundary 스냅샷 동작 확인
|
||||||
|
|
||||||
|
## 다음 단계
|
||||||
|
- Allow Once / Allow This Conversation 파일 접근 승인
|
||||||
|
- Retry / 코드 편집 승인 테스트
|
||||||
@@ -114,6 +114,7 @@ function ensureBridgeDir() {
|
|||||||
}
|
}
|
||||||
// Module-level activeSessionId so writeChatSnapshot can register sessions lazily
|
// Module-level activeSessionId so writeChatSnapshot can register sessions lazily
|
||||||
let activeSessionId = '';
|
let activeSessionId = '';
|
||||||
|
let activeTrajectoryId = '';
|
||||||
function writeChatSnapshot(text) {
|
function writeChatSnapshot(text) {
|
||||||
try {
|
try {
|
||||||
// Write to chat_snapshots/*.json for Bot's chat_snapshot_scanner to pick up
|
// Write to chat_snapshots/*.json for Bot's chat_snapshot_scanner to pick up
|
||||||
@@ -131,6 +132,8 @@ function writeChatSnapshot(text) {
|
|||||||
const filePath = path.join(snapshotDir, `${id}.json`);
|
const filePath = path.join(snapshotDir, `${id}.json`);
|
||||||
fs.writeFileSync(filePath, JSON.stringify(data, null, 2), 'utf-8');
|
fs.writeFileSync(filePath, JSON.stringify(data, null, 2), 'utf-8');
|
||||||
console.log(`Gravity Bridge: chat snapshot written (${text.length} chars) → ${id}.json`);
|
console.log(`Gravity Bridge: chat snapshot written (${text.length} chars) → ${id}.json`);
|
||||||
|
logToFile(`[SNAPSHOT] written ${id}.json (${text.length} chars)`);
|
||||||
|
logToFile(`[SNAPSHOT] content: ${text.substring(0, 200)}`);
|
||||||
// Lazily register session → project mapping (correct because projectName is per-window)
|
// Lazily register session → project mapping (correct because projectName is per-window)
|
||||||
if (activeSessionId) {
|
if (activeSessionId) {
|
||||||
writeRegistration(activeSessionId);
|
writeRegistration(activeSessionId);
|
||||||
@@ -426,6 +429,7 @@ const pendingResponses = new Map();
|
|||||||
// Click trigger: extension sets this, renderer polls and clicks button
|
// Click trigger: extension sets this, renderer polls and clicks button
|
||||||
let clickTrigger = null;
|
let clickTrigger = null;
|
||||||
let sessionStalled = false; // true when session is stalled waiting for approval
|
let sessionStalled = false; // true when session is stalled waiting for approval
|
||||||
|
let lastPendingStepIndex = -1; // dedup: don't re-create pending for same step (module-level for tryApprovalStrategies)
|
||||||
// Deep inspect trigger: curl sets this, renderer picks it up and POSTs results back
|
// Deep inspect trigger: curl sets this, renderer picks it up and POSTs results back
|
||||||
let deepInspectRequested = false;
|
let deepInspectRequested = false;
|
||||||
let deepInspectResult = null;
|
let deepInspectResult = null;
|
||||||
@@ -511,8 +515,15 @@ function startObserverHttpBridge() {
|
|||||||
if (fs.existsSync(respFile)) {
|
if (fs.existsSync(respFile)) {
|
||||||
try {
|
try {
|
||||||
const data = JSON.parse(fs.readFileSync(respFile, 'utf8'));
|
const data = JSON.parse(fs.readFileSync(respFile, 'utf8'));
|
||||||
fs.unlinkSync(respFile);
|
logToFile(`[HTTP] response served to renderer: ${rid} approved=${data.approved}`);
|
||||||
logToFile(`[HTTP] response sent: ${rid} approved=${data.approved}`);
|
// Delay deletion: processResponseFile (response watcher) may need to read it too.
|
||||||
|
// The watcher fires with 300ms delay, so 2s is safe.
|
||||||
|
setTimeout(() => {
|
||||||
|
try {
|
||||||
|
fs.unlinkSync(respFile);
|
||||||
|
}
|
||||||
|
catch { }
|
||||||
|
}, 2000);
|
||||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||||
res.end(JSON.stringify(data));
|
res.end(JSON.stringify(data));
|
||||||
}
|
}
|
||||||
@@ -1398,13 +1409,16 @@ function setupMonitor() {
|
|||||||
let lastKnownStepCount = 0;
|
let lastKnownStepCount = 0;
|
||||||
let lastNotifyStepIndex = -1;
|
let lastNotifyStepIndex = -1;
|
||||||
let lastTaskStepIndex = -1;
|
let lastTaskStepIndex = -1;
|
||||||
let lastPendingStepIndex = -1; // dedup: don't re-create pending for same step
|
// lastPendingStepIndex is module-level (above sessionStalled)
|
||||||
let consecutiveIdleCount = 0; // debounce: require N consecutive stall polls
|
let consecutiveIdleCount = 0; // debounce: require N consecutive stall polls
|
||||||
let lastPendingTime = 0; // cooldown: minimum gap between pendings
|
let lastPendingTime = 0; // cooldown: minimum gap between pendings
|
||||||
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
|
let lastRelayedTaskText = ''; // dedup TASK_BOUNDARY relay
|
||||||
|
let wasRunning = false; // track RUNNING→IDLE transition for response capture
|
||||||
|
let lastUserInputStepIdx = -1; // track user input for response matching
|
||||||
|
let lastResponseCaptureStep = -1; // dedup: don't capture same response twice
|
||||||
setInterval(async () => {
|
setInterval(async () => {
|
||||||
pollCount++;
|
pollCount++;
|
||||||
if (pollCount <= 3 || pollCount % 12 === 0) {
|
if (pollCount <= 3 || pollCount % 12 === 0) {
|
||||||
@@ -1452,6 +1466,7 @@ function setupMonitor() {
|
|||||||
// Session changed?
|
// Session changed?
|
||||||
if (bestSessionId !== activeSessionId) {
|
if (bestSessionId !== activeSessionId) {
|
||||||
activeSessionId = bestSessionId;
|
activeSessionId = bestSessionId;
|
||||||
|
activeTrajectoryId = bestSession.trajectoryId || '';
|
||||||
activeSessionTitle = currentTitle;
|
activeSessionTitle = currentTitle;
|
||||||
lastKnownStepCount = currentCount;
|
lastKnownStepCount = currentCount;
|
||||||
lastNotifyStepIndex = bestSession.latestNotifyUserStep?.stepIndex ?? -1;
|
lastNotifyStepIndex = bestSession.latestNotifyUserStep?.stepIndex ?? -1;
|
||||||
@@ -1474,6 +1489,46 @@ function setupMonitor() {
|
|||||||
lastKnownStepCount = currentCount;
|
lastKnownStepCount = currentCount;
|
||||||
if (delta > 0) {
|
if (delta > 0) {
|
||||||
console.log(`Gravity Bridge: [POLL#${pollCount}] +${delta} steps (${currentCount}) "${currentTitle}"`);
|
console.log(`Gravity Bridge: [POLL#${pollCount}] +${delta} steps (${currentCount}) "${currentTitle}"`);
|
||||||
|
// Real-time response capture: fetch latest steps on every delta>0
|
||||||
|
if (isRunning && currentCount > lastResponseCaptureStep && sdk) {
|
||||||
|
try {
|
||||||
|
const rtOffset = Math.max(0, currentCount - 3);
|
||||||
|
const rtResp = await sdk.ls.rawRPC('GetCascadeTrajectorySteps', {
|
||||||
|
cascadeId: bestSessionId,
|
||||||
|
stepOffset: rtOffset,
|
||||||
|
verbosity: 1, // DEBUG — includes plannerResponse text
|
||||||
|
});
|
||||||
|
if (rtResp?.steps?.length > 0) {
|
||||||
|
for (let ri = rtResp.steps.length - 1; ri >= 0; ri--) {
|
||||||
|
const s = rtResp.steps[ri];
|
||||||
|
const sType = s?.type || '';
|
||||||
|
const actualIdx = rtOffset + ri;
|
||||||
|
if (actualIdx <= lastResponseCaptureStep)
|
||||||
|
continue;
|
||||||
|
if (sType.includes('PLANNER_RESPONSE') && s?.status?.includes('DONE')) {
|
||||||
|
const pr = s?.plannerResponse;
|
||||||
|
if (pr) {
|
||||||
|
let text = pr.modifiedResponse || pr.rawText || pr.text || '';
|
||||||
|
if (text.length > 10) {
|
||||||
|
lastResponseCaptureStep = actualIdx;
|
||||||
|
logToFile(`[RT-CAPTURE] step=${actualIdx} (${text.length} chars)`);
|
||||||
|
const truncated = text.length > 1800
|
||||||
|
? text.substring(0, 1800) + '\n\n_(이하 생략)_'
|
||||||
|
: text;
|
||||||
|
writeChatSnapshot(`💬 **AI 응답**\n\n${truncated}`);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (rte) {
|
||||||
|
// Non-critical — don't spam logs
|
||||||
|
if (pollCount <= 5)
|
||||||
|
logToFile(`[RT-CAPTURE] error: ${rte.message.substring(0, 80)}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// Log session state on EVERY poll for diagnostics
|
// Log session state on EVERY poll for diagnostics
|
||||||
const statusStr = String(bestSession.status || 'UNKNOWN');
|
const statusStr = String(bestSession.status || 'UNKNOWN');
|
||||||
@@ -1549,7 +1604,63 @@ function setupMonitor() {
|
|||||||
// 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) {
|
if (steps.length < currentCount) {
|
||||||
logToFile(`[STEP-PROBE] ⚠️ 775-limit hit! steps=${steps.length} < stepCount=${currentCount}`);
|
logToFile(`[STEP-PROBE] ⚠️ 775-limit hit! steps=${steps.length} < stepCount=${currentCount} — retrying with stepOffset`);
|
||||||
|
// 775-LIMIT FIX: Retry with stepOffset to get latest steps
|
||||||
|
try {
|
||||||
|
const offset = Math.max(0, currentCount - 10);
|
||||||
|
const offsetResp = await sdk.ls.rawRPC('GetCascadeTrajectorySteps', {
|
||||||
|
cascadeId: bestSessionId,
|
||||||
|
stepOffset: offset,
|
||||||
|
});
|
||||||
|
if (offsetResp?.steps?.length > 0) {
|
||||||
|
// Replace steps array with offset results
|
||||||
|
const offsetSteps = offsetResp.steps;
|
||||||
|
logToFile(`[STEP-PROBE] offset=${offset} returned ${offsetSteps.length} steps (latest)`);
|
||||||
|
// Scan for WAITING in offset results
|
||||||
|
for (let osi = offsetSteps.length - 1; osi >= 0; osi--) {
|
||||||
|
const oStep = offsetSteps[osi];
|
||||||
|
if (oStep?.status === 'CORTEX_STEP_STATUS_WAITING') {
|
||||||
|
const toolCall = oStep?.metadata?.toolCall;
|
||||||
|
const toolName = toolCall?.name || (oStep.type || '').replace('CORTEX_STEP_TYPE_', '').toLowerCase();
|
||||||
|
let command = toolName;
|
||||||
|
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 actualIndex = offset + osi;
|
||||||
|
logToFile(`[STEP-PROBE] ★ WAITING (via offset)! step=${actualIndex} type=${oStep.type} cmd='${command}'`);
|
||||||
|
if (actualIndex !== lastPendingStepIndex) {
|
||||||
|
stallProbed = true;
|
||||||
|
lastPendingStepIndex = actualIndex;
|
||||||
|
lastPendingTime = Date.now();
|
||||||
|
sawRunningAfterPending = false;
|
||||||
|
writePendingApproval({
|
||||||
|
conversation_id: activeSessionId,
|
||||||
|
command,
|
||||||
|
description: `Step #${actualIndex} (${(oStep.type || '').replace('CORTEX_STEP_TYPE_', '')})`,
|
||||||
|
step_type: toolName,
|
||||||
|
step_index: actualIndex,
|
||||||
|
source: 'step_probe_offset',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (oe) {
|
||||||
|
logToFile(`[STEP-PROBE] offset retry failed: ${oe.message.substring(0, 100)}`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// 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;
|
||||||
@@ -1603,6 +1714,10 @@ function setupMonitor() {
|
|||||||
if (!foundWaiting) {
|
if (!foundWaiting) {
|
||||||
const lastStep = steps[steps.length - 1];
|
const lastStep = steps[steps.length - 1];
|
||||||
logToFile(`[STEP-PROBE] lastStep status=${lastStep?.status} type=${lastStep?.type} (not waiting)`);
|
logToFile(`[STEP-PROBE] lastStep status=${lastStep?.status} type=${lastStep?.type} (not waiting)`);
|
||||||
|
// CRITICAL: Reset sessionStalled when step_probe confirms no WAITING.
|
||||||
|
// Without this, sessionStalled stays true during long AI generations
|
||||||
|
// (PLANNER_RESPONSE with delta=0), causing false positive "Run" detections.
|
||||||
|
sessionStalled = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1670,37 +1785,134 @@ function setupMonitor() {
|
|||||||
}
|
}
|
||||||
// ── Process latestNotifyUserStep ──
|
// ── Process latestNotifyUserStep ──
|
||||||
const notifyStep = bestSession.latestNotifyUserStep;
|
const notifyStep = bestSession.latestNotifyUserStep;
|
||||||
if (notifyStep && notifyStep.stepIndex > lastNotifyStepIndex) {
|
if (notifyStep) {
|
||||||
lastNotifyStepIndex = notifyStep.stepIndex;
|
if (notifyStep.stepIndex > lastNotifyStepIndex) {
|
||||||
const content = notifyStep.step?.notifyUser?.notificationContent || '';
|
lastNotifyStepIndex = notifyStep.stepIndex;
|
||||||
// Filter: only relay meaningful notifications (skip trivial ones)
|
const content = notifyStep.step?.notifyUser?.notificationContent || '';
|
||||||
if (content.length > 50) {
|
logToFile(`[NOTIFY-STEP] NEW step=${notifyStep.stepIndex} content=${content.length} chars`);
|
||||||
writeChatSnapshot(`📣 **알림** (step ${notifyStep.stepIndex})\n\n${content}`);
|
// Filter: relay all non-empty notifications
|
||||||
console.log(`Gravity Bridge: [POLL#${pollCount}] NOTIFY step=${notifyStep.stepIndex} ${content.length} chars`);
|
if (content.length > 10) {
|
||||||
}
|
writeChatSnapshot(`📣 **알림** (step ${notifyStep.stepIndex})\n\n${content}`);
|
||||||
else if (content.length > 0) {
|
}
|
||||||
logToFile(`[POLL] NOTIFY skipped (too short: ${content.length} chars): ${content.substring(0, 80)}`);
|
else if (content.length > 0) {
|
||||||
|
logToFile(`[NOTIFY-STEP] skipped (too short: ${content.length} chars): ${content}`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
else if (pollCount <= 5) {
|
||||||
|
logToFile(`[NOTIFY-STEP] null (no notify step in session)`);
|
||||||
|
}
|
||||||
// ── Process latestTaskBoundaryStep ──
|
// ── Process latestTaskBoundaryStep ──
|
||||||
const taskStep = bestSession.latestTaskBoundaryStep;
|
const taskStep = bestSession.latestTaskBoundaryStep;
|
||||||
if (taskStep && taskStep.stepIndex > lastTaskStepIndex) {
|
if (taskStep) {
|
||||||
lastTaskStepIndex = taskStep.stepIndex;
|
if (taskStep.stepIndex > lastTaskStepIndex) {
|
||||||
const tb = taskStep.step?.taskBoundary;
|
lastTaskStepIndex = taskStep.stepIndex;
|
||||||
if (tb?.taskName) {
|
const tb = taskStep.step?.taskBoundary;
|
||||||
const mode = tb.mode ? tb.mode.replace('AGENT_MODE_', '') : '';
|
if (tb?.taskName) {
|
||||||
// Filter: skip status-only updates with same task name (noise)
|
const mode = tb.mode ? tb.mode.replace('AGENT_MODE_', '') : '';
|
||||||
const taskText = `${tb.taskName}|${tb.taskStatus || ''}`;
|
const taskText = `${tb.taskName}|${tb.taskStatus || ''}`;
|
||||||
if (taskText !== lastRelayedTaskText) {
|
logToFile(`[TASK-STEP] NEW step=${taskStep.stepIndex} name="${tb.taskName}" mode=${mode}`);
|
||||||
lastRelayedTaskText = taskText;
|
if (taskText !== lastRelayedTaskText) {
|
||||||
writeChatSnapshot(`📋 **[${mode}] ${tb.taskName}**\n${tb.taskStatus || ''}\n\n${tb.taskSummary || ''}`);
|
lastRelayedTaskText = taskText;
|
||||||
console.log(`Gravity Bridge: [POLL#${pollCount}] TASK step=${taskStep.stepIndex} "${tb.taskName}"`);
|
writeChatSnapshot(`📋 **[${mode}] ${tb.taskName}**\n${tb.taskStatus || ''}\n\n${tb.taskSummary || ''}`);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
logToFile(`[POLL] TASK skipped (duplicate): "${tb.taskName}"`);
|
logToFile(`[TASK-STEP] skipped (duplicate): "${tb.taskName}"`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
else if (pollCount <= 5) {
|
||||||
|
logToFile(`[TASK-STEP] null (no task step in session)`);
|
||||||
|
}
|
||||||
|
// ── RUNNING → IDLE transition: capture AI response for Discord ──
|
||||||
|
const userInputIdx = bestSession.lastUserInputStepIndex ?? -1;
|
||||||
|
if (userInputIdx > lastUserInputStepIdx) {
|
||||||
|
lastUserInputStepIdx = userInputIdx;
|
||||||
|
logToFile(`[RESPONSE-CAPTURE] user input detected at step ${userInputIdx}`);
|
||||||
|
}
|
||||||
|
if (wasRunning && !isRunning && currentCount > lastResponseCaptureStep) {
|
||||||
|
logToFile(`[RESPONSE-CAPTURE] RUNNING→IDLE, steps=${currentCount}, capturing response...`);
|
||||||
|
lastResponseCaptureStep = currentCount;
|
||||||
|
try {
|
||||||
|
const offset = Math.max(0, currentCount - 5);
|
||||||
|
const latestResp = await sdk.ls.rawRPC('GetCascadeTrajectorySteps', {
|
||||||
|
cascadeId: bestSessionId,
|
||||||
|
stepOffset: offset,
|
||||||
|
verbosity: 1, // CLIENT_TRAJECTORY_VERBOSITY_DEBUG — includes full plannerResponse text
|
||||||
|
});
|
||||||
|
if (latestResp?.steps?.length > 0) {
|
||||||
|
const steps = latestResp.steps;
|
||||||
|
for (let ri = steps.length - 1; ri >= 0; ri--) {
|
||||||
|
const s = steps[ri];
|
||||||
|
const sType = s?.type || '';
|
||||||
|
if (sType.includes('PLANNER_RESPONSE') || sType.includes('MESSAGE')) {
|
||||||
|
let textContent = '';
|
||||||
|
// Extract from plannerResponse field
|
||||||
|
const pr = s?.plannerResponse;
|
||||||
|
if (pr) {
|
||||||
|
// Priority: modifiedResponse (confirmed field from AG)
|
||||||
|
if (pr.modifiedResponse)
|
||||||
|
textContent = pr.modifiedResponse;
|
||||||
|
else if (pr.rawText)
|
||||||
|
textContent = pr.rawText;
|
||||||
|
else if (pr.text)
|
||||||
|
textContent = pr.text;
|
||||||
|
else if (pr.message)
|
||||||
|
textContent = typeof pr.message === 'string' ? pr.message : '';
|
||||||
|
else if (pr.content?.parts) {
|
||||||
|
for (const p of pr.content.parts) {
|
||||||
|
if (p?.text)
|
||||||
|
textContent += p.text;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Log first time to capture actual field names
|
||||||
|
if (!textContent) {
|
||||||
|
logToFile(`[RESPONSE-CAPTURE] plannerResponse keys: [${Object.keys(pr).join(',')}] values(100): ${JSON.stringify(pr).substring(0, 200)}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Extract from ephemeralMessage field
|
||||||
|
const em = s?.ephemeralMessage;
|
||||||
|
if (!textContent && em) {
|
||||||
|
if (typeof em === 'string')
|
||||||
|
textContent = em;
|
||||||
|
else if (em.message)
|
||||||
|
textContent = em.message;
|
||||||
|
else if (em.content)
|
||||||
|
textContent = typeof em.content === 'string' ? em.content : JSON.stringify(em.content);
|
||||||
|
}
|
||||||
|
// Fallback: metadata, content, rawOutput
|
||||||
|
if (!textContent) {
|
||||||
|
const parts = s?.content?.parts || s?.parts || [];
|
||||||
|
for (const p of parts) {
|
||||||
|
if (p?.text)
|
||||||
|
textContent += p.text;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!textContent && s?.metadata?.text)
|
||||||
|
textContent = s.metadata.text;
|
||||||
|
if (!textContent && s?.rawOutput)
|
||||||
|
textContent = typeof s.rawOutput === 'string' ? s.rawOutput : JSON.stringify(s.rawOutput);
|
||||||
|
if (textContent.length > 10) {
|
||||||
|
logToFile(`[RESPONSE-CAPTURE] found ${sType} (${textContent.length} chars) at step ${offset + ri}`);
|
||||||
|
const truncated = textContent.length > 1800
|
||||||
|
? textContent.substring(0, 1800) + '\n\n_(이하 생략)_'
|
||||||
|
: textContent;
|
||||||
|
writeChatSnapshot(`💬 **AI 응답**\n\n${truncated}`);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
logToFile(`[RESPONSE-CAPTURE] ${sType} too short (${textContent.length}), keys=[${Object.keys(s).join(',')}]`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (re) {
|
||||||
|
logToFile(`[RESPONSE-CAPTURE] error: ${re.message.substring(0, 100)}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
wasRunning = isRunning;
|
||||||
}
|
}
|
||||||
catch (e) {
|
catch (e) {
|
||||||
if (pollCount <= 5 || pollCount % 20 === 0) {
|
if (pollCount <= 5 || pollCount % 20 === 0) {
|
||||||
@@ -1746,6 +1958,11 @@ function setupResponseWatcher() {
|
|||||||
}
|
}
|
||||||
async function processResponseFile(filePath) {
|
async function processResponseFile(filePath) {
|
||||||
try {
|
try {
|
||||||
|
// Gracefully handle files already consumed by HTTP handler
|
||||||
|
if (!fs.existsSync(filePath)) {
|
||||||
|
// HTTP GET /response/:rid already served and deleted this file — skip silently
|
||||||
|
return;
|
||||||
|
}
|
||||||
const content = fs.readFileSync(filePath, 'utf-8');
|
const content = fs.readFileSync(filePath, 'utf-8');
|
||||||
const resp = JSON.parse(content);
|
const resp = JSON.parse(content);
|
||||||
const msg = `[RESPONSE] rid=${resp.request_id} approved=${resp.approved}`;
|
const msg = `[RESPONSE] rid=${resp.request_id} approved=${resp.approved}`;
|
||||||
@@ -1756,12 +1973,16 @@ async function processResponseFile(filePath) {
|
|||||||
const pendingFile = path.join(pendingDir, `${resp.request_id}.json`);
|
const pendingFile = path.join(pendingDir, `${resp.request_id}.json`);
|
||||||
let sessionId = '';
|
let sessionId = '';
|
||||||
let isDomObserver = false;
|
let isDomObserver = false;
|
||||||
|
let pendingStepType = '';
|
||||||
|
let pendingStepIndex = -1;
|
||||||
if (fs.existsSync(pendingFile)) {
|
if (fs.existsSync(pendingFile)) {
|
||||||
try {
|
try {
|
||||||
const pending = JSON.parse(fs.readFileSync(pendingFile, 'utf-8'));
|
const pending = JSON.parse(fs.readFileSync(pendingFile, 'utf-8'));
|
||||||
sessionId = pending.conversation_id || '';
|
sessionId = pending.conversation_id || '';
|
||||||
isDomObserver = pending.auto_detected === true
|
isDomObserver = pending.auto_detected === true
|
||||||
|| pending.source === 'dom_observer';
|
|| pending.source === 'dom_observer';
|
||||||
|
pendingStepType = pending.step_type || '';
|
||||||
|
pendingStepIndex = pending.step_index ?? lastPendingStepIndex;
|
||||||
}
|
}
|
||||||
catch { }
|
catch { }
|
||||||
}
|
}
|
||||||
@@ -1775,15 +1996,10 @@ async function processResponseFile(filePath) {
|
|||||||
logToFile(`[RESPONSE] renderer-handled approval (rid=${resp.request_id})`);
|
logToFile(`[RESPONSE] renderer-handled approval (rid=${resp.request_id})`);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
// Step probe path: approve → trigger renderer click, reject → log only
|
// Step probe path: run ALL approval strategies
|
||||||
if (approved) {
|
logToFile(`[RESPONSE] step_probe → tryApprovalStrategies(${approved}, ${activeSessionId.substring(0, 8)}, type=${pendingStepType}, step=${pendingStepIndex})`);
|
||||||
logToFile(`[RESPONSE] step_probe → approve via trigger-click`);
|
const strategyResult = await tryApprovalStrategies(approved, activeSessionId, pendingStepType, pendingStepIndex);
|
||||||
clickTrigger = { action: 'approve', timestamp: Date.now() };
|
logToFile(`[RESPONSE] strategy result: ${strategyResult}`);
|
||||||
}
|
|
||||||
else {
|
|
||||||
// SAFE: reject logs only — NO ResolveOutstandingSteps (it cancels AI work!)
|
|
||||||
logToFile(`[RESPONSE] step_probe → reject (log only, no destructive action)`);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
logToFile(`[RESPONSE] ${approved ? 'approve' : 'reject'} done (${isDomObserver ? 'dom' : 'step_probe'})`);
|
logToFile(`[RESPONSE] ${approved ? 'approve' : 'reject'} done (${isDomObserver ? 'dom' : 'step_probe'})`);
|
||||||
// Cleanup response file
|
// Cleanup response file
|
||||||
@@ -1996,79 +2212,274 @@ function writePendingApproval(data) {
|
|||||||
* 2. VS Code accept/reject commands (focus-dependent)
|
* 2. VS Code accept/reject commands (focus-dependent)
|
||||||
* 3. Log failure for manual intervention
|
* 3. Log failure for manual intervention
|
||||||
*/
|
*/
|
||||||
async function tryApprovalStrategies(approved, sessionId) {
|
async function tryApprovalStrategies(approved, sessionId, stepType = '', stepIndex = -1) {
|
||||||
const action = approved ? 'APPROVE' : 'REJECT';
|
const action = approved ? 'APPROVE' : 'REJECT';
|
||||||
logToFile(`[APPROVAL] Starting ${action} strategies for session ${sessionId.substring(0, 8)}`);
|
const effectiveStepIndex = stepIndex >= 0 ? stepIndex : lastPendingStepIndex;
|
||||||
// ── Dynamic Command Discovery (check if approval commands appear when step is WAITING) ──
|
logToFile(`[APPROVAL] Starting ${action} strategies for session ${sessionId.substring(0, 8)} stepType=${stepType} stepIndex=${effectiveStepIndex}`);
|
||||||
|
// ── Dynamic Command Discovery (log what's available during WAITING state) ──
|
||||||
|
let approvalCmdList = [];
|
||||||
try {
|
try {
|
||||||
const allCmds = await vscode.commands.getCommands(true);
|
const allCmds = await vscode.commands.getCommands(true);
|
||||||
const agCmds = allCmds.filter((c) => c.startsWith('antigravity.'));
|
const agCmds = allCmds.filter((c) => c.startsWith('antigravity.'));
|
||||||
const approvalCmds = agCmds.filter((c) => {
|
approvalCmdList = agCmds.filter((c) => {
|
||||||
const lower = c.toLowerCase();
|
const lower = c.toLowerCase();
|
||||||
return lower.includes('accept') || lower.includes('reject') || lower.includes('approve')
|
return lower.includes('accept') || lower.includes('reject') || lower.includes('approve')
|
||||||
|| lower.includes('terminal') || lower.includes('run') || lower.includes('step');
|
|| lower.includes('terminal') || lower.includes('run') || lower.includes('step')
|
||||||
|
|| lower.includes('cascade') || lower.includes('action');
|
||||||
});
|
});
|
||||||
logToFile(`[APPROVAL-CMD-CHECK] ${agCmds.length} total, ${approvalCmds.length} approval-related:`);
|
logToFile(`[APPROVAL-CMD-CHECK] ${agCmds.length} total, ${approvalCmdList.length} approval-related:`);
|
||||||
for (const c of approvalCmds) {
|
for (const c of approvalCmdList) {
|
||||||
logToFile(`[APPROVAL-CMD-CHECK] → ${c}`);
|
logToFile(`[APPROVAL-CMD-CHECK] → ${c}`);
|
||||||
}
|
}
|
||||||
// Dump ALL antigravity.* commands for full comparison
|
|
||||||
logToFile(`[APPROVAL-CMD-CHECK] FULL LIST (${agCmds.length}):`);
|
|
||||||
for (const c of agCmds) {
|
|
||||||
logToFile(`[APPROVAL-CMD-FULL] ${c}`);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
catch (e) {
|
catch (e) {
|
||||||
logToFile(`[APPROVAL-CMD-CHECK] error: ${e.message}`);
|
logToFile(`[APPROVAL-CMD-CHECK] error: ${e.message}`);
|
||||||
}
|
}
|
||||||
// ── Strategy 1: HandleCascadeUserInteraction RPC ──
|
// ══════════════════════════════════════════════════════════
|
||||||
if (sdk) {
|
// STRATEGY 0-PROTO: Correct proto-based RPC (decoded from AG source)
|
||||||
// Try variant A: { cascadeId, approved }
|
// Routes interaction sub-message by step_type:
|
||||||
try {
|
// run_command → CascadeRunCommandInteraction { confirm }
|
||||||
logToFile(`[APPROVAL-1A] HandleCascadeUserInteraction { cascadeId, approved: ${approved} }`);
|
// write_to_file → AcknowledgeCascadeCodeEdit RPC (separate)
|
||||||
const rpcResult = await sdk.ls.rawRPC('HandleCascadeUserInteraction', {
|
// open_browser_url → CascadeOpenBrowserUrlInteraction { confirm }
|
||||||
|
// send_command_input → CascadeSendCommandInputInteraction { confirm }
|
||||||
|
// read_url_content → CascadeReadUrlContentInteraction { confirm }
|
||||||
|
// mcp_tool → CascadeMcpInteraction { confirm }
|
||||||
|
// invoke_subagent → CascadeRunExtensionCodeInteraction { confirm }
|
||||||
|
// ══════════════════════════════════════════════════════════
|
||||||
|
if (sdk && approved) {
|
||||||
|
// Build interaction sub-message based on step_type
|
||||||
|
const typeLower = stepType.toLowerCase().replace('cortex_step_type_', '');
|
||||||
|
let interactionPayload = {};
|
||||||
|
if (typeLower.includes('write_to_file') || typeLower.includes('propose_code') || typeLower.includes('write_cascade_edit')) {
|
||||||
|
// CODE EDIT: Uses separate AcknowledgeCascadeCodeEdit RPC
|
||||||
|
try {
|
||||||
|
logToFile(`[APPROVAL-PROTO-ACK] AcknowledgeCascadeCodeEdit(cascadeId=${sessionId.substring(0, 8)}, accept=true, stepIndices=[${effectiveStepIndex}])`);
|
||||||
|
const ackResult = await sdk.ls.rawRPC('AcknowledgeCascadeCodeEdit', {
|
||||||
|
cascadeId: sessionId,
|
||||||
|
accept: true,
|
||||||
|
stepIndices: [effectiveStepIndex],
|
||||||
|
});
|
||||||
|
logToFile(`[APPROVAL-PROTO-ACK] ✅ SUCCESS: ${JSON.stringify(ackResult).substring(0, 200)}`);
|
||||||
|
return `RPC-PROTO-ACK:AcknowledgeCascadeCodeEdit`;
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
logToFile(`[APPROVAL-PROTO-ACK] ❌ ${e.message.substring(0, 200)}`);
|
||||||
|
// Fall through to HandleCascadeUserInteraction
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Map step_type to interaction sub-message field
|
||||||
|
if (typeLower.includes('run_command') || typeLower.includes('shell_exec')) {
|
||||||
|
interactionPayload = { runCommand: { confirm: true } };
|
||||||
|
}
|
||||||
|
else if (typeLower.includes('open_browser')) {
|
||||||
|
interactionPayload = { openBrowserUrl: { confirm: true } };
|
||||||
|
}
|
||||||
|
else if (typeLower.includes('send_command_input')) {
|
||||||
|
interactionPayload = { sendCommandInput: { confirm: true } }; // guess field name
|
||||||
|
}
|
||||||
|
else if (typeLower.includes('read_url')) {
|
||||||
|
interactionPayload = { readUrlContent: { confirm: true } }; // guess
|
||||||
|
}
|
||||||
|
else if (typeLower.includes('mcp')) {
|
||||||
|
interactionPayload = { mcpTool: { confirm: true } }; // guess
|
||||||
|
}
|
||||||
|
else if (typeLower.includes('invoke_subagent') || typeLower.includes('extension_code')) {
|
||||||
|
interactionPayload = { runExtensionCode: { confirm: true } };
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// Default: try run_command (most common)
|
||||||
|
interactionPayload = { runCommand: { confirm: true } };
|
||||||
|
}
|
||||||
|
const protoVariants = [
|
||||||
|
// Variant A: camelCase with trajectoryId (proven working for run_command)
|
||||||
|
{
|
||||||
cascadeId: sessionId,
|
cascadeId: sessionId,
|
||||||
approved: approved,
|
interaction: {
|
||||||
});
|
trajectoryId: activeTrajectoryId || sessionId,
|
||||||
logToFile(`[APPROVAL-1A] ✅ SUCCESS: ${JSON.stringify(rpcResult).substring(0, 200)}`);
|
stepIndex: effectiveStepIndex,
|
||||||
return `RPC-1A:HandleCascadeUserInteraction(approved=${approved})`;
|
...interactionPayload,
|
||||||
}
|
},
|
||||||
catch (e) {
|
},
|
||||||
logToFile(`[APPROVAL-1A] ❌ FAIL: ${e.message}`);
|
// Variant B: snake_case
|
||||||
}
|
{
|
||||||
// Try variant B: { cascadeId, stepAction }
|
cascade_id: sessionId,
|
||||||
try {
|
interaction: {
|
||||||
const stepAction = approved ? 'STEP_ACTION_ACCEPT' : 'STEP_ACTION_REJECT';
|
trajectory_id: activeTrajectoryId || sessionId,
|
||||||
logToFile(`[APPROVAL-1B] HandleCascadeUserInteraction { cascadeId, stepAction: '${stepAction}' }`);
|
step_index: effectiveStepIndex,
|
||||||
const rpcResult = await sdk.ls.rawRPC('HandleCascadeUserInteraction', {
|
...interactionPayload,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// Variant C: minimal (no trajectoryId)
|
||||||
|
{
|
||||||
cascadeId: sessionId,
|
cascadeId: sessionId,
|
||||||
stepAction: stepAction,
|
interaction: {
|
||||||
});
|
stepIndex: effectiveStepIndex,
|
||||||
logToFile(`[APPROVAL-1B] ✅ SUCCESS: ${JSON.stringify(rpcResult).substring(0, 200)}`);
|
...interactionPayload,
|
||||||
return `RPC-1B:HandleCascadeUserInteraction(stepAction=${stepAction})`;
|
},
|
||||||
}
|
},
|
||||||
catch (e) {
|
];
|
||||||
logToFile(`[APPROVAL-1B] ❌ FAIL: ${e.message}`);
|
for (let i = 0; i < protoVariants.length; i++) {
|
||||||
}
|
try {
|
||||||
// Try variant C: { cascadeId, userAction } (experimental)
|
const payload = protoVariants[i];
|
||||||
try {
|
logToFile(`[APPROVAL-PROTO-${i}] HandleCascadeUserInteraction(${JSON.stringify(payload).substring(0, 250)})`);
|
||||||
const userAction = approved ? 'USER_ACTION_APPROVED' : 'USER_ACTION_REJECTED';
|
const rpcResult = await sdk.ls.rawRPC('HandleCascadeUserInteraction', payload);
|
||||||
logToFile(`[APPROVAL-1C] HandleCascadeUserInteraction { cascadeId, userAction: '${userAction}' }`);
|
logToFile(`[APPROVAL-PROTO-${i}] ✅ SUCCESS: ${JSON.stringify(rpcResult).substring(0, 200)}`);
|
||||||
const rpcResult = await sdk.ls.rawRPC('HandleCascadeUserInteraction', {
|
return `RPC-PROTO-${i}:HandleCascadeUserInteraction(${typeLower})`;
|
||||||
cascadeId: sessionId,
|
}
|
||||||
userAction: userAction,
|
catch (e) {
|
||||||
});
|
logToFile(`[APPROVAL-PROTO-${i}] ❌ ${e.message.substring(0, 300)}`);
|
||||||
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: Renderer DOM Click via HTTP Bridge ──
|
// ══════════════════════════════════════════════════════════
|
||||||
// 2026-03-09: All SDK approval commands NOT REGISTERED (119 cmds tested).
|
// STRATEGY 0A: executeCascadeAction
|
||||||
// Keyboard simulation sends Enter to chat input (sends empty message — WRONG).
|
// ══════════════════════════════════════════════════════════
|
||||||
// New approach: set clickTrigger flag → renderer polls /trigger-click → clicks button.
|
const cascadeActionVariants = [
|
||||||
|
{ action: approved ? 'accept' : 'reject' },
|
||||||
|
{ action: approved ? 'ACCEPT' : 'REJECT' },
|
||||||
|
{ action: approved ? 'approve' : 'deny' },
|
||||||
|
{ action: approved ? 'run' : 'reject' },
|
||||||
|
{ cascadeId: sessionId, action: approved ? 'accept' : 'reject' },
|
||||||
|
{ cascadeId: sessionId, type: approved ? 'accept' : 'reject' },
|
||||||
|
approved ? 'accept' : 'reject', // plain string arg
|
||||||
|
approved ? 1 : 0, // numeric arg
|
||||||
|
];
|
||||||
|
for (let i = 0; i < cascadeActionVariants.length; i++) {
|
||||||
|
try {
|
||||||
|
const arg = cascadeActionVariants[i];
|
||||||
|
logToFile(`[APPROVAL-0A-${i}] executeCascadeAction(${JSON.stringify(arg)})`);
|
||||||
|
const result = await vscode.commands.executeCommand('antigravity.executeCascadeAction', arg);
|
||||||
|
logToFile(`[APPROVAL-0A-${i}] ✅ SUCCESS: ${JSON.stringify(result).substring(0, 200)}`);
|
||||||
|
return `CMD-0A:executeCascadeAction(variant=${i})`;
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
logToFile(`[APPROVAL-0A-${i}] ❌ ${e.message.substring(0, 100)}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// ══════════════════════════════════════════════════════════
|
||||||
|
// STRATEGY 0B: Direct approval commands (brute-force try all 7 SDK commands)
|
||||||
|
// ══════════════════════════════════════════════════════════
|
||||||
|
const commandsToTry = approved
|
||||||
|
? [
|
||||||
|
'antigravity.terminalCommand.run',
|
||||||
|
'antigravity.terminalCommand.accept',
|
||||||
|
'antigravity.agent.acceptAgentStep',
|
||||||
|
'antigravity.command.accept',
|
||||||
|
]
|
||||||
|
: [
|
||||||
|
'antigravity.terminalCommand.reject',
|
||||||
|
'antigravity.agent.rejectAgentStep',
|
||||||
|
'antigravity.command.reject',
|
||||||
|
];
|
||||||
|
for (const cmd of commandsToTry) {
|
||||||
|
// Try with no args, then with sessionId
|
||||||
|
for (const args of [[], [sessionId], [{ cascadeId: sessionId }]]) {
|
||||||
|
try {
|
||||||
|
logToFile(`[APPROVAL-0B] ${cmd}(${JSON.stringify(args)})`);
|
||||||
|
const result = await vscode.commands.executeCommand(cmd, ...args);
|
||||||
|
logToFile(`[APPROVAL-0B] ✅ SUCCESS: ${JSON.stringify(result).substring(0, 200)}`);
|
||||||
|
return `CMD-0B:${cmd}`;
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
logToFile(`[APPROVAL-0B] ❌ ${e.message.substring(0, 80)}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// ══════════════════════════════════════════════════════════
|
||||||
|
// STRATEGY 0C: Electron webContents access (pierce webview isolation)
|
||||||
|
// ══════════════════════════════════════════════════════════
|
||||||
|
try {
|
||||||
|
logToFile(`[APPROVAL-0C] Attempting Electron webContents access...`);
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||||
|
const electron = require('electron');
|
||||||
|
const remote = electron.remote;
|
||||||
|
if (remote) {
|
||||||
|
logToFile(`[APPROVAL-0C] electron.remote available!`);
|
||||||
|
const allWC = remote.webContents.getAllWebContents();
|
||||||
|
logToFile(`[APPROVAL-0C] Found ${allWC.length} webContents`);
|
||||||
|
for (const wc of allWC) {
|
||||||
|
const wcUrl = wc.getURL() || '';
|
||||||
|
logToFile(`[APPROVAL-0C] wc: ${wcUrl.substring(0, 100)}`);
|
||||||
|
if (wcUrl.includes('vscode-webview://') || wcUrl.includes('webview')) {
|
||||||
|
const clickScript = approved
|
||||||
|
? `(function(){ var btns = document.querySelectorAll('button'); for(var b of btns){ if(/Run|Accept|Allow/i.test(b.textContent)){ b.click(); return 'clicked:'+b.textContent; } } return 'no-match'; })()`
|
||||||
|
: `(function(){ var btns = document.querySelectorAll('button'); for(var b of btns){ if(/Reject|Deny|Cancel/i.test(b.textContent)){ b.click(); return 'clicked:'+b.textContent; } } return 'no-match'; })()`;
|
||||||
|
const clickResult = await wc.executeJavaScript(clickScript);
|
||||||
|
logToFile(`[APPROVAL-0C] executeJavaScript result: ${clickResult}`);
|
||||||
|
if (clickResult && clickResult.startsWith('clicked:')) {
|
||||||
|
return `ELECTRON-0C:webContents(${clickResult})`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
logToFile(`[APPROVAL-0C] electron.remote NOT available`);
|
||||||
|
// Try without remote (main process context)
|
||||||
|
const wc = electron.webContents;
|
||||||
|
if (wc && typeof wc.getAllWebContents === 'function') {
|
||||||
|
logToFile(`[APPROVAL-0C] Direct electron.webContents available!`);
|
||||||
|
const allWC = wc.getAllWebContents();
|
||||||
|
logToFile(`[APPROVAL-0C] Found ${allWC.length} webContents`);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
logToFile(`[APPROVAL-0C] No webContents access from Extension Host`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
logToFile(`[APPROVAL-0C] ❌ ${e.message.substring(0, 150)}`);
|
||||||
|
}
|
||||||
|
// ══════════════════════════════════════════════════════════
|
||||||
|
// STRATEGY 0D: sendChatActionMessage (may route to agent)
|
||||||
|
// ══════════════════════════════════════════════════════════
|
||||||
|
const chatActionVariants = [
|
||||||
|
{ action: approved ? 'accept' : 'reject', cascadeId: sessionId },
|
||||||
|
{ message: approved ? 'accept' : 'reject', conversationId: sessionId },
|
||||||
|
approved ? 'accept' : 'reject',
|
||||||
|
];
|
||||||
|
for (let i = 0; i < chatActionVariants.length; i++) {
|
||||||
|
try {
|
||||||
|
logToFile(`[APPROVAL-0D-${i}] sendChatActionMessage(${JSON.stringify(chatActionVariants[i])})`);
|
||||||
|
const result = await vscode.commands.executeCommand('antigravity.sendChatActionMessage', chatActionVariants[i]);
|
||||||
|
logToFile(`[APPROVAL-0D-${i}] ✅ SUCCESS: ${JSON.stringify(result).substring(0, 200)}`);
|
||||||
|
return `CMD-0D:sendChatActionMessage(variant=${i})`;
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
logToFile(`[APPROVAL-0D-${i}] ❌ ${e.message.substring(0, 80)}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// ══════════════════════════════════════════════════════════
|
||||||
|
// STRATEGY 1: HandleCascadeUserInteraction RPC (expanded variants)
|
||||||
|
// ══════════════════════════════════════════════════════════
|
||||||
|
if (sdk) {
|
||||||
|
const rpcVariants = [
|
||||||
|
// Original variants
|
||||||
|
{ cascadeId: sessionId, approved: approved },
|
||||||
|
{ cascadeId: sessionId, stepAction: approved ? 'STEP_ACTION_ACCEPT' : 'STEP_ACTION_REJECT' },
|
||||||
|
{ cascadeId: sessionId, userAction: approved ? 'USER_ACTION_APPROVED' : 'USER_ACTION_REJECTED' },
|
||||||
|
// New variants — ConnectRPC protobuf field naming conventions
|
||||||
|
{ cascade_id: sessionId, accepted: approved },
|
||||||
|
{ cascadeId: sessionId, accepted: approved },
|
||||||
|
{ cascade_id: sessionId, user_action: approved ? 1 : 2 }, // enum as int
|
||||||
|
{ cascadeId: sessionId, interaction: approved ? 'ACCEPT' : 'REJECT' },
|
||||||
|
{ cascadeId: sessionId, action: approved ? 'accept' : 'reject' },
|
||||||
|
// With step index from last known waiting step
|
||||||
|
{ cascadeId: sessionId, stepIndex: lastPendingStepIndex, accepted: approved },
|
||||||
|
{ cascadeId: sessionId, stepIndex: lastPendingStepIndex, approved: approved },
|
||||||
|
];
|
||||||
|
for (let i = 0; i < rpcVariants.length; i++) {
|
||||||
|
try {
|
||||||
|
logToFile(`[APPROVAL-1-${i}] HandleCascadeUserInteraction(${JSON.stringify(rpcVariants[i]).substring(0, 120)})`);
|
||||||
|
const rpcResult = await sdk.ls.rawRPC('HandleCascadeUserInteraction', rpcVariants[i]);
|
||||||
|
logToFile(`[APPROVAL-1-${i}] ✅ SUCCESS: ${JSON.stringify(rpcResult).substring(0, 200)}`);
|
||||||
|
return `RPC-1-${i}:HandleCascadeUserInteraction`;
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
logToFile(`[APPROVAL-1-${i}] ❌ ${e.message.substring(0, 80)}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// ── Strategy 2: Renderer DOM Click via HTTP Bridge (kept as fallback) ──
|
||||||
try {
|
try {
|
||||||
const triggerAction = approved ? 'approve' : 'reject';
|
const triggerAction = approved ? 'approve' : 'reject';
|
||||||
logToFile(`[APPROVAL-2] Setting clickTrigger=${triggerAction} for renderer DOM click`);
|
logToFile(`[APPROVAL-2] Setting clickTrigger=${triggerAction} for renderer DOM click`);
|
||||||
@@ -2078,19 +2489,11 @@ async function tryApprovalStrategies(approved, sessionId) {
|
|||||||
catch (e) {
|
catch (e) {
|
||||||
logToFile(`[APPROVAL-2] ❌ FAIL: ${e.message}`);
|
logToFile(`[APPROVAL-2] ❌ FAIL: ${e.message}`);
|
||||||
}
|
}
|
||||||
// ── Strategy 3: ResolveOutstandingSteps (REJECT ONLY — this CANCELS!) ──
|
// ── Strategy 3: ResolveOutstandingSteps — DISABLED (too destructive!) ──
|
||||||
|
// This was cancelling AG work when bot sent approved=false.
|
||||||
|
// DO NOT enable without explicit user confirmation.
|
||||||
if (!approved && sdk) {
|
if (!approved && sdk) {
|
||||||
try {
|
logToFile(`[APPROVAL-3] ResolveOutstandingSteps DISABLED — reject only logs, no cancel`);
|
||||||
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`);
|
logToFile(`[APPROVAL] ⚠️ ALL strategies attempted — check logs for results`);
|
||||||
return `ALL_ATTEMPTED:${action}`;
|
return `ALL_ATTEMPTED:${action}`;
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -81,6 +81,7 @@ function ensureBridgeDir() {
|
|||||||
|
|
||||||
// Module-level activeSessionId so writeChatSnapshot can register sessions lazily
|
// Module-level activeSessionId so writeChatSnapshot can register sessions lazily
|
||||||
let activeSessionId = '';
|
let activeSessionId = '';
|
||||||
|
let activeTrajectoryId = '';
|
||||||
|
|
||||||
function writeChatSnapshot(text: string) {
|
function writeChatSnapshot(text: string) {
|
||||||
try {
|
try {
|
||||||
@@ -97,6 +98,8 @@ function writeChatSnapshot(text: string) {
|
|||||||
const filePath = path.join(snapshotDir, `${id}.json`);
|
const filePath = path.join(snapshotDir, `${id}.json`);
|
||||||
fs.writeFileSync(filePath, JSON.stringify(data, null, 2), 'utf-8');
|
fs.writeFileSync(filePath, JSON.stringify(data, null, 2), 'utf-8');
|
||||||
console.log(`Gravity Bridge: chat snapshot written (${text.length} chars) → ${id}.json`);
|
console.log(`Gravity Bridge: chat snapshot written (${text.length} chars) → ${id}.json`);
|
||||||
|
logToFile(`[SNAPSHOT] written ${id}.json (${text.length} chars)`);
|
||||||
|
logToFile(`[SNAPSHOT] content: ${text.substring(0, 200)}`);
|
||||||
// Lazily register session → project mapping (correct because projectName is per-window)
|
// Lazily register session → project mapping (correct because projectName is per-window)
|
||||||
if (activeSessionId) { writeRegistration(activeSessionId); }
|
if (activeSessionId) { writeRegistration(activeSessionId); }
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
@@ -403,6 +406,7 @@ const pendingResponses = new Map<string, { approved: boolean } | null>();
|
|||||||
// Click trigger: extension sets this, renderer polls and clicks button
|
// Click trigger: extension sets this, renderer polls and clicks button
|
||||||
let clickTrigger: { action: 'approve' | 'reject'; timestamp: number } | null = null;
|
let clickTrigger: { action: 'approve' | 'reject'; timestamp: number } | null = null;
|
||||||
let sessionStalled = false; // true when session is stalled waiting for approval
|
let sessionStalled = false; // true when session is stalled waiting for approval
|
||||||
|
let lastPendingStepIndex = -1; // dedup: don't re-create pending for same step (module-level for tryApprovalStrategies)
|
||||||
|
|
||||||
// Deep inspect trigger: curl sets this, renderer picks it up and POSTs results back
|
// Deep inspect trigger: curl sets this, renderer picks it up and POSTs results back
|
||||||
let deepInspectRequested = false;
|
let deepInspectRequested = false;
|
||||||
@@ -489,8 +493,12 @@ function startObserverHttpBridge(): Promise<number> {
|
|||||||
if (fs.existsSync(respFile)) {
|
if (fs.existsSync(respFile)) {
|
||||||
try {
|
try {
|
||||||
const data = JSON.parse(fs.readFileSync(respFile, 'utf8'));
|
const data = JSON.parse(fs.readFileSync(respFile, 'utf8'));
|
||||||
fs.unlinkSync(respFile);
|
logToFile(`[HTTP] response served to renderer: ${rid} approved=${data.approved}`);
|
||||||
logToFile(`[HTTP] response sent: ${rid} approved=${data.approved}`);
|
// Delay deletion: processResponseFile (response watcher) may need to read it too.
|
||||||
|
// The watcher fires with 300ms delay, so 2s is safe.
|
||||||
|
setTimeout(() => {
|
||||||
|
try { fs.unlinkSync(respFile); } catch { }
|
||||||
|
}, 2000);
|
||||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||||
res.end(JSON.stringify(data));
|
res.end(JSON.stringify(data));
|
||||||
} catch {
|
} catch {
|
||||||
@@ -1376,13 +1384,16 @@ function setupMonitor() {
|
|||||||
let lastKnownStepCount = 0;
|
let lastKnownStepCount = 0;
|
||||||
let lastNotifyStepIndex = -1;
|
let lastNotifyStepIndex = -1;
|
||||||
let lastTaskStepIndex = -1;
|
let lastTaskStepIndex = -1;
|
||||||
let lastPendingStepIndex = -1; // dedup: don't re-create pending for same step
|
// lastPendingStepIndex is module-level (above sessionStalled)
|
||||||
let consecutiveIdleCount = 0; // debounce: require N consecutive stall polls
|
let consecutiveIdleCount = 0; // debounce: require N consecutive stall polls
|
||||||
let lastPendingTime = 0; // cooldown: minimum gap between pendings
|
let lastPendingTime = 0; // cooldown: minimum gap between pendings
|
||||||
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
|
let lastRelayedTaskText = ''; // dedup TASK_BOUNDARY relay
|
||||||
|
let wasRunning = false; // track RUNNING→IDLE transition for response capture
|
||||||
|
let lastUserInputStepIdx = -1; // track user input for response matching
|
||||||
|
let lastResponseCaptureStep = -1; // dedup: don't capture same response twice
|
||||||
|
|
||||||
setInterval(async () => {
|
setInterval(async () => {
|
||||||
pollCount++;
|
pollCount++;
|
||||||
@@ -1432,6 +1443,7 @@ function setupMonitor() {
|
|||||||
// Session changed?
|
// Session changed?
|
||||||
if (bestSessionId !== activeSessionId) {
|
if (bestSessionId !== activeSessionId) {
|
||||||
activeSessionId = bestSessionId;
|
activeSessionId = bestSessionId;
|
||||||
|
activeTrajectoryId = (bestSession as any).trajectoryId || '';
|
||||||
activeSessionTitle = currentTitle;
|
activeSessionTitle = currentTitle;
|
||||||
lastKnownStepCount = currentCount;
|
lastKnownStepCount = currentCount;
|
||||||
lastNotifyStepIndex = bestSession.latestNotifyUserStep?.stepIndex ?? -1;
|
lastNotifyStepIndex = bestSession.latestNotifyUserStep?.stepIndex ?? -1;
|
||||||
@@ -1456,6 +1468,44 @@ function setupMonitor() {
|
|||||||
|
|
||||||
if (delta > 0) {
|
if (delta > 0) {
|
||||||
console.log(`Gravity Bridge: [POLL#${pollCount}] +${delta} steps (${currentCount}) "${currentTitle}"`);
|
console.log(`Gravity Bridge: [POLL#${pollCount}] +${delta} steps (${currentCount}) "${currentTitle}"`);
|
||||||
|
|
||||||
|
// Real-time response capture: fetch latest steps on every delta>0
|
||||||
|
if (isRunning && currentCount > lastResponseCaptureStep && sdk) {
|
||||||
|
try {
|
||||||
|
const rtOffset = Math.max(0, currentCount - 3);
|
||||||
|
const rtResp = await sdk.ls.rawRPC('GetCascadeTrajectorySteps', {
|
||||||
|
cascadeId: bestSessionId,
|
||||||
|
stepOffset: rtOffset,
|
||||||
|
verbosity: 1, // DEBUG — includes plannerResponse text
|
||||||
|
});
|
||||||
|
if (rtResp?.steps?.length > 0) {
|
||||||
|
for (let ri = rtResp.steps.length - 1; ri >= 0; ri--) {
|
||||||
|
const s = rtResp.steps[ri];
|
||||||
|
const sType = s?.type || '';
|
||||||
|
const actualIdx = rtOffset + ri;
|
||||||
|
if (actualIdx <= lastResponseCaptureStep) continue;
|
||||||
|
if (sType.includes('PLANNER_RESPONSE') && s?.status?.includes('DONE')) {
|
||||||
|
const pr = s?.plannerResponse;
|
||||||
|
if (pr) {
|
||||||
|
let text = pr.modifiedResponse || pr.rawText || pr.text || '';
|
||||||
|
if (text.length > 10) {
|
||||||
|
lastResponseCaptureStep = actualIdx;
|
||||||
|
logToFile(`[RT-CAPTURE] step=${actualIdx} (${text.length} chars)`);
|
||||||
|
const truncated = text.length > 1800
|
||||||
|
? text.substring(0, 1800) + '\n\n_(이하 생략)_'
|
||||||
|
: text;
|
||||||
|
writeChatSnapshot(`💬 **AI 응답**\n\n${truncated}`);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (rte: any) {
|
||||||
|
// Non-critical — don't spam logs
|
||||||
|
if (pollCount <= 5) logToFile(`[RT-CAPTURE] error: ${rte.message.substring(0, 80)}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Log session state on EVERY poll for diagnostics
|
// Log session state on EVERY poll for diagnostics
|
||||||
@@ -1532,7 +1582,56 @@ function setupMonitor() {
|
|||||||
// 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) {
|
if (steps.length < currentCount) {
|
||||||
logToFile(`[STEP-PROBE] ⚠️ 775-limit hit! steps=${steps.length} < stepCount=${currentCount}`);
|
logToFile(`[STEP-PROBE] ⚠️ 775-limit hit! steps=${steps.length} < stepCount=${currentCount} — retrying with stepOffset`);
|
||||||
|
// 775-LIMIT FIX: Retry with stepOffset to get latest steps
|
||||||
|
try {
|
||||||
|
const offset = Math.max(0, currentCount - 10);
|
||||||
|
const offsetResp = await sdk.ls.rawRPC('GetCascadeTrajectorySteps', {
|
||||||
|
cascadeId: bestSessionId,
|
||||||
|
stepOffset: offset,
|
||||||
|
});
|
||||||
|
if (offsetResp?.steps?.length > 0) {
|
||||||
|
// Replace steps array with offset results
|
||||||
|
const offsetSteps = offsetResp.steps;
|
||||||
|
logToFile(`[STEP-PROBE] offset=${offset} returned ${offsetSteps.length} steps (latest)`);
|
||||||
|
// Scan for WAITING in offset results
|
||||||
|
for (let osi = offsetSteps.length - 1; osi >= 0; osi--) {
|
||||||
|
const oStep = offsetSteps[osi];
|
||||||
|
if (oStep?.status === 'CORTEX_STEP_STATUS_WAITING') {
|
||||||
|
const toolCall = oStep?.metadata?.toolCall;
|
||||||
|
const toolName = toolCall?.name || (oStep.type || '').replace('CORTEX_STEP_TYPE_', '').toLowerCase();
|
||||||
|
let command = toolName;
|
||||||
|
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 actualIndex = offset + osi;
|
||||||
|
logToFile(`[STEP-PROBE] ★ WAITING (via offset)! step=${actualIndex} type=${oStep.type} cmd='${command}'`);
|
||||||
|
if (actualIndex !== lastPendingStepIndex) {
|
||||||
|
stallProbed = true;
|
||||||
|
lastPendingStepIndex = actualIndex;
|
||||||
|
lastPendingTime = Date.now();
|
||||||
|
sawRunningAfterPending = false;
|
||||||
|
writePendingApproval({
|
||||||
|
conversation_id: activeSessionId,
|
||||||
|
command,
|
||||||
|
description: `Step #${actualIndex} (${(oStep.type || '').replace('CORTEX_STEP_TYPE_', '')})`,
|
||||||
|
step_type: toolName,
|
||||||
|
step_index: actualIndex,
|
||||||
|
source: 'step_probe_offset',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (oe: any) {
|
||||||
|
logToFile(`[STEP-PROBE] offset retry failed: ${oe.message.substring(0, 100)}`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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)
|
||||||
@@ -1586,6 +1685,10 @@ function setupMonitor() {
|
|||||||
if (!foundWaiting) {
|
if (!foundWaiting) {
|
||||||
const lastStep = steps[steps.length - 1];
|
const lastStep = steps[steps.length - 1];
|
||||||
logToFile(`[STEP-PROBE] lastStep status=${lastStep?.status} type=${lastStep?.type} (not waiting)`);
|
logToFile(`[STEP-PROBE] lastStep status=${lastStep?.status} type=${lastStep?.type} (not waiting)`);
|
||||||
|
// CRITICAL: Reset sessionStalled when step_probe confirms no WAITING.
|
||||||
|
// Without this, sessionStalled stays true during long AI generations
|
||||||
|
// (PLANNER_RESPONSE with delta=0), causing false positive "Run" detections.
|
||||||
|
sessionStalled = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
@@ -1649,36 +1752,120 @@ function setupMonitor() {
|
|||||||
|
|
||||||
// ── Process latestNotifyUserStep ──
|
// ── Process latestNotifyUserStep ──
|
||||||
const notifyStep = bestSession.latestNotifyUserStep;
|
const notifyStep = bestSession.latestNotifyUserStep;
|
||||||
if (notifyStep && notifyStep.stepIndex > lastNotifyStepIndex) {
|
if (notifyStep) {
|
||||||
lastNotifyStepIndex = notifyStep.stepIndex;
|
if (notifyStep.stepIndex > lastNotifyStepIndex) {
|
||||||
const content = notifyStep.step?.notifyUser?.notificationContent || '';
|
lastNotifyStepIndex = notifyStep.stepIndex;
|
||||||
// Filter: only relay meaningful notifications (skip trivial ones)
|
const content = notifyStep.step?.notifyUser?.notificationContent || '';
|
||||||
if (content.length > 50) {
|
logToFile(`[NOTIFY-STEP] NEW step=${notifyStep.stepIndex} content=${content.length} chars`);
|
||||||
writeChatSnapshot(`📣 **알림** (step ${notifyStep.stepIndex})\n\n${content}`);
|
// Filter: relay all non-empty notifications
|
||||||
console.log(`Gravity Bridge: [POLL#${pollCount}] NOTIFY step=${notifyStep.stepIndex} ${content.length} chars`);
|
if (content.length > 10) {
|
||||||
} else if (content.length > 0) {
|
writeChatSnapshot(`📣 **알림** (step ${notifyStep.stepIndex})\n\n${content}`);
|
||||||
logToFile(`[POLL] NOTIFY skipped (too short: ${content.length} chars): ${content.substring(0, 80)}`);
|
} else if (content.length > 0) {
|
||||||
|
logToFile(`[NOTIFY-STEP] skipped (too short: ${content.length} chars): ${content}`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
} else if (pollCount <= 5) {
|
||||||
|
logToFile(`[NOTIFY-STEP] null (no notify step in session)`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Process latestTaskBoundaryStep ──
|
// ── Process latestTaskBoundaryStep ──
|
||||||
const taskStep = bestSession.latestTaskBoundaryStep;
|
const taskStep = bestSession.latestTaskBoundaryStep;
|
||||||
if (taskStep && taskStep.stepIndex > lastTaskStepIndex) {
|
if (taskStep) {
|
||||||
lastTaskStepIndex = taskStep.stepIndex;
|
if (taskStep.stepIndex > lastTaskStepIndex) {
|
||||||
const tb = taskStep.step?.taskBoundary;
|
lastTaskStepIndex = taskStep.stepIndex;
|
||||||
if (tb?.taskName) {
|
const tb = taskStep.step?.taskBoundary;
|
||||||
const mode = tb.mode ? tb.mode.replace('AGENT_MODE_', '') : '';
|
if (tb?.taskName) {
|
||||||
// Filter: skip status-only updates with same task name (noise)
|
const mode = tb.mode ? tb.mode.replace('AGENT_MODE_', '') : '';
|
||||||
const taskText = `${tb.taskName}|${tb.taskStatus || ''}`;
|
const taskText = `${tb.taskName}|${tb.taskStatus || ''}`;
|
||||||
if (taskText !== lastRelayedTaskText) {
|
logToFile(`[TASK-STEP] NEW step=${taskStep.stepIndex} name="${tb.taskName}" mode=${mode}`);
|
||||||
lastRelayedTaskText = taskText;
|
if (taskText !== lastRelayedTaskText) {
|
||||||
writeChatSnapshot(`📋 **[${mode}] ${tb.taskName}**\n${tb.taskStatus || ''}\n\n${tb.taskSummary || ''}`);
|
lastRelayedTaskText = taskText;
|
||||||
console.log(`Gravity Bridge: [POLL#${pollCount}] TASK step=${taskStep.stepIndex} "${tb.taskName}"`);
|
writeChatSnapshot(`📋 **[${mode}] ${tb.taskName}**\n${tb.taskStatus || ''}\n\n${tb.taskSummary || ''}`);
|
||||||
} else {
|
} else {
|
||||||
logToFile(`[POLL] TASK skipped (duplicate): "${tb.taskName}"`);
|
logToFile(`[TASK-STEP] skipped (duplicate): "${tb.taskName}"`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else if (pollCount <= 5) {
|
||||||
|
logToFile(`[TASK-STEP] null (no task step in session)`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── RUNNING → IDLE transition: capture AI response for Discord ──
|
||||||
|
const userInputIdx = bestSession.lastUserInputStepIndex ?? -1;
|
||||||
|
if (userInputIdx > lastUserInputStepIdx) {
|
||||||
|
lastUserInputStepIdx = userInputIdx;
|
||||||
|
logToFile(`[RESPONSE-CAPTURE] user input detected at step ${userInputIdx}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (wasRunning && !isRunning && currentCount > lastResponseCaptureStep) {
|
||||||
|
logToFile(`[RESPONSE-CAPTURE] RUNNING→IDLE, steps=${currentCount}, capturing response...`);
|
||||||
|
lastResponseCaptureStep = currentCount;
|
||||||
|
try {
|
||||||
|
const offset = Math.max(0, currentCount - 5);
|
||||||
|
const latestResp = await sdk.ls.rawRPC('GetCascadeTrajectorySteps', {
|
||||||
|
cascadeId: bestSessionId,
|
||||||
|
stepOffset: offset,
|
||||||
|
verbosity: 1, // CLIENT_TRAJECTORY_VERBOSITY_DEBUG — includes full plannerResponse text
|
||||||
|
});
|
||||||
|
if (latestResp?.steps?.length > 0) {
|
||||||
|
const steps = latestResp.steps;
|
||||||
|
for (let ri = steps.length - 1; ri >= 0; ri--) {
|
||||||
|
const s = steps[ri];
|
||||||
|
const sType = s?.type || '';
|
||||||
|
if (sType.includes('PLANNER_RESPONSE') || sType.includes('MESSAGE')) {
|
||||||
|
let textContent = '';
|
||||||
|
// Extract from plannerResponse field
|
||||||
|
const pr = s?.plannerResponse;
|
||||||
|
if (pr) {
|
||||||
|
// Priority: modifiedResponse (confirmed field from AG)
|
||||||
|
if (pr.modifiedResponse) textContent = pr.modifiedResponse;
|
||||||
|
else if (pr.rawText) textContent = pr.rawText;
|
||||||
|
else if (pr.text) textContent = pr.text;
|
||||||
|
else if (pr.message) textContent = typeof pr.message === 'string' ? pr.message : '';
|
||||||
|
else if (pr.content?.parts) {
|
||||||
|
for (const p of pr.content.parts) {
|
||||||
|
if (p?.text) textContent += p.text;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Log first time to capture actual field names
|
||||||
|
if (!textContent) {
|
||||||
|
logToFile(`[RESPONSE-CAPTURE] plannerResponse keys: [${Object.keys(pr).join(',')}] values(100): ${JSON.stringify(pr).substring(0,200)}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Extract from ephemeralMessage field
|
||||||
|
const em = s?.ephemeralMessage;
|
||||||
|
if (!textContent && em) {
|
||||||
|
if (typeof em === 'string') textContent = em;
|
||||||
|
else if (em.message) textContent = em.message;
|
||||||
|
else if (em.content) textContent = typeof em.content === 'string' ? em.content : JSON.stringify(em.content);
|
||||||
|
}
|
||||||
|
// Fallback: metadata, content, rawOutput
|
||||||
|
if (!textContent) {
|
||||||
|
const parts = s?.content?.parts || s?.parts || [];
|
||||||
|
for (const p of parts) {
|
||||||
|
if (p?.text) textContent += p.text;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!textContent && s?.metadata?.text) textContent = s.metadata.text;
|
||||||
|
if (!textContent && s?.rawOutput) textContent = typeof s.rawOutput === 'string' ? s.rawOutput : JSON.stringify(s.rawOutput);
|
||||||
|
if (textContent.length > 10) {
|
||||||
|
logToFile(`[RESPONSE-CAPTURE] found ${sType} (${textContent.length} chars) at step ${offset + ri}`);
|
||||||
|
const truncated = textContent.length > 1800
|
||||||
|
? textContent.substring(0, 1800) + '\n\n_(이하 생략)_'
|
||||||
|
: textContent;
|
||||||
|
writeChatSnapshot(`💬 **AI 응답**\n\n${truncated}`);
|
||||||
|
break;
|
||||||
|
} else {
|
||||||
|
logToFile(`[RESPONSE-CAPTURE] ${sType} too short (${textContent.length}), keys=[${Object.keys(s).join(',')}]`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (re: any) {
|
||||||
|
logToFile(`[RESPONSE-CAPTURE] error: ${re.message.substring(0, 100)}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
wasRunning = isRunning;
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
if (pollCount <= 5 || pollCount % 20 === 0) {
|
if (pollCount <= 5 || pollCount % 20 === 0) {
|
||||||
console.log(`Gravity Bridge: [POLL#${pollCount}] error: ${e.message}`);
|
console.log(`Gravity Bridge: [POLL#${pollCount}] error: ${e.message}`);
|
||||||
@@ -1726,6 +1913,11 @@ function setupResponseWatcher() {
|
|||||||
|
|
||||||
async function processResponseFile(filePath: string) {
|
async function processResponseFile(filePath: string) {
|
||||||
try {
|
try {
|
||||||
|
// Gracefully handle files already consumed by HTTP handler
|
||||||
|
if (!fs.existsSync(filePath)) {
|
||||||
|
// HTTP GET /response/:rid already served and deleted this file — skip silently
|
||||||
|
return;
|
||||||
|
}
|
||||||
const content = fs.readFileSync(filePath, 'utf-8');
|
const content = fs.readFileSync(filePath, 'utf-8');
|
||||||
const resp = JSON.parse(content);
|
const resp = JSON.parse(content);
|
||||||
const msg = `[RESPONSE] rid=${resp.request_id} approved=${resp.approved}`;
|
const msg = `[RESPONSE] rid=${resp.request_id} approved=${resp.approved}`;
|
||||||
@@ -1737,12 +1929,16 @@ async function processResponseFile(filePath: string) {
|
|||||||
const pendingFile = path.join(pendingDir, `${resp.request_id}.json`);
|
const pendingFile = path.join(pendingDir, `${resp.request_id}.json`);
|
||||||
let sessionId = '';
|
let sessionId = '';
|
||||||
let isDomObserver = false;
|
let isDomObserver = false;
|
||||||
|
let pendingStepType = '';
|
||||||
|
let pendingStepIndex = -1;
|
||||||
if (fs.existsSync(pendingFile)) {
|
if (fs.existsSync(pendingFile)) {
|
||||||
try {
|
try {
|
||||||
const pending = JSON.parse(fs.readFileSync(pendingFile, 'utf-8'));
|
const pending = JSON.parse(fs.readFileSync(pendingFile, 'utf-8'));
|
||||||
sessionId = pending.conversation_id || '';
|
sessionId = pending.conversation_id || '';
|
||||||
isDomObserver = pending.auto_detected === true
|
isDomObserver = pending.auto_detected === true
|
||||||
|| pending.source === 'dom_observer';
|
|| pending.source === 'dom_observer';
|
||||||
|
pendingStepType = pending.step_type || '';
|
||||||
|
pendingStepIndex = pending.step_index ?? lastPendingStepIndex;
|
||||||
} catch { }
|
} catch { }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1757,14 +1953,10 @@ 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 path: approve → trigger renderer click, reject → log only
|
// Step probe path: run ALL approval strategies
|
||||||
if (approved) {
|
logToFile(`[RESPONSE] step_probe → tryApprovalStrategies(${approved}, ${activeSessionId.substring(0, 8)}, type=${pendingStepType}, step=${pendingStepIndex})`);
|
||||||
logToFile(`[RESPONSE] step_probe → approve via trigger-click`);
|
const strategyResult = await tryApprovalStrategies(approved, activeSessionId, pendingStepType, pendingStepIndex);
|
||||||
clickTrigger = { action: 'approve' as const, timestamp: Date.now() };
|
logToFile(`[RESPONSE] strategy result: ${strategyResult}`);
|
||||||
} else {
|
|
||||||
// SAFE: reject logs only — NO ResolveOutstandingSteps (it cancels AI work!)
|
|
||||||
logToFile(`[RESPONSE] step_probe → reject (log only, no destructive action)`);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
logToFile(`[RESPONSE] ${approved ? 'approve' : 'reject'} done (${isDomObserver ? 'dom' : 'step_probe'})`);
|
logToFile(`[RESPONSE] ${approved ? 'approve' : 'reject'} done (${isDomObserver ? 'dom' : 'step_probe'})`);
|
||||||
@@ -1964,80 +2156,269 @@ function writePendingApproval(data: { conversation_id: string; command: string;
|
|||||||
* 2. VS Code accept/reject commands (focus-dependent)
|
* 2. VS Code accept/reject commands (focus-dependent)
|
||||||
* 3. Log failure for manual intervention
|
* 3. Log failure for manual intervention
|
||||||
*/
|
*/
|
||||||
async function tryApprovalStrategies(approved: boolean, sessionId: string): Promise<string> {
|
async function tryApprovalStrategies(approved: boolean, sessionId: string, stepType: string = '', stepIndex: number = -1): Promise<string> {
|
||||||
const action = approved ? 'APPROVE' : 'REJECT';
|
const action = approved ? 'APPROVE' : 'REJECT';
|
||||||
logToFile(`[APPROVAL] Starting ${action} strategies for session ${sessionId.substring(0, 8)}`);
|
const effectiveStepIndex = stepIndex >= 0 ? stepIndex : lastPendingStepIndex;
|
||||||
|
logToFile(`[APPROVAL] Starting ${action} strategies for session ${sessionId.substring(0, 8)} stepType=${stepType} stepIndex=${effectiveStepIndex}`);
|
||||||
|
|
||||||
// ── Dynamic Command Discovery (check if approval commands appear when step is WAITING) ──
|
// ── Dynamic Command Discovery (log what's available during WAITING state) ──
|
||||||
|
let approvalCmdList: string[] = [];
|
||||||
try {
|
try {
|
||||||
const allCmds = await vscode.commands.getCommands(true);
|
const allCmds = await vscode.commands.getCommands(true);
|
||||||
const agCmds = allCmds.filter((c: string) => c.startsWith('antigravity.'));
|
const agCmds = allCmds.filter((c: string) => c.startsWith('antigravity.'));
|
||||||
const approvalCmds = agCmds.filter((c: string) => {
|
approvalCmdList = agCmds.filter((c: string) => {
|
||||||
const lower = c.toLowerCase();
|
const lower = c.toLowerCase();
|
||||||
return lower.includes('accept') || lower.includes('reject') || lower.includes('approve')
|
return lower.includes('accept') || lower.includes('reject') || lower.includes('approve')
|
||||||
|| lower.includes('terminal') || lower.includes('run') || lower.includes('step');
|
|| lower.includes('terminal') || lower.includes('run') || lower.includes('step')
|
||||||
|
|| lower.includes('cascade') || lower.includes('action');
|
||||||
});
|
});
|
||||||
logToFile(`[APPROVAL-CMD-CHECK] ${agCmds.length} total, ${approvalCmds.length} approval-related:`);
|
logToFile(`[APPROVAL-CMD-CHECK] ${agCmds.length} total, ${approvalCmdList.length} approval-related:`);
|
||||||
for (const c of approvalCmds) {
|
for (const c of approvalCmdList) {
|
||||||
logToFile(`[APPROVAL-CMD-CHECK] → ${c}`);
|
logToFile(`[APPROVAL-CMD-CHECK] → ${c}`);
|
||||||
}
|
}
|
||||||
// Dump ALL antigravity.* commands for full comparison
|
|
||||||
logToFile(`[APPROVAL-CMD-CHECK] FULL LIST (${agCmds.length}):`);
|
|
||||||
for (const c of agCmds) {
|
|
||||||
logToFile(`[APPROVAL-CMD-FULL] ${c}`);
|
|
||||||
}
|
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
logToFile(`[APPROVAL-CMD-CHECK] error: ${e.message}`);
|
logToFile(`[APPROVAL-CMD-CHECK] error: ${e.message}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Strategy 1: HandleCascadeUserInteraction RPC ──
|
// ══════════════════════════════════════════════════════════
|
||||||
if (sdk) {
|
// STRATEGY 0-PROTO: Correct proto-based RPC (decoded from AG source)
|
||||||
// Try variant A: { cascadeId, approved }
|
// Routes interaction sub-message by step_type:
|
||||||
try {
|
// run_command → CascadeRunCommandInteraction { confirm }
|
||||||
logToFile(`[APPROVAL-1A] HandleCascadeUserInteraction { cascadeId, approved: ${approved} }`);
|
// write_to_file → AcknowledgeCascadeCodeEdit RPC (separate)
|
||||||
const rpcResult = await sdk.ls.rawRPC('HandleCascadeUserInteraction', {
|
// open_browser_url → CascadeOpenBrowserUrlInteraction { confirm }
|
||||||
cascadeId: sessionId,
|
// send_command_input → CascadeSendCommandInputInteraction { confirm }
|
||||||
approved: approved,
|
// read_url_content → CascadeReadUrlContentInteraction { confirm }
|
||||||
});
|
// mcp_tool → CascadeMcpInteraction { confirm }
|
||||||
logToFile(`[APPROVAL-1A] ✅ SUCCESS: ${JSON.stringify(rpcResult).substring(0, 200)}`);
|
// invoke_subagent → CascadeRunExtensionCodeInteraction { confirm }
|
||||||
return `RPC-1A:HandleCascadeUserInteraction(approved=${approved})`;
|
// ══════════════════════════════════════════════════════════
|
||||||
} catch (e: any) {
|
if (sdk && approved) {
|
||||||
logToFile(`[APPROVAL-1A] ❌ FAIL: ${e.message}`);
|
// Build interaction sub-message based on step_type
|
||||||
|
const typeLower = stepType.toLowerCase().replace('cortex_step_type_', '');
|
||||||
|
let interactionPayload: Record<string, any> = {};
|
||||||
|
|
||||||
|
if (typeLower.includes('write_to_file') || typeLower.includes('propose_code') || typeLower.includes('write_cascade_edit')) {
|
||||||
|
// CODE EDIT: Uses separate AcknowledgeCascadeCodeEdit RPC
|
||||||
|
try {
|
||||||
|
logToFile(`[APPROVAL-PROTO-ACK] AcknowledgeCascadeCodeEdit(cascadeId=${sessionId.substring(0,8)}, accept=true, stepIndices=[${effectiveStepIndex}])`);
|
||||||
|
const ackResult = await sdk.ls.rawRPC('AcknowledgeCascadeCodeEdit', {
|
||||||
|
cascadeId: sessionId,
|
||||||
|
accept: true,
|
||||||
|
stepIndices: [effectiveStepIndex],
|
||||||
|
});
|
||||||
|
logToFile(`[APPROVAL-PROTO-ACK] ✅ SUCCESS: ${JSON.stringify(ackResult).substring(0, 200)}`);
|
||||||
|
return `RPC-PROTO-ACK:AcknowledgeCascadeCodeEdit`;
|
||||||
|
} catch (e: any) {
|
||||||
|
logToFile(`[APPROVAL-PROTO-ACK] ❌ ${e.message.substring(0, 200)}`);
|
||||||
|
// Fall through to HandleCascadeUserInteraction
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try variant B: { cascadeId, stepAction }
|
// Map step_type to interaction sub-message field
|
||||||
try {
|
if (typeLower.includes('run_command') || typeLower.includes('shell_exec')) {
|
||||||
const stepAction = approved ? 'STEP_ACTION_ACCEPT' : 'STEP_ACTION_REJECT';
|
interactionPayload = { runCommand: { confirm: true } };
|
||||||
logToFile(`[APPROVAL-1B] HandleCascadeUserInteraction { cascadeId, stepAction: '${stepAction}' }`);
|
} else if (typeLower.includes('open_browser')) {
|
||||||
const rpcResult = await sdk.ls.rawRPC('HandleCascadeUserInteraction', {
|
interactionPayload = { openBrowserUrl: { confirm: true } };
|
||||||
cascadeId: sessionId,
|
} else if (typeLower.includes('send_command_input')) {
|
||||||
stepAction: stepAction,
|
interactionPayload = { sendCommandInput: { confirm: true } }; // guess field name
|
||||||
});
|
} else if (typeLower.includes('read_url')) {
|
||||||
logToFile(`[APPROVAL-1B] ✅ SUCCESS: ${JSON.stringify(rpcResult).substring(0, 200)}`);
|
interactionPayload = { readUrlContent: { confirm: true } }; // guess
|
||||||
return `RPC-1B:HandleCascadeUserInteraction(stepAction=${stepAction})`;
|
} else if (typeLower.includes('mcp')) {
|
||||||
} catch (e: any) {
|
interactionPayload = { mcpTool: { confirm: true } }; // guess
|
||||||
logToFile(`[APPROVAL-1B] ❌ FAIL: ${e.message}`);
|
} else if (typeLower.includes('invoke_subagent') || typeLower.includes('extension_code')) {
|
||||||
|
interactionPayload = { runExtensionCode: { confirm: true } };
|
||||||
|
} else {
|
||||||
|
// Default: try run_command (most common)
|
||||||
|
interactionPayload = { runCommand: { confirm: true } };
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try variant C: { cascadeId, userAction } (experimental)
|
const protoVariants = [
|
||||||
try {
|
// Variant A: camelCase with trajectoryId (proven working for run_command)
|
||||||
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,
|
cascadeId: sessionId,
|
||||||
userAction: userAction,
|
interaction: {
|
||||||
});
|
trajectoryId: activeTrajectoryId || sessionId,
|
||||||
logToFile(`[APPROVAL-1C] ✅ SUCCESS: ${JSON.stringify(rpcResult).substring(0, 200)}`);
|
stepIndex: effectiveStepIndex,
|
||||||
return `RPC-1C:HandleCascadeUserInteraction(userAction=${userAction})`;
|
...interactionPayload,
|
||||||
} catch (e: any) {
|
},
|
||||||
logToFile(`[APPROVAL-1C] ❌ FAIL: ${e.message}`);
|
},
|
||||||
|
// Variant B: snake_case
|
||||||
|
{
|
||||||
|
cascade_id: sessionId,
|
||||||
|
interaction: {
|
||||||
|
trajectory_id: activeTrajectoryId || sessionId,
|
||||||
|
step_index: effectiveStepIndex,
|
||||||
|
...interactionPayload,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// Variant C: minimal (no trajectoryId)
|
||||||
|
{
|
||||||
|
cascadeId: sessionId,
|
||||||
|
interaction: {
|
||||||
|
stepIndex: effectiveStepIndex,
|
||||||
|
...interactionPayload,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
for (let i = 0; i < protoVariants.length; i++) {
|
||||||
|
try {
|
||||||
|
const payload = protoVariants[i];
|
||||||
|
logToFile(`[APPROVAL-PROTO-${i}] HandleCascadeUserInteraction(${JSON.stringify(payload).substring(0, 250)})`);
|
||||||
|
const rpcResult = await sdk.ls.rawRPC('HandleCascadeUserInteraction', payload);
|
||||||
|
logToFile(`[APPROVAL-PROTO-${i}] ✅ SUCCESS: ${JSON.stringify(rpcResult).substring(0, 200)}`);
|
||||||
|
return `RPC-PROTO-${i}:HandleCascadeUserInteraction(${typeLower})`;
|
||||||
|
} catch (e: any) {
|
||||||
|
logToFile(`[APPROVAL-PROTO-${i}] ❌ ${e.message.substring(0, 300)}`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Strategy 2: Renderer DOM Click via HTTP Bridge ──
|
// ══════════════════════════════════════════════════════════
|
||||||
// 2026-03-09: All SDK approval commands NOT REGISTERED (119 cmds tested).
|
// STRATEGY 0A: executeCascadeAction
|
||||||
// Keyboard simulation sends Enter to chat input (sends empty message — WRONG).
|
// ══════════════════════════════════════════════════════════
|
||||||
// New approach: set clickTrigger flag → renderer polls /trigger-click → clicks button.
|
const cascadeActionVariants = [
|
||||||
|
{ action: approved ? 'accept' : 'reject' },
|
||||||
|
{ action: approved ? 'ACCEPT' : 'REJECT' },
|
||||||
|
{ action: approved ? 'approve' : 'deny' },
|
||||||
|
{ action: approved ? 'run' : 'reject' },
|
||||||
|
{ cascadeId: sessionId, action: approved ? 'accept' : 'reject' },
|
||||||
|
{ cascadeId: sessionId, type: approved ? 'accept' : 'reject' },
|
||||||
|
approved ? 'accept' : 'reject', // plain string arg
|
||||||
|
approved ? 1 : 0, // numeric arg
|
||||||
|
];
|
||||||
|
for (let i = 0; i < cascadeActionVariants.length; i++) {
|
||||||
|
try {
|
||||||
|
const arg = cascadeActionVariants[i];
|
||||||
|
logToFile(`[APPROVAL-0A-${i}] executeCascadeAction(${JSON.stringify(arg)})`);
|
||||||
|
const result = await vscode.commands.executeCommand('antigravity.executeCascadeAction', arg);
|
||||||
|
logToFile(`[APPROVAL-0A-${i}] ✅ SUCCESS: ${JSON.stringify(result).substring(0, 200)}`);
|
||||||
|
return `CMD-0A:executeCascadeAction(variant=${i})`;
|
||||||
|
} catch (e: any) {
|
||||||
|
logToFile(`[APPROVAL-0A-${i}] ❌ ${e.message.substring(0, 100)}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════════════════
|
||||||
|
// STRATEGY 0B: Direct approval commands (brute-force try all 7 SDK commands)
|
||||||
|
// ══════════════════════════════════════════════════════════
|
||||||
|
const commandsToTry = approved
|
||||||
|
? [
|
||||||
|
'antigravity.terminalCommand.run',
|
||||||
|
'antigravity.terminalCommand.accept',
|
||||||
|
'antigravity.agent.acceptAgentStep',
|
||||||
|
'antigravity.command.accept',
|
||||||
|
]
|
||||||
|
: [
|
||||||
|
'antigravity.terminalCommand.reject',
|
||||||
|
'antigravity.agent.rejectAgentStep',
|
||||||
|
'antigravity.command.reject',
|
||||||
|
];
|
||||||
|
for (const cmd of commandsToTry) {
|
||||||
|
// Try with no args, then with sessionId
|
||||||
|
for (const args of [[], [sessionId], [{ cascadeId: sessionId }]]) {
|
||||||
|
try {
|
||||||
|
logToFile(`[APPROVAL-0B] ${cmd}(${JSON.stringify(args)})`);
|
||||||
|
const result = await vscode.commands.executeCommand(cmd, ...args);
|
||||||
|
logToFile(`[APPROVAL-0B] ✅ SUCCESS: ${JSON.stringify(result).substring(0, 200)}`);
|
||||||
|
return `CMD-0B:${cmd}`;
|
||||||
|
} catch (e: any) {
|
||||||
|
logToFile(`[APPROVAL-0B] ❌ ${e.message.substring(0, 80)}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════════════════
|
||||||
|
// STRATEGY 0C: Electron webContents access (pierce webview isolation)
|
||||||
|
// ══════════════════════════════════════════════════════════
|
||||||
|
try {
|
||||||
|
logToFile(`[APPROVAL-0C] Attempting Electron webContents access...`);
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||||
|
const electron = require('electron');
|
||||||
|
const remote = (electron as any).remote;
|
||||||
|
if (remote) {
|
||||||
|
logToFile(`[APPROVAL-0C] electron.remote available!`);
|
||||||
|
const allWC = remote.webContents.getAllWebContents();
|
||||||
|
logToFile(`[APPROVAL-0C] Found ${allWC.length} webContents`);
|
||||||
|
for (const wc of allWC) {
|
||||||
|
const wcUrl = wc.getURL() || '';
|
||||||
|
logToFile(`[APPROVAL-0C] wc: ${wcUrl.substring(0, 100)}`);
|
||||||
|
if (wcUrl.includes('vscode-webview://') || wcUrl.includes('webview')) {
|
||||||
|
const clickScript = approved
|
||||||
|
? `(function(){ var btns = document.querySelectorAll('button'); for(var b of btns){ if(/Run|Accept|Allow/i.test(b.textContent)){ b.click(); return 'clicked:'+b.textContent; } } return 'no-match'; })()`
|
||||||
|
: `(function(){ var btns = document.querySelectorAll('button'); for(var b of btns){ if(/Reject|Deny|Cancel/i.test(b.textContent)){ b.click(); return 'clicked:'+b.textContent; } } return 'no-match'; })()`;
|
||||||
|
const clickResult = await wc.executeJavaScript(clickScript);
|
||||||
|
logToFile(`[APPROVAL-0C] executeJavaScript result: ${clickResult}`);
|
||||||
|
if (clickResult && clickResult.startsWith('clicked:')) {
|
||||||
|
return `ELECTRON-0C:webContents(${clickResult})`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
logToFile(`[APPROVAL-0C] electron.remote NOT available`);
|
||||||
|
// Try without remote (main process context)
|
||||||
|
const wc = (electron as any).webContents;
|
||||||
|
if (wc && typeof wc.getAllWebContents === 'function') {
|
||||||
|
logToFile(`[APPROVAL-0C] Direct electron.webContents available!`);
|
||||||
|
const allWC = wc.getAllWebContents();
|
||||||
|
logToFile(`[APPROVAL-0C] Found ${allWC.length} webContents`);
|
||||||
|
} else {
|
||||||
|
logToFile(`[APPROVAL-0C] No webContents access from Extension Host`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: any) {
|
||||||
|
logToFile(`[APPROVAL-0C] ❌ ${e.message.substring(0, 150)}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════════════════
|
||||||
|
// STRATEGY 0D: sendChatActionMessage (may route to agent)
|
||||||
|
// ══════════════════════════════════════════════════════════
|
||||||
|
const chatActionVariants = [
|
||||||
|
{ action: approved ? 'accept' : 'reject', cascadeId: sessionId },
|
||||||
|
{ message: approved ? 'accept' : 'reject', conversationId: sessionId },
|
||||||
|
approved ? 'accept' : 'reject',
|
||||||
|
];
|
||||||
|
for (let i = 0; i < chatActionVariants.length; i++) {
|
||||||
|
try {
|
||||||
|
logToFile(`[APPROVAL-0D-${i}] sendChatActionMessage(${JSON.stringify(chatActionVariants[i])})`);
|
||||||
|
const result = await vscode.commands.executeCommand('antigravity.sendChatActionMessage', chatActionVariants[i]);
|
||||||
|
logToFile(`[APPROVAL-0D-${i}] ✅ SUCCESS: ${JSON.stringify(result).substring(0, 200)}`);
|
||||||
|
return `CMD-0D:sendChatActionMessage(variant=${i})`;
|
||||||
|
} catch (e: any) {
|
||||||
|
logToFile(`[APPROVAL-0D-${i}] ❌ ${e.message.substring(0, 80)}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════════════════
|
||||||
|
// STRATEGY 1: HandleCascadeUserInteraction RPC (expanded variants)
|
||||||
|
// ══════════════════════════════════════════════════════════
|
||||||
|
if (sdk) {
|
||||||
|
const rpcVariants = [
|
||||||
|
// Original variants
|
||||||
|
{ cascadeId: sessionId, approved: approved },
|
||||||
|
{ cascadeId: sessionId, stepAction: approved ? 'STEP_ACTION_ACCEPT' : 'STEP_ACTION_REJECT' },
|
||||||
|
{ cascadeId: sessionId, userAction: approved ? 'USER_ACTION_APPROVED' : 'USER_ACTION_REJECTED' },
|
||||||
|
// New variants — ConnectRPC protobuf field naming conventions
|
||||||
|
{ cascade_id: sessionId, accepted: approved },
|
||||||
|
{ cascadeId: sessionId, accepted: approved },
|
||||||
|
{ cascade_id: sessionId, user_action: approved ? 1 : 2 }, // enum as int
|
||||||
|
{ cascadeId: sessionId, interaction: approved ? 'ACCEPT' : 'REJECT' },
|
||||||
|
{ cascadeId: sessionId, action: approved ? 'accept' : 'reject' },
|
||||||
|
// With step index from last known waiting step
|
||||||
|
{ cascadeId: sessionId, stepIndex: lastPendingStepIndex, accepted: approved },
|
||||||
|
{ cascadeId: sessionId, stepIndex: lastPendingStepIndex, approved: approved },
|
||||||
|
];
|
||||||
|
for (let i = 0; i < rpcVariants.length; i++) {
|
||||||
|
try {
|
||||||
|
logToFile(`[APPROVAL-1-${i}] HandleCascadeUserInteraction(${JSON.stringify(rpcVariants[i]).substring(0, 120)})`);
|
||||||
|
const rpcResult = await sdk.ls.rawRPC('HandleCascadeUserInteraction', rpcVariants[i]);
|
||||||
|
logToFile(`[APPROVAL-1-${i}] ✅ SUCCESS: ${JSON.stringify(rpcResult).substring(0, 200)}`);
|
||||||
|
return `RPC-1-${i}:HandleCascadeUserInteraction`;
|
||||||
|
} catch (e: any) {
|
||||||
|
logToFile(`[APPROVAL-1-${i}] ❌ ${e.message.substring(0, 80)}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Strategy 2: Renderer DOM Click via HTTP Bridge (kept as fallback) ──
|
||||||
try {
|
try {
|
||||||
const triggerAction = approved ? 'approve' : 'reject';
|
const triggerAction = approved ? 'approve' : 'reject';
|
||||||
logToFile(`[APPROVAL-2] Setting clickTrigger=${triggerAction} for renderer DOM click`);
|
logToFile(`[APPROVAL-2] Setting clickTrigger=${triggerAction} for renderer DOM click`);
|
||||||
@@ -2047,18 +2428,11 @@ async function tryApprovalStrategies(approved: boolean, sessionId: string): Prom
|
|||||||
logToFile(`[APPROVAL-2] ❌ FAIL: ${e.message}`);
|
logToFile(`[APPROVAL-2] ❌ FAIL: ${e.message}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Strategy 3: ResolveOutstandingSteps (REJECT ONLY — this CANCELS!) ──
|
// ── Strategy 3: ResolveOutstandingSteps — DISABLED (too destructive!) ──
|
||||||
|
// This was cancelling AG work when bot sent approved=false.
|
||||||
|
// DO NOT enable without explicit user confirmation.
|
||||||
if (!approved && sdk) {
|
if (!approved && sdk) {
|
||||||
try {
|
logToFile(`[APPROVAL-3] ResolveOutstandingSteps DISABLED — reject only logs, no cancel`);
|
||||||
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`);
|
logToFile(`[APPROVAL] ⚠️ ALL strategies attempted — check logs for results`);
|
||||||
|
|||||||
Reference in New Issue
Block a user