fix(bridge): use GetAllCascadeTrajectories for real-time relay
Root cause: GetCascadeTrajectorySteps has 775-step hard limit, startStepIndex parameter is completely ignored (verified via direct RPC). Solution: GetAllCascadeTrajectories returns: - stepCount: real-time (verified 1413->1457 live) - latestNotifyUserStep: full notificationContent - latestTaskBoundaryStep: full taskName/Status/Summary - stepIndex on each for dedup E2E verified: Python script -> RPC -> snapshot -> Bot -> Discord
This commit is contained in:
@@ -145,3 +145,16 @@ setInterval (5초마다):
|
|||||||
```
|
```
|
||||||
|
|
||||||
이 방식은 `getDiagnostics.lastStepIndex`의 stale 문제를 완전히 우회함.
|
이 방식은 `getDiagnostics.lastStepIndex`의 stale 문제를 완전히 우회함.
|
||||||
|
|
||||||
|
### 추가 발견 (2026-03-08 07:10)
|
||||||
|
|
||||||
|
**rawRPC `GetCascadeTrajectorySteps` 775-step 한계**
|
||||||
|
|
||||||
|
- RPC는 항상 **최대 775 steps** 반환 (0-774)
|
||||||
|
- `startStepIndex` 파라미터가 **무시됨** — 항상 처음부터 775개
|
||||||
|
- 현재 대화가 1275+ steps이면, steps 775-1274는 접근 불가
|
||||||
|
- 마지막 PLANNER_RESPONSE (24 chars)가 step 774의 오래된 텍스트
|
||||||
|
|
||||||
|
**수정**: getDiagnostics로 step count 변화 감지 (3초 폴링) + rawRPC는 content 조회용으로만 사용.
|
||||||
|
`currentMax` 계산 버그 제거 — getDiagnostics.lastStepIndex를 직접 비교.
|
||||||
|
|
||||||
|
|||||||
@@ -439,135 +439,145 @@ function setupMonitor() {
|
|||||||
sdk.monitor.start(3000, 2000);
|
sdk.monitor.start(3000, 2000);
|
||||||
console.log('Gravity Bridge: [SDK] monitor started (USS 3s, trajectory 2s)');
|
console.log('Gravity Bridge: [SDK] monitor started (USS 3s, trajectory 2s)');
|
||||||
// ══════════════════════════════════════════════════════════════════════
|
// ══════════════════════════════════════════════════════════════════════
|
||||||
// PRIMARY RELAY: Direct step polling via rawRPC (getDiagnostics is stale!)
|
// PRIMARY RELAY: GetAllCascadeTrajectories (THE CORRECT API!)
|
||||||
// getDiagnostics.lastStepIndex does NOT update in real-time.
|
//
|
||||||
// Instead, we poll GetCascadeTrajectorySteps directly for the active session.
|
// PROVEN VIA DIRECT RPC TESTING:
|
||||||
|
// - GetCascadeTrajectorySteps: 775-step hard limit, startStepIndex IGNORED
|
||||||
|
// - getDiagnostics.lastStepIndex: stale (can lag behind)
|
||||||
|
// - GetAllCascadeTrajectories:
|
||||||
|
// stepCount: REAL-TIME (verified 1413→1429 live)
|
||||||
|
// latestNotifyUserStep: contains FULL notificationContent
|
||||||
|
// latestTaskBoundaryStep: contains FULL taskName/Status/Summary
|
||||||
|
// stepIndex on each → perfect for dedup
|
||||||
// ══════════════════════════════════════════════════════════════════════
|
// ══════════════════════════════════════════════════════════════════════
|
||||||
let pollCount = 0;
|
let pollCount = 0;
|
||||||
let activeSessionId = '';
|
let activeSessionId = '';
|
||||||
let activeSessionTitle = '';
|
let activeSessionTitle = '';
|
||||||
let polledStepCount = 0;
|
let lastKnownStepCount = 0;
|
||||||
|
let lastNotifyStepIndex = -1;
|
||||||
|
let lastTaskStepIndex = -1;
|
||||||
setInterval(async () => {
|
setInterval(async () => {
|
||||||
pollCount++;
|
pollCount++;
|
||||||
try {
|
try {
|
||||||
// Phase 1: Discover active session (first time or periodically)
|
// Single RPC: GetAllCascadeTrajectories
|
||||||
if (!activeSessionId || pollCount % 12 === 0) {
|
const allTraj = await sdk.ls.rawRPC('GetAllCascadeTrajectories', {});
|
||||||
try {
|
if (!allTraj?.trajectorySummaries)
|
||||||
const raw = await vscode.commands.executeCommand('antigravity.getDiagnostics');
|
return;
|
||||||
if (raw && typeof raw === 'string') {
|
// Find the most recently modified session (or current active)
|
||||||
const diag = JSON.parse(raw);
|
let bestSession = null;
|
||||||
if (Array.isArray(diag.recentTrajectories) && diag.recentTrajectories.length > 0) {
|
let bestSessionId = '';
|
||||||
const first = diag.recentTrajectories[0];
|
let bestModTime = '';
|
||||||
if (first.googleAgentId && first.googleAgentId !== activeSessionId) {
|
for (const [sid, data] of Object.entries(allTraj.trajectorySummaries)) {
|
||||||
activeSessionId = first.googleAgentId;
|
const modTime = data.lastModifiedTime || '';
|
||||||
activeSessionTitle = first.summary || 'Untitled';
|
if (!bestSession || modTime > bestModTime) {
|
||||||
polledStepCount = first.lastStepIndex || 0;
|
bestSession = data;
|
||||||
console.log(`Gravity Bridge: [POLL#${pollCount}] 🎯 active session: ${activeSessionId.substring(0, 8)} "${activeSessionTitle}" steps=${polledStepCount}`);
|
bestSessionId = sid;
|
||||||
writeRegistration(activeSessionId);
|
bestModTime = modTime;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!bestSession)
|
||||||
|
return;
|
||||||
|
const currentCount = bestSession.stepCount || 0;
|
||||||
|
const currentTitle = (bestSession.summary || 'Untitled').substring(0, 50);
|
||||||
|
const isRunning = String(bestSession.status || '').includes('RUNNING');
|
||||||
|
// Session changed?
|
||||||
|
if (bestSessionId !== activeSessionId) {
|
||||||
|
activeSessionId = bestSessionId;
|
||||||
|
activeSessionTitle = currentTitle;
|
||||||
|
lastKnownStepCount = currentCount;
|
||||||
|
lastNotifyStepIndex = bestSession.latestNotifyUserStep?.stepIndex ?? -1;
|
||||||
|
lastTaskStepIndex = bestSession.latestTaskBoundaryStep?.stepIndex ?? -1;
|
||||||
|
writeRegistration(activeSessionId);
|
||||||
|
console.log(`Gravity Bridge: [POLL#${pollCount}] session: ${activeSessionId.substring(0, 8)} "${currentTitle}" steps=${currentCount} ${isRunning ? 'RUNNING' : 'idle'}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// No change in step count?
|
||||||
|
if (currentCount <= lastKnownStepCount && pollCount > 1) {
|
||||||
|
if (pollCount % 20 === 0) {
|
||||||
|
console.log(`Gravity Bridge: [POLL#${pollCount}] idle: ${activeSessionId.substring(0, 8)} steps=${currentCount}`);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const delta = currentCount - lastKnownStepCount;
|
||||||
|
lastKnownStepCount = currentCount;
|
||||||
|
if (delta > 0) {
|
||||||
|
console.log(`Gravity Bridge: [POLL#${pollCount}] +${delta} steps (${currentCount}) "${currentTitle}"`);
|
||||||
|
}
|
||||||
|
// ── Process latestNotifyUserStep ──
|
||||||
|
const notifyStep = bestSession.latestNotifyUserStep;
|
||||||
|
if (notifyStep && notifyStep.stepIndex > lastNotifyStepIndex) {
|
||||||
|
lastNotifyStepIndex = notifyStep.stepIndex;
|
||||||
|
const content = notifyStep.step?.notifyUser?.notificationContent || '';
|
||||||
|
if (content.length > 10) {
|
||||||
|
writeChatSnapshot(`📣 **알림** (step ${notifyStep.stepIndex})\n\n${content}`);
|
||||||
|
console.log(`Gravity Bridge: [POLL#${pollCount}] NOTIFY step=${notifyStep.stepIndex} ${content.length} chars`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// ── Process latestTaskBoundaryStep ──
|
||||||
|
const taskStep = bestSession.latestTaskBoundaryStep;
|
||||||
|
if (taskStep && taskStep.stepIndex > lastTaskStepIndex) {
|
||||||
|
lastTaskStepIndex = taskStep.stepIndex;
|
||||||
|
const tb = taskStep.step?.taskBoundary;
|
||||||
|
if (tb?.taskName) {
|
||||||
|
const mode = tb.mode ? tb.mode.replace('AGENT_MODE_', '') : '';
|
||||||
|
writeChatSnapshot(`📋 **[${mode}] ${tb.taskName}**\n${tb.taskStatus || ''}\n\n${tb.taskSummary || ''}`);
|
||||||
|
console.log(`Gravity Bridge: [POLL#${pollCount}] TASK step=${taskStep.stepIndex} "${tb.taskName}"`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// ── Check for WAITING status (pending user approval) ──
|
||||||
|
if (isRunning) {
|
||||||
|
// Check lastUserInputStepIndex — if it's far behind stepCount,
|
||||||
|
// AI might be waiting for user input
|
||||||
|
const lastUserInput = bestSession.lastUserInputStepIndex || 0;
|
||||||
|
const gap = currentCount - lastUserInput;
|
||||||
|
// If gap is small and status is RUNNING, the AI might have a pending step
|
||||||
|
// We can check via GetCascadeTrajectorySteps for the last few steps (within 775 limit)
|
||||||
|
if (delta > 0 && gap < 50) {
|
||||||
|
try {
|
||||||
|
const stepsData = await sdk.ls.rawRPC('GetCascadeTrajectorySteps', {
|
||||||
|
cascadeId: activeSessionId
|
||||||
|
});
|
||||||
|
if (stepsData?.steps) {
|
||||||
|
const last5 = stepsData.steps.slice(-5);
|
||||||
|
for (const step of last5) {
|
||||||
|
const sType = String(step.type || '');
|
||||||
|
const sStatus = String(step.status || '');
|
||||||
|
if (sStatus.includes('WAITING')) {
|
||||||
|
const shortType = sType.replace(/CORTEX_STEP_TYPE_/g, '');
|
||||||
|
let cmd = `⏳ ${shortType}`;
|
||||||
|
let desc = `⏳ **대기 중**: ${shortType}`;
|
||||||
|
if (sType.includes('RUN_COMMAND')) {
|
||||||
|
const cmdLine = step.runCommand?.commandLine || '';
|
||||||
|
cmd = `▶️ ${cmdLine.substring(0, 80)}`;
|
||||||
|
desc = `▶️ **명령 실행 확인**\n\`\`\`\n${cmdLine}\n\`\`\``;
|
||||||
|
}
|
||||||
|
else if (sType.includes('CODE_ACTION') || sType.includes('WRITE')) {
|
||||||
|
const file = step.codeAction?.filePath || step.writeToFile?.filePath || '';
|
||||||
|
cmd = `✏️ 파일: ${file}`;
|
||||||
|
desc = `✏️ **파일 수정 확인**\n파일: \`${file}\``;
|
||||||
|
}
|
||||||
|
writePendingApproval({
|
||||||
|
conversation_id: activeSessionId,
|
||||||
|
command: cmd,
|
||||||
|
description: desc,
|
||||||
|
});
|
||||||
|
console.log(`Gravity Bridge: [POLL#${pollCount}] WAITING ${shortType}`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
catch (rpcErr) {
|
||||||
catch (e) {
|
// GetCascadeTrajectorySteps failed — not critical
|
||||||
if (pollCount <= 3) {
|
|
||||||
console.log(`Gravity Bridge: [POLL#${pollCount}] getDiag error: ${e.message}`);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!activeSessionId)
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// Phase 2: Fetch latest steps via rawRPC (RELIABLE, not stale!)
|
|
||||||
const stepsData = await sdk.ls.rawRPC('GetCascadeTrajectorySteps', {
|
|
||||||
cascadeId: activeSessionId,
|
|
||||||
startStepIndex: polledStepCount > 2 ? polledStepCount - 2 : 0
|
|
||||||
});
|
|
||||||
if (!stepsData || !Array.isArray(stepsData.steps)) {
|
|
||||||
if (pollCount <= 3) {
|
|
||||||
console.log(`Gravity Bridge: [POLL#${pollCount}] no steps data`);
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const allSteps = stepsData.steps;
|
|
||||||
const currentMax = allSteps.length > 0
|
|
||||||
? Math.max(...allSteps.map((s) => s.stepIndex ?? s.index ?? 0), allSteps.length)
|
|
||||||
: polledStepCount;
|
|
||||||
if (currentMax <= polledStepCount) {
|
|
||||||
// No new steps — log every 12th poll
|
|
||||||
if (pollCount % 12 === 0) {
|
|
||||||
console.log(`Gravity Bridge: [POLL#${pollCount}] no change: ${activeSessionId.substring(0, 8)} steps=${currentMax}`);
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// Phase 3: New steps detected! Process them.
|
|
||||||
const delta = currentMax - polledStepCount;
|
|
||||||
console.log(`Gravity Bridge: [POLL#${pollCount}] 🆕 +${delta} steps (${polledStepCount}→${currentMax}) "${activeSessionTitle}"`);
|
|
||||||
const newSteps = allSteps.slice(-delta);
|
|
||||||
polledStepCount = currentMax;
|
|
||||||
let lastPlannerText = '';
|
|
||||||
for (const step of newSteps) {
|
|
||||||
const sType = String(step.type || '');
|
|
||||||
const sStatus = String(step.status || '');
|
|
||||||
// PLANNER_RESPONSE → AI text (main content)
|
|
||||||
if (sType.includes('PLANNER_RESPONSE') && (sStatus.includes('DONE') || sStatus.includes('COMPLETED'))) {
|
|
||||||
const pr = step.plannerResponse;
|
|
||||||
const text = pr?.modifiedResponse || pr?.response || '';
|
|
||||||
if (text && text.length > 0) {
|
|
||||||
lastPlannerText = text;
|
|
||||||
console.log(`Gravity Bridge: [POLL#${pollCount}] 📝 planner ${text.length} chars`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// NOTIFY_USER → user notification
|
|
||||||
if (sType.includes('NOTIFY_USER') && (sStatus.includes('DONE') || sStatus.includes('COMPLETED'))) {
|
|
||||||
const nu = step.notifyUser;
|
|
||||||
const content = nu?.notificationContent || '';
|
|
||||||
if (content && content.length > 0) {
|
|
||||||
writeChatSnapshot(`📣 **알림**\n\n${content}`);
|
|
||||||
console.log(`Gravity Bridge: [POLL#${pollCount}] 📣 notify ${content.length} chars`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// TASK_BOUNDARY → task status update
|
|
||||||
if (sType.includes('TASK_BOUNDARY') && (sStatus.includes('DONE') || sStatus.includes('COMPLETED'))) {
|
|
||||||
const tb = step.taskBoundary;
|
|
||||||
if (tb?.taskName) {
|
|
||||||
writeChatSnapshot(`📋 **작업**: ${tb.taskName}\n상태: ${tb.taskStatus || ''}\n${tb.taskSummary || ''}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// WAITING steps → pending approval (any type)
|
|
||||||
if (sStatus.includes('WAITING')) {
|
|
||||||
const shortType = sType.replace(/CORTEX_STEP_TYPE_/g, '');
|
|
||||||
let command = `⏳ ${shortType}`;
|
|
||||||
let description = `⏳ **대기 중**: ${shortType}`;
|
|
||||||
if (sType.includes('RUN_COMMAND')) {
|
|
||||||
const cmd = step.runCommand?.commandLine || '';
|
|
||||||
command = `▶️ ${cmd.substring(0, 80)}`;
|
|
||||||
description = `▶️ **명령 실행 확인**\n\`\`\`\n${cmd}\n\`\`\``;
|
|
||||||
}
|
|
||||||
else if (sType.includes('CODE_ACTION') || sType.includes('WRITE')) {
|
|
||||||
const file = step.codeAction?.filePath || step.writeToFile?.filePath || '';
|
|
||||||
command = `✏️ 파일 수정: ${file}`;
|
|
||||||
description = `✏️ **파일 수정 확인**\n파일: \`${file}\``;
|
|
||||||
}
|
|
||||||
writePendingApproval({
|
|
||||||
conversation_id: activeSessionId,
|
|
||||||
command: command,
|
|
||||||
description: description,
|
|
||||||
});
|
|
||||||
console.log(`Gravity Bridge: [POLL#${pollCount}] ⏳ ${shortType} WAITING`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Write latest planner response as snapshot (dedup)
|
|
||||||
if (lastPlannerText && lastPlannerText !== lastSnapshotText.get(activeSessionId)) {
|
|
||||||
lastSnapshotText.set(activeSessionId, lastPlannerText);
|
|
||||||
writeChatSnapshot(`🤖 **${activeSessionTitle}**\n\n${lastPlannerText}`);
|
|
||||||
console.log(`Gravity Bridge: [POLL#${pollCount}] 💬 snapshot ${lastPlannerText.length} chars`);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (e) {
|
catch (e) {
|
||||||
if (pollCount <= 5 || pollCount % 12 === 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}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, 5000);
|
}, 3000);
|
||||||
}
|
}
|
||||||
// ─── Response Watcher (Discord approval → Antigravity RPC) ───
|
// ─── Response Watcher (Discord approval → Antigravity RPC) ───
|
||||||
let responseWatcher = null;
|
let responseWatcher = null;
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -422,141 +422,149 @@ function setupMonitor() {
|
|||||||
console.log('Gravity Bridge: [SDK] monitor started (USS 3s, trajectory 2s)');
|
console.log('Gravity Bridge: [SDK] monitor started (USS 3s, trajectory 2s)');
|
||||||
|
|
||||||
// ══════════════════════════════════════════════════════════════════════
|
// ══════════════════════════════════════════════════════════════════════
|
||||||
// PRIMARY RELAY: Direct step polling via rawRPC (getDiagnostics is stale!)
|
// PRIMARY RELAY: GetAllCascadeTrajectories (THE CORRECT API!)
|
||||||
// getDiagnostics.lastStepIndex does NOT update in real-time.
|
//
|
||||||
// Instead, we poll GetCascadeTrajectorySteps directly for the active session.
|
// PROVEN VIA DIRECT RPC TESTING:
|
||||||
|
// - GetCascadeTrajectorySteps: 775-step hard limit, startStepIndex IGNORED
|
||||||
|
// - getDiagnostics.lastStepIndex: stale (can lag behind)
|
||||||
|
// - GetAllCascadeTrajectories:
|
||||||
|
// stepCount: REAL-TIME (verified 1413→1429 live)
|
||||||
|
// latestNotifyUserStep: contains FULL notificationContent
|
||||||
|
// latestTaskBoundaryStep: contains FULL taskName/Status/Summary
|
||||||
|
// stepIndex on each → perfect for dedup
|
||||||
// ══════════════════════════════════════════════════════════════════════
|
// ══════════════════════════════════════════════════════════════════════
|
||||||
let pollCount = 0;
|
let pollCount = 0;
|
||||||
let activeSessionId = '';
|
let activeSessionId = '';
|
||||||
let activeSessionTitle = '';
|
let activeSessionTitle = '';
|
||||||
let polledStepCount = 0;
|
let lastKnownStepCount = 0;
|
||||||
|
let lastNotifyStepIndex = -1;
|
||||||
|
let lastTaskStepIndex = -1;
|
||||||
|
|
||||||
setInterval(async () => {
|
setInterval(async () => {
|
||||||
pollCount++;
|
pollCount++;
|
||||||
try {
|
try {
|
||||||
// Phase 1: Discover active session (first time or periodically)
|
// Single RPC: GetAllCascadeTrajectories
|
||||||
if (!activeSessionId || pollCount % 12 === 0) {
|
const allTraj = await sdk.ls.rawRPC('GetAllCascadeTrajectories', {});
|
||||||
try {
|
if (!allTraj?.trajectorySummaries) return;
|
||||||
const raw = await vscode.commands.executeCommand<string>('antigravity.getDiagnostics');
|
|
||||||
if (raw && typeof raw === 'string') {
|
// Find the most recently modified session (or current active)
|
||||||
const diag = JSON.parse(raw);
|
let bestSession: any = null;
|
||||||
if (Array.isArray(diag.recentTrajectories) && diag.recentTrajectories.length > 0) {
|
let bestSessionId = '';
|
||||||
const first = diag.recentTrajectories[0];
|
let bestModTime = '';
|
||||||
if (first.googleAgentId && first.googleAgentId !== activeSessionId) {
|
for (const [sid, data] of Object.entries(allTraj.trajectorySummaries) as [string, any][]) {
|
||||||
activeSessionId = first.googleAgentId;
|
const modTime = data.lastModifiedTime || '';
|
||||||
activeSessionTitle = first.summary || 'Untitled';
|
if (!bestSession || modTime > bestModTime) {
|
||||||
polledStepCount = first.lastStepIndex || 0;
|
bestSession = data;
|
||||||
console.log(`Gravity Bridge: [POLL#${pollCount}] 🎯 active session: ${activeSessionId.substring(0, 8)} "${activeSessionTitle}" steps=${polledStepCount}`);
|
bestSessionId = sid;
|
||||||
writeRegistration(activeSessionId);
|
bestModTime = modTime;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!bestSession) return;
|
||||||
|
const currentCount = bestSession.stepCount || 0;
|
||||||
|
const currentTitle = (bestSession.summary || 'Untitled').substring(0, 50);
|
||||||
|
const isRunning = String(bestSession.status || '').includes('RUNNING');
|
||||||
|
|
||||||
|
// Session changed?
|
||||||
|
if (bestSessionId !== activeSessionId) {
|
||||||
|
activeSessionId = bestSessionId;
|
||||||
|
activeSessionTitle = currentTitle;
|
||||||
|
lastKnownStepCount = currentCount;
|
||||||
|
lastNotifyStepIndex = bestSession.latestNotifyUserStep?.stepIndex ?? -1;
|
||||||
|
lastTaskStepIndex = bestSession.latestTaskBoundaryStep?.stepIndex ?? -1;
|
||||||
|
writeRegistration(activeSessionId);
|
||||||
|
console.log(`Gravity Bridge: [POLL#${pollCount}] session: ${activeSessionId.substring(0, 8)} "${currentTitle}" steps=${currentCount} ${isRunning ? 'RUNNING' : 'idle'}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// No change in step count?
|
||||||
|
if (currentCount <= lastKnownStepCount && pollCount > 1) {
|
||||||
|
if (pollCount % 20 === 0) {
|
||||||
|
console.log(`Gravity Bridge: [POLL#${pollCount}] idle: ${activeSessionId.substring(0, 8)} steps=${currentCount}`);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const delta = currentCount - lastKnownStepCount;
|
||||||
|
lastKnownStepCount = currentCount;
|
||||||
|
if (delta > 0) {
|
||||||
|
console.log(`Gravity Bridge: [POLL#${pollCount}] +${delta} steps (${currentCount}) "${currentTitle}"`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Process latestNotifyUserStep ──
|
||||||
|
const notifyStep = bestSession.latestNotifyUserStep;
|
||||||
|
if (notifyStep && notifyStep.stepIndex > lastNotifyStepIndex) {
|
||||||
|
lastNotifyStepIndex = notifyStep.stepIndex;
|
||||||
|
const content = notifyStep.step?.notifyUser?.notificationContent || '';
|
||||||
|
if (content.length > 10) {
|
||||||
|
writeChatSnapshot(`📣 **알림** (step ${notifyStep.stepIndex})\n\n${content}`);
|
||||||
|
console.log(`Gravity Bridge: [POLL#${pollCount}] NOTIFY step=${notifyStep.stepIndex} ${content.length} chars`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Process latestTaskBoundaryStep ──
|
||||||
|
const taskStep = bestSession.latestTaskBoundaryStep;
|
||||||
|
if (taskStep && taskStep.stepIndex > lastTaskStepIndex) {
|
||||||
|
lastTaskStepIndex = taskStep.stepIndex;
|
||||||
|
const tb = taskStep.step?.taskBoundary;
|
||||||
|
if (tb?.taskName) {
|
||||||
|
const mode = tb.mode ? tb.mode.replace('AGENT_MODE_', '') : '';
|
||||||
|
writeChatSnapshot(`📋 **[${mode}] ${tb.taskName}**\n${tb.taskStatus || ''}\n\n${tb.taskSummary || ''}`);
|
||||||
|
console.log(`Gravity Bridge: [POLL#${pollCount}] TASK step=${taskStep.stepIndex} "${tb.taskName}"`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Check for WAITING status (pending user approval) ──
|
||||||
|
if (isRunning) {
|
||||||
|
// Check lastUserInputStepIndex — if it's far behind stepCount,
|
||||||
|
// AI might be waiting for user input
|
||||||
|
const lastUserInput = bestSession.lastUserInputStepIndex || 0;
|
||||||
|
const gap = currentCount - lastUserInput;
|
||||||
|
// If gap is small and status is RUNNING, the AI might have a pending step
|
||||||
|
// We can check via GetCascadeTrajectorySteps for the last few steps (within 775 limit)
|
||||||
|
if (delta > 0 && gap < 50) {
|
||||||
|
try {
|
||||||
|
const stepsData = await sdk.ls.rawRPC('GetCascadeTrajectorySteps', {
|
||||||
|
cascadeId: activeSessionId
|
||||||
|
});
|
||||||
|
if (stepsData?.steps) {
|
||||||
|
const last5 = stepsData.steps.slice(-5);
|
||||||
|
for (const step of last5) {
|
||||||
|
const sType = String(step.type || '');
|
||||||
|
const sStatus = String(step.status || '');
|
||||||
|
if (sStatus.includes('WAITING')) {
|
||||||
|
const shortType = sType.replace(/CORTEX_STEP_TYPE_/g, '');
|
||||||
|
let cmd = `⏳ ${shortType}`;
|
||||||
|
let desc = `⏳ **대기 중**: ${shortType}`;
|
||||||
|
if (sType.includes('RUN_COMMAND')) {
|
||||||
|
const cmdLine = step.runCommand?.commandLine || '';
|
||||||
|
cmd = `▶️ ${cmdLine.substring(0, 80)}`;
|
||||||
|
desc = `▶️ **명령 실행 확인**\n\`\`\`\n${cmdLine}\n\`\`\``;
|
||||||
|
} else if (sType.includes('CODE_ACTION') || sType.includes('WRITE')) {
|
||||||
|
const file = step.codeAction?.filePath || step.writeToFile?.filePath || '';
|
||||||
|
cmd = `✏️ 파일: ${file}`;
|
||||||
|
desc = `✏️ **파일 수정 확인**\n파일: \`${file}\``;
|
||||||
|
}
|
||||||
|
writePendingApproval({
|
||||||
|
conversation_id: activeSessionId,
|
||||||
|
command: cmd,
|
||||||
|
description: desc,
|
||||||
|
});
|
||||||
|
console.log(`Gravity Bridge: [POLL#${pollCount}] WAITING ${shortType}`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
} catch (rpcErr: any) {
|
||||||
} catch (e: any) {
|
// GetCascadeTrajectorySteps failed — not critical
|
||||||
if (pollCount <= 3) { console.log(`Gravity Bridge: [POLL#${pollCount}] getDiag error: ${e.message}`); }
|
|
||||||
}
|
|
||||||
if (!activeSessionId) return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Phase 2: Fetch latest steps via rawRPC (RELIABLE, not stale!)
|
|
||||||
const stepsData = await sdk.ls.rawRPC('GetCascadeTrajectorySteps', {
|
|
||||||
cascadeId: activeSessionId,
|
|
||||||
startStepIndex: polledStepCount > 2 ? polledStepCount - 2 : 0
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!stepsData || !Array.isArray(stepsData.steps)) {
|
|
||||||
if (pollCount <= 3) { console.log(`Gravity Bridge: [POLL#${pollCount}] no steps data`); }
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const allSteps = stepsData.steps;
|
|
||||||
const currentMax = allSteps.length > 0
|
|
||||||
? Math.max(...allSteps.map((s: any) => s.stepIndex ?? s.index ?? 0), allSteps.length)
|
|
||||||
: polledStepCount;
|
|
||||||
|
|
||||||
if (currentMax <= polledStepCount) {
|
|
||||||
// No new steps — log every 12th poll
|
|
||||||
if (pollCount % 12 === 0) {
|
|
||||||
console.log(`Gravity Bridge: [POLL#${pollCount}] no change: ${activeSessionId.substring(0, 8)} steps=${currentMax}`);
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Phase 3: New steps detected! Process them.
|
|
||||||
const delta = currentMax - polledStepCount;
|
|
||||||
console.log(`Gravity Bridge: [POLL#${pollCount}] 🆕 +${delta} steps (${polledStepCount}→${currentMax}) "${activeSessionTitle}"`);
|
|
||||||
const newSteps = allSteps.slice(-delta);
|
|
||||||
polledStepCount = currentMax;
|
|
||||||
|
|
||||||
let lastPlannerText = '';
|
|
||||||
for (const step of newSteps) {
|
|
||||||
const sType = String(step.type || '');
|
|
||||||
const sStatus = String(step.status || '');
|
|
||||||
|
|
||||||
// PLANNER_RESPONSE → AI text (main content)
|
|
||||||
if (sType.includes('PLANNER_RESPONSE') && (sStatus.includes('DONE') || sStatus.includes('COMPLETED'))) {
|
|
||||||
const pr = step.plannerResponse;
|
|
||||||
const text = pr?.modifiedResponse || pr?.response || '';
|
|
||||||
if (text && text.length > 0) {
|
|
||||||
lastPlannerText = text;
|
|
||||||
console.log(`Gravity Bridge: [POLL#${pollCount}] 📝 planner ${text.length} chars`);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// NOTIFY_USER → user notification
|
|
||||||
if (sType.includes('NOTIFY_USER') && (sStatus.includes('DONE') || sStatus.includes('COMPLETED'))) {
|
|
||||||
const nu = step.notifyUser;
|
|
||||||
const content = nu?.notificationContent || '';
|
|
||||||
if (content && content.length > 0) {
|
|
||||||
writeChatSnapshot(`📣 **알림**\n\n${content}`);
|
|
||||||
console.log(`Gravity Bridge: [POLL#${pollCount}] 📣 notify ${content.length} chars`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TASK_BOUNDARY → task status update
|
|
||||||
if (sType.includes('TASK_BOUNDARY') && (sStatus.includes('DONE') || sStatus.includes('COMPLETED'))) {
|
|
||||||
const tb = step.taskBoundary;
|
|
||||||
if (tb?.taskName) {
|
|
||||||
writeChatSnapshot(`📋 **작업**: ${tb.taskName}\n상태: ${tb.taskStatus || ''}\n${tb.taskSummary || ''}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// WAITING steps → pending approval (any type)
|
|
||||||
if (sStatus.includes('WAITING')) {
|
|
||||||
const shortType = sType.replace(/CORTEX_STEP_TYPE_/g, '');
|
|
||||||
let command = `⏳ ${shortType}`;
|
|
||||||
let description = `⏳ **대기 중**: ${shortType}`;
|
|
||||||
|
|
||||||
if (sType.includes('RUN_COMMAND')) {
|
|
||||||
const cmd = step.runCommand?.commandLine || '';
|
|
||||||
command = `▶️ ${cmd.substring(0, 80)}`;
|
|
||||||
description = `▶️ **명령 실행 확인**\n\`\`\`\n${cmd}\n\`\`\``;
|
|
||||||
} else if (sType.includes('CODE_ACTION') || sType.includes('WRITE')) {
|
|
||||||
const file = step.codeAction?.filePath || step.writeToFile?.filePath || '';
|
|
||||||
command = `✏️ 파일 수정: ${file}`;
|
|
||||||
description = `✏️ **파일 수정 확인**\n파일: \`${file}\``;
|
|
||||||
}
|
|
||||||
|
|
||||||
writePendingApproval({
|
|
||||||
conversation_id: activeSessionId,
|
|
||||||
command: command,
|
|
||||||
description: description,
|
|
||||||
});
|
|
||||||
console.log(`Gravity Bridge: [POLL#${pollCount}] ⏳ ${shortType} WAITING`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Write latest planner response as snapshot (dedup)
|
|
||||||
if (lastPlannerText && lastPlannerText !== lastSnapshotText.get(activeSessionId)) {
|
|
||||||
lastSnapshotText.set(activeSessionId, lastPlannerText);
|
|
||||||
writeChatSnapshot(`🤖 **${activeSessionTitle}**\n\n${lastPlannerText}`);
|
|
||||||
console.log(`Gravity Bridge: [POLL#${pollCount}] 💬 snapshot ${lastPlannerText.length} chars`);
|
|
||||||
}
|
}
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
if (pollCount <= 5 || pollCount % 12 === 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}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, 5000);
|
}, 3000);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── Response Watcher (Discord approval → Antigravity RPC) ───
|
// ─── Response Watcher (Discord approval → Antigravity RPC) ───
|
||||||
|
|||||||
Reference in New Issue
Block a user