fix(bridge): rawRPC direct polling + SDK analysis docs + trial-and-error log

- Root cause: getDiagnostics.lastStepIndex is stale, SDK EventMonitor cannot detect real-time step changes
- Fix: Direct rawRPC('GetCascadeTrajectorySteps') polling every 5s
- Relay: PLANNER_RESPONSE, NOTIFY_USER, TASK_BOUNDARY, WAITING steps
- Added: docs/discord-bridge-analysis.md (full SDK architecture analysis)
- Added: docs/devlog/entries/20260308-003.md (trial-and-error history)
- Added: antigravity-sdk-main/ source reference
- Vikunja: #252 done, #253 created, #251 commented
This commit is contained in:
2026-03-08 07:08:25 +09:00
parent 731dad35bf
commit c3964f8e7a
40 changed files with 11086 additions and 25 deletions

View File

@@ -193,7 +193,30 @@ async function initSDK(context: vscode.ExtensionContext): Promise<boolean> {
// Track last seen step per session to avoid re-fetching
const lastSeenStep = new Map<string, number>();
const lastSnapshotText = new Map<string, string>(); // dedup: last written text per session
const lastSnapshotText = new Map<string, string>();
const registeredSessions = new Set<string>(); // track which sessions have been registered
/**
* Write a registration file for the Bot to discover session → project mapping.
* Called automatically on first step event per session.
*/
function writeRegistration(sessionId: string) {
if (registeredSessions.has(sessionId)) { return; }
registeredSessions.add(sessionId);
try {
const regDir = path.join(bridgePath, 'register');
if (!fs.existsSync(regDir)) { fs.mkdirSync(regDir, { recursive: true }); }
const data = {
conversation_id: sessionId,
project_name: projectName,
timestamp: Date.now() / 1000,
};
fs.writeFileSync(path.join(regDir, `${sessionId}.json`), JSON.stringify(data, null, 2), 'utf-8');
console.log(`Gravity Bridge: registered session ${sessionId.substring(0, 8)}${projectName}`);
} catch (e: any) {
console.log(`Gravity Bridge: registration write error: ${e.message}`);
}
}
function setupMonitor() {
if (!sdk) { return; }
@@ -202,6 +225,50 @@ function setupMonitor() {
sdk.monitor.onStepCountChanged(async (e: any) => {
console.log(`Gravity Bridge: [SDK] step changed: "${e.title}" step ${e.newCount} (+${e.delta})`);
// Auto-register session with Bot on first step event
writeRegistration(e.sessionId);
// ── ONE-TIME FULL STEP TYPE DUMP ──
if (!lastSeenStep.has(e.sessionId)) {
try {
const fullData = await sdk.ls.rawRPC('GetCascadeTrajectorySteps', {
cascadeId: e.sessionId
});
if (fullData && Array.isArray(fullData.steps)) {
const typeCounts = new Map<string, { count: number, statuses: Set<string>, keys: Set<string>, sample: any }>();
for (const step of fullData.steps) {
const t = (step.type || '').replace('CORTEX_STEP_TYPE_', '');
const s = (step.status || '').replace('CORTEX_STEP_STATUS_', '');
const dataKeys = Object.keys(step).filter(k => !['type', 'status', 'metadata'].includes(k));
if (!typeCounts.has(t)) {
typeCounts.set(t, { count: 0, statuses: new Set(), keys: new Set(), sample: step });
}
const entry = typeCounts.get(t)!;
entry.count++;
entry.statuses.add(s);
for (const k of dataKeys) { entry.keys.add(k); }
}
console.log(`Gravity Bridge: ══════════════════════════════════════`);
console.log(`Gravity Bridge: FULL STEP TYPE MAP (${fullData.steps.length} total steps)`);
console.log(`Gravity Bridge: ══════════════════════════════════════`);
for (const [type, info] of typeCounts.entries()) {
console.log(`Gravity Bridge: [TYPE] ${type} ×${info.count} statuses=[${[...info.statuses].join(',')}] keys=[${[...info.keys].join(',')}]`);
// Dump ONE sample of each type
const s = info.sample;
const dataKeys = Object.keys(s).filter(k => !['type', 'status', 'metadata'].includes(k));
for (const k of dataKeys) {
const v = s[k];
const vStr = typeof v === 'object' ? JSON.stringify(v).substring(0, 150) : String(v).substring(0, 150);
console.log(`Gravity Bridge: .${k} (${typeof v}): ${vStr}`);
}
}
console.log(`Gravity Bridge: ══════════════════════════════════════`);
}
} catch (e: any) {
console.log(`Gravity Bridge: full dump error: ${e.message}`);
}
}
try {
// IMPORTANT: Only fetch NEW steps, never re-fetch history
const fromStep = Math.max(
@@ -223,34 +290,98 @@ function setupMonitor() {
for (const step of newSteps) {
const sType = step.type || '';
const sStatus = step.status || '';
const shortType = sType.replace('CORTEX_STEP_TYPE_', '');
const shortStatus = sStatus.replace('CORTEX_STEP_STATUS_', '');
// ── DIAGNOSTIC: log ALL step types (minimal) ──
console.log(`Gravity Bridge: [STEP] ${shortType} ${shortStatus}`);
// ══════════════════════════════════════════════
// ANY WAITING step → Pending Approval to Discord
// ══════════════════════════════════════════════
if (sStatus.includes('WAITING')) {
let description = '';
let command = '';
if (sType.includes('RUN_COMMAND')) {
const rc = step.runCommand || {};
command = rc.commandLine || rc.proposedCommandLine || '';
description = `💻 **명령 실행 요청**\n\`\`\`\n${command}\n\`\`\`\ncwd: ${rc.cwd || ''}`;
} else if (sType.includes('EDIT_FILE') || sType.includes('CODE_EDIT') || sType.includes('CODE_ACTION') || sType.includes('WRITE_FILE') || sType.includes('FILE_EDIT')) {
// File edit/write/code action
const edit = step.codeEdit || step.editFile || step.writeFile || step.codeAction || {};
const filePath = edit.filePath || edit.targetFile || edit.path || '';
const desc = edit.description || edit.instruction || '';
command = `📝 파일 수정: ${filePath}`;
description = `📝 **파일 변경 확인**\n파일: \`${filePath}\`\n${desc ? `설명: ${desc}` : ''}`;
// Full dump for diagnostic
console.log(`Gravity Bridge: [STEP-DETAIL] ${shortType} WAITING keys=${JSON.stringify(Object.keys(step).filter(k => !['type', 'status', 'metadata'].includes(k)))}`);
for (const k of Object.keys(step).filter(k => !['type', 'status', 'metadata'].includes(k))) {
const v = step[k];
console.log(`Gravity Bridge: [STEP-DETAIL] .${k} = ${typeof v === 'object' ? JSON.stringify(v).substring(0, 200) : String(v).substring(0, 200)}`);
}
} else if (sType.includes('FILE_ACCESS') || sType.includes('READ_FILE') || sType.includes('FILE_READ')) {
// File access permission
const fa = step.fileAccess || step.readFile || step.fileRead || {};
const filePath = fa.filePath || fa.path || '';
command = `📖 파일 접근: ${filePath}`;
description = `📖 **파일 접근 권한 요청**\n파일: \`${filePath}\``;
console.log(`Gravity Bridge: [STEP-DETAIL] ${shortType} WAITING keys=${JSON.stringify(Object.keys(step).filter(k => !['type', 'status', 'metadata'].includes(k)))}`);
for (const k of Object.keys(step).filter(k => !['type', 'status', 'metadata'].includes(k))) {
const v = step[k];
console.log(`Gravity Bridge: [STEP-DETAIL] .${k} = ${typeof v === 'object' ? JSON.stringify(v).substring(0, 200) : String(v).substring(0, 200)}`);
}
} else {
// Unknown WAITING step — still relay it with full diagnostic
const stepKeys = Object.keys(step).filter(k => !['type', 'status', 'metadata'].includes(k));
command = `${shortType}`;
description = `⏳ **대기 중: ${shortType}**\nkeys: ${stepKeys.join(', ')}`;
console.log(`Gravity Bridge: [STEP-DETAIL] UNKNOWN WAITING: ${shortType} keys=${JSON.stringify(stepKeys)}`);
for (const k of stepKeys) {
const v = step[k];
console.log(`Gravity Bridge: [STEP-DETAIL] .${k} = ${typeof v === 'object' ? JSON.stringify(v).substring(0, 200) : String(v).substring(0, 200)}`);
}
}
// ── RUN_COMMAND + WAITING → Pending Approval ──
if (sType.includes('RUN_COMMAND') && sStatus.includes('WAITING')) {
const rc = step.runCommand || {};
const cmdLine = rc.commandLine || rc.proposedCommandLine || '';
writePendingApproval({
conversation_id: e.sessionId,
command: cmdLine,
description: `💻 ${e.title}\n\`\`\`\n${cmdLine}\n\`\`\`\ncwd: ${rc.cwd || ''}`,
command: command,
description: `${description}\n\n🏷️ ${e.title}`,
});
console.log(`Gravity Bridge: [SDK] ⏳ pending: "${cmdLine.substring(0, 100)}"`);
console.log(`Gravity Bridge: [SDK] ⏳ pending ${shortType}: "${command.substring(0, 80)}"`);
continue;
}
// ── PLANNER_RESPONSE → collect AI text (COMPLETED/DONE only) ──
// ══════════════════════════════════════════════
// PLANNER_RESPONSE → AI text relay (COMPLETED/DONE)
// ══════════════════════════════════════════════
if (sType.includes('PLANNER_RESPONSE')) {
if (!sStatus.includes('COMPLETED') && !sStatus.includes('DONE')) {
continue;
}
const pr = step.plannerResponse;
// Use confirmed field: plannerResponse.response or .modifiedResponse
const responseText = pr?.modifiedResponse || pr?.response || '';
if (responseText && typeof responseText === 'string' && responseText.length > 0) {
lastPlannerText = responseText; // Overwrite — last one wins
lastPlannerText = responseText;
console.log(`Gravity Bridge: [SDK] 📝 planner response found (${responseText.length} chars)`);
}
continue;
}
// ══════════════════════════════════════════════
// NOTIFY_USER → also relay as chat snapshot
// ══════════════════════════════════════════════
if (sType.includes('NOTIFY_USER') && (sStatus.includes('COMPLETED') || sStatus.includes('DONE'))) {
const nu = step.notifyUser;
const content = nu?.notificationContent || '';
if (content && content.length > 0) {
// Write NOTIFY_USER as snapshot too
writeChatSnapshot(`📣 **알림**\n\n${content}`);
console.log(`Gravity Bridge: [SDK] 📣 NOTIFY_USER relayed (${content.length} chars)`);
}
continue;
}
}
// Write the LAST planner response as snapshot (with dedup)
@@ -271,12 +402,14 @@ function setupMonitor() {
// New conversation started
sdk.monitor.onNewConversation((e: any) => {
console.log(`Gravity Bridge: [SDK] new conversation: ${e.title}`);
writeRegistration(e.sessionId || e.id || '');
writeChatSnapshot(`🚀 **${e.title}** — 새 대화 시작`);
});
// Active session changed
sdk.monitor.onActiveSessionChanged((e: any) => {
console.log(`Gravity Bridge: [SDK] active session: "${e.title}" (${e.sessionId?.substring(0, 8)})`);
writeRegistration(e.sessionId || e.id || '');
});
// State changed (USS update)
@@ -287,6 +420,143 @@ function setupMonitor() {
// Start monitoring (USS every 3s, trajectory every 2s for faster detection)
sdk.monitor.start(3000, 2000);
console.log('Gravity Bridge: [SDK] monitor started (USS 3s, trajectory 2s)');
// ══════════════════════════════════════════════════════════════════════
// PRIMARY RELAY: Direct step polling via rawRPC (getDiagnostics is stale!)
// getDiagnostics.lastStepIndex does NOT update in real-time.
// Instead, we poll GetCascadeTrajectorySteps directly for the active session.
// ══════════════════════════════════════════════════════════════════════
let pollCount = 0;
let activeSessionId = '';
let activeSessionTitle = '';
let polledStepCount = 0;
setInterval(async () => {
pollCount++;
try {
// Phase 1: Discover active session (first time or periodically)
if (!activeSessionId || pollCount % 12 === 0) {
try {
const raw = await vscode.commands.executeCommand<string>('antigravity.getDiagnostics');
if (raw && typeof raw === 'string') {
const diag = JSON.parse(raw);
if (Array.isArray(diag.recentTrajectories) && diag.recentTrajectories.length > 0) {
const first = diag.recentTrajectories[0];
if (first.googleAgentId && first.googleAgentId !== activeSessionId) {
activeSessionId = first.googleAgentId;
activeSessionTitle = first.summary || 'Untitled';
polledStepCount = first.lastStepIndex || 0;
console.log(`Gravity Bridge: [POLL#${pollCount}] 🎯 active session: ${activeSessionId.substring(0, 8)} "${activeSessionTitle}" steps=${polledStepCount}`);
writeRegistration(activeSessionId);
}
}
}
} catch (e: any) {
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) {
if (pollCount <= 5 || pollCount % 12 === 0) {
console.log(`Gravity Bridge: [POLL#${pollCount}] error: ${e.message}`);
}
}
}, 5000);
}
// ─── Response Watcher (Discord approval → Antigravity RPC) ───