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:
@@ -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) ───
|
||||
|
||||
Reference in New Issue
Block a user