From 112b8bf182ccd19d19d3c418efc8674f295fbfe9 Mon Sep 17 00:00:00 2001 From: mehmet turac Date: Sun, 31 May 2026 10:34:48 +0300 Subject: [PATCH] fix: recover ACP parser after bracket logs --- apps/daemon/src/acp.ts | 17 ++++++++++++++++- apps/daemon/tests/acp.test.ts | 27 +++++++++++++++++++++++++++ 2 files changed, 43 insertions(+), 1 deletion(-) diff --git a/apps/daemon/src/acp.ts b/apps/daemon/src/acp.ts index 03146cf98..e376e4ef2 100644 --- a/apps/daemon/src/acp.ts +++ b/apps/daemon/src/acp.ts @@ -289,6 +289,7 @@ function currentModelFromSessionResult(result: JsonObject): string | null { export function createJsonLineStream(onMessage: (message: unknown, rawLine: string) => void) { let buffer = ''; let pendingJson = ''; + let pendingJsonLineCount = 0; const emit = (candidate: string): boolean => { try { @@ -301,18 +302,32 @@ export function createJsonLineStream(onMessage: (message: unknown, rawLine: stri const startPendingJson = (line: string) => { pendingJson = line; + pendingJsonLineCount = 1; }; const handleLine = (line: string) => { const trimmed = line.trim(); if (!trimmed) return; if (pendingJson) { + if ((trimmed.startsWith('{') || trimmed.startsWith('[')) && emit(trimmed)) { + pendingJson = ''; + pendingJsonLineCount = 0; + return; + } const nextCandidate = `${pendingJson}\n${trimmed}`; if (emit(nextCandidate)) { pendingJson = ''; + pendingJsonLineCount = 0; return; } - pendingJson = nextCandidate.length <= 128_000 ? nextCandidate : ''; + pendingJsonLineCount += 1; + if (nextCandidate.length <= 128_000 && pendingJsonLineCount <= 256) { + pendingJson = nextCandidate; + return; + } + pendingJson = ''; + pendingJsonLineCount = 0; + handleLine(trimmed); return; } if (emit(trimmed)) return; diff --git a/apps/daemon/tests/acp.test.ts b/apps/daemon/tests/acp.test.ts index 2352742f6..435fb9fbd 100644 --- a/apps/daemon/tests/acp.test.ts +++ b/apps/daemon/tests/acp.test.ts @@ -408,6 +408,33 @@ test('attachAcpSession accepts pretty-printed ACP startup responses', () => { assert.equal(events.some((entry) => entry.event === 'error'), false); }); +test('attachAcpSession recovers when bracket-prefixed logs precede JSON frames', () => { + const child = new FakeAcpChild(); + const writes: string[] = []; + const events: Array<{ event: string; payload: unknown }> = []; + child.stdin.on('data', (chunk) => writes.push(String(chunk))); + + attachAcpSession({ + child: child as never, + prompt: 'hello', + cwd: '/tmp/od-project', + model: null, + mcpServers: [], + send: (event, payload) => events.push({ event, payload }), + }); + + child.stdout.write('[vela] starting OpenCode bridge\n'); + child.stdout.write(`${JSON.stringify({ id: 1, result: {} })}\n`); + child.stdout.write('{not json but looks like an object log\n'); + child.stdout.write(`${JSON.stringify({ id: 2, result: { sessionId: 'session-1' } })}\n`); + + const methods = parseRpcWrites(writes) + .map((entry) => entry.method) + .filter(Boolean); + assert.deepEqual(methods, ['initialize', 'session/new', 'session/prompt']); + assert.equal(events.some((entry) => entry.event === 'error'), false); +}); + function parseRpcWrites(writes: string[]): Array> { return writes .join('')