fix(bridge): align Extension protocol with Bot — 3 mismatches fixed

- Snapshot: response/chat_snapshot.txt → chat_snapshots/*.json
- Command field: cmd.message → cmd.text (matches Bot.write_command)
- RPC: GetConversation (404) → GetCascadeTrajectorySteps
- Bundle sql-wasm.js + sql-wasm.wasm into VSIX (45KB→379KB)
- Handle consumed flag, clean 38 stale commands
- Add extractAIText helper with fallback chain
This commit is contained in:
2026-03-08 01:13:28 +09:00
parent 4bb72921ae
commit e4dc1b171f
6 changed files with 297 additions and 90 deletions

View File

@@ -0,0 +1,5 @@
# 2026-03-08 Devlog — Bridge 프로토콜 수정
| # | 시간 | 작업 | 커밋 | 상태 |
|---|------|------|------|------|
| 1 | 01:00 | Extension↔Bot 프로토콜 불일치 3건 수정 + sql-wasm 번들링 | 커밋예정 | 🔧 |

View File

@@ -0,0 +1,26 @@
# Extension↔Bot 프로토콜 불일치 수정 + sql-wasm 번들링
- **시간**: 2026-03-08 00:50~01:10
- **Commit**: 커밋예정
- **Vikunja**: #240 → 진행중
## 결정 사항
### GetConversation → GetCascadeTrajectorySteps
- SDK의 `getConversation()` 메서드가 `GetConversation` RPC를 호출하지만, LS에 해당 메서드 없음 (404)
- `ls-rpc-reference.md`에서 확인: 실제 메서드는 `GetCascadeTrajectorySteps`
- Fallback 체인: Steps → GetCascadeTrajectory → title-only
### Bot↔Extension 프로토콜 불일치 3건
1. **Snapshot**: Bot은 `chat_snapshots/*.json` (JSON), Extension은 `response/chat_snapshot.txt` (텍스트)
2. **Command**: Bot은 `text` 필드, Extension은 `message` 필드 읽음
3. **Consumed**: Bot이 `consumed: true` 설정하지만 Extension이 삭제하지 않아 38개 누적
### sql-wasm.wasm VSIX 누락
- `.vscodeignore``node_modules/**` 제외 → sql.js wasm 파일 미포함
- 해결: 빌드 시 `out/sdk/`에 수동 복사
## 미완료
- [ ] 윈도우 리로드 후 E2E 테스트 (Discord→Antigravity, Antigravity→Discord)
- [ ] `GetCascadeTrajectorySteps` 응답 구조 파악 → extractAIText 정확도 검증
- [ ] VSIX 빌드 스크립트에 sql-wasm 복사 자동화

Binary file not shown.

View File

@@ -87,7 +87,7 @@ function detectProjectName() {
}
// ─── Bridge File I/O ───
function ensureBridgeDir() {
const dirs = ['', 'response', 'commands'];
const dirs = ['', 'response', 'commands', 'chat_snapshots'];
for (const d of dirs) {
const p = path.join(bridgePath, d);
if (!fs.existsSync(p)) {
@@ -97,9 +97,21 @@ function ensureBridgeDir() {
}
function writeChatSnapshot(text) {
try {
const filePath = path.join(bridgePath, 'response', 'chat_snapshot.txt');
fs.writeFileSync(filePath, text, 'utf-8');
console.log(`Gravity Bridge: chat snapshot written (${text.length} chars)`);
// Write to chat_snapshots/*.json for Bot's chat_snapshot_scanner to pick up
const snapshotDir = path.join(bridgePath, 'chat_snapshots');
if (!fs.existsSync(snapshotDir)) {
fs.mkdirSync(snapshotDir, { recursive: true });
}
const id = Date.now().toString();
const data = {
id,
project_name: projectName,
content: text,
timestamp: Date.now() / 1000,
};
const filePath = path.join(snapshotDir, `${id}.json`);
fs.writeFileSync(filePath, JSON.stringify(data, null, 2), 'utf-8');
console.log(`Gravity Bridge: chat snapshot written (${text.length} chars) → ${id}.json`);
}
catch (e) {
console.log(`Gravity Bridge: snapshot write error: ${e.message}`);
@@ -117,31 +129,46 @@ function processCommandFile(filePath) {
try {
const content = fs.readFileSync(filePath, 'utf-8');
const cmd = JSON.parse(content);
// Ignore commands for other projects
if (cmd.project_name && cmd.project_name !== projectName) {
console.log(`Gravity Bridge: skipping command for "${cmd.project_name}"`);
// Skip already consumed commands
if (cmd.consumed) {
try {
fs.unlinkSync(filePath);
}
catch { }
return;
}
console.log(`Gravity Bridge: command — "${cmd.message || cmd.action}"`);
if (cmd.action === 'approve' && sdk) {
// Ignore commands for other projects
if (cmd.project_name && cmd.project_name !== projectName) {
console.log(`Gravity Bridge: skipping command for "${cmd.project_name}" (we are "${projectName}")`);
return;
}
// Bot writes 'text' field, not 'message'
const text = cmd.text || cmd.message || '';
const action = cmd.action || '';
console.log(`Gravity Bridge: command — text="${text}" action="${action}"`);
if (action === 'approve' && sdk) {
sdk.cascade.acceptStep().catch((e) => console.log(`Gravity Bridge: approve error: ${e.message}`));
}
else if (cmd.action === 'reject' && sdk) {
else if (action === 'reject' && sdk) {
sdk.cascade.rejectStep().catch((e) => console.log(`Gravity Bridge: reject error: ${e.message}`));
}
else if (cmd.action === 'approve_terminal' && sdk) {
else if (action === 'approve_terminal' && sdk) {
sdk.cascade.acceptTerminalCommand().catch((e) => console.log(`Gravity Bridge: approve_terminal error: ${e.message}`));
}
else if (cmd.message && sdk) {
// Send message to Antigravity
sdk.cascade.sendPrompt(cmd.message).then(() => {
console.log(`Gravity Bridge: ✅ sent via SDK sendPrompt`);
}).catch((e) => {
// Fallback to VS Code command
console.log(`Gravity Bridge: SDK sendPrompt failed: ${e.message}, trying command...`);
vscode.commands.executeCommand('antigravity.sendPromptToAgentPanel', cmd.message)
.then(() => console.log('Gravity Bridge: ✅ sent via sendPromptToAgentPanel'));
});
else if (text === '!stop') {
// Cancel current operation
vscode.commands.executeCommand('antigravity.agent.rejectAgentStep')
.then(() => console.log('Gravity Bridge: ✅ stop sent'), () => { });
}
else if (text.startsWith('!auto ')) {
// Auto-approve mode toggle
const mode = text.includes('on') ? 'true' : 'false';
console.log(`Gravity Bridge: auto-approve → ${mode}`);
}
else if (text) {
// Send message to Antigravity — use VS Code command (most reliable)
vscode.commands.executeCommand('antigravity.sendPromptToAgentPanel', text)
.then(() => console.log(`Gravity Bridge: ✅ sent "${text.substring(0, 50)}" via sendPromptToAgentPanel`), (e) => console.log(`Gravity Bridge: sendPrompt failed: ${e.message}`));
}
// Remove processed command file
try {
@@ -198,42 +225,59 @@ async function initSDK(context) {
return false;
}
}
// Track last seen step per session to avoid re-fetching
const lastSeenStep = new Map();
function setupMonitor() {
if (!sdk) {
return;
}
// Step count changed → fetch conversation content
// Step count changed → fetch new steps via GetCascadeTrajectorySteps
sdk.monitor.onStepCountChanged(async (e) => {
console.log(`Gravity Bridge: [SDK] step changed: "${e.title}" step ${e.newCount} (+${e.delta})`);
// Get fresh session to have the ID
try {
const conversation = await sdk.ls.getConversation(e.sessionId);
if (conversation && conversation.messages) {
// Find the last assistant message
const assistantMsgs = conversation.messages.filter((m) => m.role === 'assistant' && m.content);
if (assistantMsgs.length > 0) {
const lastMsg = assistantMsgs[assistantMsgs.length - 1];
const text = `🤖 **${e.title}**\n\n${lastMsg.content}`;
// Use the correct LS RPC: GetCascadeTrajectorySteps (not GetConversation which doesn't exist)
const fromStep = lastSeenStep.get(e.sessionId) ?? Math.max(0, e.newCount - e.delta);
const stepsData = await sdk.ls.rawRPC('GetCascadeTrajectorySteps', {
cascadeId: e.sessionId,
startStepIndex: fromStep
});
lastSeenStep.set(e.sessionId, e.newCount);
if (stepsData) {
// Try to extract AI text from the steps response
const aiText = extractAIText(stepsData);
if (aiText) {
const text = `🤖 **${e.title}**\n\n${aiText}`;
writeChatSnapshot(text);
console.log(`Gravity Bridge: [SDK] relayed AI response (${lastMsg.content.length} chars)`);
return;
}
}
// Find PlannerResponse steps
if (conversation && conversation.steps) {
const responses = conversation.steps.filter((s) => s.type === 'PlannerResponse' && s.summary);
if (responses.length > 0) {
const last = responses[responses.length - 1];
writeChatSnapshot(`🤖 **${e.title}**\n\n${last.summary}`);
console.log(`Gravity Bridge: [SDK] relayed PlannerResponse (${last.summary.length} chars)`);
console.log(`Gravity Bridge: [SDK] relayed AI response (${aiText.length} chars)`);
return;
}
// Log the raw structure for debugging
console.log(`Gravity Bridge: [SDK] steps data keys: ${JSON.stringify(Object.keys(stepsData))}`);
}
}
catch (err) {
console.log(`Gravity Bridge: [SDK] getConversation error: ${err.message}`);
console.log(`Gravity Bridge: [SDK] GetCascadeTrajectorySteps error: ${err.message}`);
// Fallback: try GetCascadeTrajectory (full trajectory, heavier)
try {
const fullTraj = await sdk.ls.rawRPC('GetCascadeTrajectory', {
cascadeId: e.sessionId
});
if (fullTraj) {
const aiText = extractAIText(fullTraj);
if (aiText) {
writeChatSnapshot(`🤖 **${e.title}**\n\n${aiText}`);
console.log(`Gravity Bridge: [SDK] relayed via GetCascadeTrajectory (${aiText.length} chars)`);
return;
}
console.log(`Gravity Bridge: [SDK] trajectory keys: ${JSON.stringify(Object.keys(fullTraj))}`);
}
}
catch (err2) {
console.log(`Gravity Bridge: [SDK] GetCascadeTrajectory also failed: ${err2.message}`);
}
}
// Fallback: just send the title + step info
lastSeenStep.set(e.sessionId, e.newCount);
writeChatSnapshot(`🤖 **${e.title}**\n\n(step ${e.newCount}, +${e.delta})`);
});
// New conversation started
@@ -252,6 +296,50 @@ function setupMonitor() {
sdk.monitor.start(3000, 2000);
console.log('Gravity Bridge: [SDK] monitor started (USS 3s, trajectory 2s)');
}
/**
* Extract AI response text from LS RPC step/trajectory data.
* The exact structure depends on the protobuf schema — we try multiple paths.
*/
function extractAIText(data) {
if (!data) {
return null;
}
// Try common protobuf response patterns
// Pattern 1: steps array with content
const steps = data.steps || data.trajectorySteps || data.cascadeSteps;
if (Array.isArray(steps) && steps.length > 0) {
// Find the last step with AI content
for (let i = steps.length - 1; i >= 0; i--) {
const step = steps[i];
// PlannerResponse / assistant content
const content = step.content || step.text || step.summary ||
step.plannerResponse || step.assistantMessage ||
step.response?.content || step.response?.text;
if (typeof content === 'string' && content.length > 10) {
return content;
}
}
}
// Pattern 2: messages array
const messages = data.messages || data.chatMessages;
if (Array.isArray(messages) && messages.length > 0) {
for (let i = messages.length - 1; i >= 0; i--) {
const msg = messages[i];
if ((msg.role === 'assistant' || msg.type === 'assistant') && msg.content) {
return msg.content;
}
}
}
// Pattern 3: nested trajectory object
if (data.trajectory) {
return extractAIText(data.trajectory);
}
// Pattern 4: single step response
if (data.content && typeof data.content === 'string') {
return data.content;
}
return null;
}
// ─── Activation ───
async function activate(context) {
console.log('Gravity Bridge: activating...');

File diff suppressed because one or more lines are too long

View File

@@ -56,7 +56,7 @@ function detectProjectName(): string {
// ─── Bridge File I/O ───
function ensureBridgeDir() {
const dirs = ['', 'response', 'commands'];
const dirs = ['', 'response', 'commands', 'chat_snapshots'];
for (const d of dirs) {
const p = path.join(bridgePath, d);
if (!fs.existsSync(p)) { fs.mkdirSync(p, { recursive: true }); }
@@ -65,9 +65,19 @@ function ensureBridgeDir() {
function writeChatSnapshot(text: string) {
try {
const filePath = path.join(bridgePath, 'response', 'chat_snapshot.txt');
fs.writeFileSync(filePath, text, 'utf-8');
console.log(`Gravity Bridge: chat snapshot written (${text.length} chars)`);
// Write to chat_snapshots/*.json for Bot's chat_snapshot_scanner to pick up
const snapshotDir = path.join(bridgePath, 'chat_snapshots');
if (!fs.existsSync(snapshotDir)) { fs.mkdirSync(snapshotDir, { recursive: true }); }
const id = Date.now().toString();
const data = {
id,
project_name: projectName,
content: text,
timestamp: Date.now() / 1000,
};
const filePath = path.join(snapshotDir, `${id}.json`);
fs.writeFileSync(filePath, JSON.stringify(data, null, 2), 'utf-8');
console.log(`Gravity Bridge: chat snapshot written (${text.length} chars) → ${id}.json`);
} catch (e: any) {
console.log(`Gravity Bridge: snapshot write error: ${e.message}`);
}
@@ -87,36 +97,50 @@ function processCommandFile(filePath: string) {
const content = fs.readFileSync(filePath, 'utf-8');
const cmd = JSON.parse(content);
// Ignore commands for other projects
if (cmd.project_name && cmd.project_name !== projectName) {
console.log(`Gravity Bridge: skipping command for "${cmd.project_name}"`);
// Skip already consumed commands
if (cmd.consumed) {
try { fs.unlinkSync(filePath); } catch { }
return;
}
console.log(`Gravity Bridge: command — "${cmd.message || cmd.action}"`);
// Ignore commands for other projects
if (cmd.project_name && cmd.project_name !== projectName) {
console.log(`Gravity Bridge: skipping command for "${cmd.project_name}" (we are "${projectName}")`);
return;
}
if (cmd.action === 'approve' && sdk) {
// Bot writes 'text' field, not 'message'
const text = cmd.text || cmd.message || '';
const action = cmd.action || '';
console.log(`Gravity Bridge: command — text="${text}" action="${action}"`);
if (action === 'approve' && sdk) {
sdk.cascade.acceptStep().catch((e: any) =>
console.log(`Gravity Bridge: approve error: ${e.message}`)
);
} else if (cmd.action === 'reject' && sdk) {
} else if (action === 'reject' && sdk) {
sdk.cascade.rejectStep().catch((e: any) =>
console.log(`Gravity Bridge: reject error: ${e.message}`)
);
} else if (cmd.action === 'approve_terminal' && sdk) {
} else if (action === 'approve_terminal' && sdk) {
sdk.cascade.acceptTerminalCommand().catch((e: any) =>
console.log(`Gravity Bridge: approve_terminal error: ${e.message}`)
);
} else if (cmd.message && sdk) {
// Send message to Antigravity
sdk.cascade.sendPrompt(cmd.message).then(() => {
console.log(`Gravity Bridge: ✅ sent via SDK sendPrompt`);
}).catch((e: any) => {
// Fallback to VS Code command
console.log(`Gravity Bridge: SDK sendPrompt failed: ${e.message}, trying command...`);
vscode.commands.executeCommand('antigravity.sendPromptToAgentPanel', cmd.message)
.then(() => console.log('Gravity Bridge: ✅ sent via sendPromptToAgentPanel'));
});
} else if (text === '!stop') {
// Cancel current operation
vscode.commands.executeCommand('antigravity.agent.rejectAgentStep')
.then(() => console.log('Gravity Bridge: ✅ stop sent'),
() => { });
} else if (text.startsWith('!auto ')) {
// Auto-approve mode toggle
const mode = text.includes('on') ? 'true' : 'false';
console.log(`Gravity Bridge: auto-approve → ${mode}`);
} else if (text) {
// Send message to Antigravity — use VS Code command (most reliable)
vscode.commands.executeCommand('antigravity.sendPromptToAgentPanel', text)
.then(() => console.log(`Gravity Bridge: ✅ sent "${text.substring(0, 50)}" via sendPromptToAgentPanel`),
(e: any) => console.log(`Gravity Bridge: sendPrompt failed: ${e.message}`));
}
// Remove processed command file
@@ -173,47 +197,63 @@ async function initSDK(context: vscode.ExtensionContext): Promise<boolean> {
}
}
// Track last seen step per session to avoid re-fetching
const lastSeenStep = new Map<string, number>();
function setupMonitor() {
if (!sdk) { return; }
// Step count changed → fetch conversation content
// Step count changed → fetch new steps via GetCascadeTrajectorySteps
sdk.monitor.onStepCountChanged(async (e: any) => {
console.log(`Gravity Bridge: [SDK] step changed: "${e.title}" step ${e.newCount} (+${e.delta})`);
// Get fresh session to have the ID
try {
const conversation = await sdk.ls.getConversation(e.sessionId);
if (conversation && conversation.messages) {
// Find the last assistant message
const assistantMsgs = conversation.messages.filter(
(m: any) => m.role === 'assistant' && m.content
);
if (assistantMsgs.length > 0) {
const lastMsg = assistantMsgs[assistantMsgs.length - 1];
const text = `🤖 **${e.title}**\n\n${lastMsg.content}`;
writeChatSnapshot(text);
console.log(`Gravity Bridge: [SDK] relayed AI response (${lastMsg.content.length} chars)`);
return;
}
}
// Use the correct LS RPC: GetCascadeTrajectorySteps (not GetConversation which doesn't exist)
const fromStep = lastSeenStep.get(e.sessionId) ?? Math.max(0, e.newCount - e.delta);
// Find PlannerResponse steps
if (conversation && conversation.steps) {
const responses = conversation.steps.filter(
(s: any) => s.type === 'PlannerResponse' && s.summary
);
if (responses.length > 0) {
const last = responses[responses.length - 1];
writeChatSnapshot(`🤖 **${e.title}**\n\n${last.summary}`);
console.log(`Gravity Bridge: [SDK] relayed PlannerResponse (${last.summary.length} chars)`);
const stepsData = await sdk.ls.rawRPC('GetCascadeTrajectorySteps', {
cascadeId: e.sessionId,
startStepIndex: fromStep
});
lastSeenStep.set(e.sessionId, e.newCount);
if (stepsData) {
// Try to extract AI text from the steps response
const aiText = extractAIText(stepsData);
if (aiText) {
const text = `🤖 **${e.title}**\n\n${aiText}`;
writeChatSnapshot(text);
console.log(`Gravity Bridge: [SDK] relayed AI response (${aiText.length} chars)`);
return;
}
// Log the raw structure for debugging
console.log(`Gravity Bridge: [SDK] steps data keys: ${JSON.stringify(Object.keys(stepsData))}`);
}
} catch (err: any) {
console.log(`Gravity Bridge: [SDK] getConversation error: ${err.message}`);
console.log(`Gravity Bridge: [SDK] GetCascadeTrajectorySteps error: ${err.message}`);
// Fallback: try GetCascadeTrajectory (full trajectory, heavier)
try {
const fullTraj = await sdk.ls.rawRPC('GetCascadeTrajectory', {
cascadeId: e.sessionId
});
if (fullTraj) {
const aiText = extractAIText(fullTraj);
if (aiText) {
writeChatSnapshot(`🤖 **${e.title}**\n\n${aiText}`);
console.log(`Gravity Bridge: [SDK] relayed via GetCascadeTrajectory (${aiText.length} chars)`);
return;
}
console.log(`Gravity Bridge: [SDK] trajectory keys: ${JSON.stringify(Object.keys(fullTraj))}`);
}
} catch (err2: any) {
console.log(`Gravity Bridge: [SDK] GetCascadeTrajectory also failed: ${err2.message}`);
}
}
// Fallback: just send the title + step info
lastSeenStep.set(e.sessionId, e.newCount);
writeChatSnapshot(`🤖 **${e.title}**\n\n(step ${e.newCount}, +${e.delta})`);
});
@@ -237,6 +277,54 @@ function setupMonitor() {
console.log('Gravity Bridge: [SDK] monitor started (USS 3s, trajectory 2s)');
}
/**
* Extract AI response text from LS RPC step/trajectory data.
* The exact structure depends on the protobuf schema — we try multiple paths.
*/
function extractAIText(data: any): string | null {
if (!data) { return null; }
// Try common protobuf response patterns
// Pattern 1: steps array with content
const steps = data.steps || data.trajectorySteps || data.cascadeSteps;
if (Array.isArray(steps) && steps.length > 0) {
// Find the last step with AI content
for (let i = steps.length - 1; i >= 0; i--) {
const step = steps[i];
// PlannerResponse / assistant content
const content = step.content || step.text || step.summary ||
step.plannerResponse || step.assistantMessage ||
step.response?.content || step.response?.text;
if (typeof content === 'string' && content.length > 10) {
return content;
}
}
}
// Pattern 2: messages array
const messages = data.messages || data.chatMessages;
if (Array.isArray(messages) && messages.length > 0) {
for (let i = messages.length - 1; i >= 0; i--) {
const msg = messages[i];
if ((msg.role === 'assistant' || msg.type === 'assistant') && msg.content) {
return msg.content;
}
}
}
// Pattern 3: nested trajectory object
if (data.trajectory) {
return extractAIText(data.trajectory);
}
// Pattern 4: single step response
if (data.content && typeof data.content === 'string') {
return data.content;
}
return null;
}
// ─── Activation ───
export async function activate(context: vscode.ExtensionContext) {