fix(daemon): normalize cumulative ACP message chunks (#3333)

* fix(daemon): normalize cumulative acp message chunks

- apps/daemon/src/acp.ts
- apps/daemon/tests/acp.test.ts
- apps/web/src/providers/daemon.ts
- apps/web/src/components/DesignSystemFlow.tsx

Convert cumulative ACP message snapshots into suffix deltas and keep temporary browser debug instrumentation for trace verification.

* chore(web): remove temporary stream debug hooks

- apps/web/src/providers/daemon.ts
- apps/web/src/components/DesignSystemFlow.tsx

Remove the browser debug accumulator after validating the ACP duplication trace.
This commit is contained in:
Ramiro 2026-05-30 06:17:32 +02:00 committed by GitHub
parent 41b1cd763e
commit c33641e592
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 88 additions and 9 deletions

View file

@ -457,6 +457,7 @@ export function attachAcpSession({
let emittedThinkingStart = false;
let emittedFirstTokenStatus = false;
let emittedTextChunk = false;
let emittedTextBuffer = '';
let finished = false;
let fatal = false;
let aborted = false;
@ -618,16 +619,22 @@ export function attachAcpSession({
if (update.sessionUpdate === 'agent_message_chunk') {
const text = asObject(update.content)?.text;
if (typeof text === 'string' && text.length > 0) {
emittedTextChunk = true;
if (!emittedFirstTokenStatus) {
emittedFirstTokenStatus = true;
send('agent', {
type: 'status',
label: 'streaming',
ttftMs: Date.now() - runStartedAt,
});
const delta = text.startsWith(emittedTextBuffer)
? text.slice(emittedTextBuffer.length)
: text;
if (delta.length > 0) {
emittedTextChunk = true;
emittedTextBuffer += delta;
if (!emittedFirstTokenStatus) {
emittedFirstTokenStatus = true;
send('agent', {
type: 'status',
label: 'streaming',
ttftMs: Date.now() - runStartedAt,
});
}
send('agent', { type: 'text_delta', delta });
}
send('agent', { type: 'text_delta', delta: text });
}
return;
}

View file

@ -237,6 +237,74 @@ test('attachAcpSession includes image attachments as ACP resource links', () =>
});
});
test('attachAcpSession converts cumulative ACP message snapshots into deltas', () => {
const child = new FakeAcpChild();
const events: Array<{ event: string; payload: unknown }> = [];
attachAcpSession({
child: child as never,
prompt: 'describe the project',
cwd: '/tmp/od-project',
model: null,
mcpServers: [],
send: (event, payload) => events.push({ event, payload }),
});
writeAcpResult(child, 1, {});
writeAcpResult(child, 2, { sessionId: 'session-1' });
writeAcpUpdate(child, {
sessionUpdate: 'agent_message_chunk',
content: { text: 'Agent Haven' },
});
writeAcpUpdate(child, {
sessionUpdate: 'agent_message_chunk',
content: { text: 'Agent Haven — managed AI agents' },
});
writeAcpUpdate(child, {
sessionUpdate: 'agent_message_chunk',
content: { text: 'Agent Haven — managed AI agents' },
});
writeAcpResult(child, 3, { usage: { inputTokens: 1, outputTokens: 2 } });
const textDeltas = events
.filter((entry) => entry.event === 'agent' && (entry.payload as { type?: unknown }).type === 'text_delta')
.map((entry) => (entry.payload as { delta?: unknown }).delta);
assert.deepEqual(textDeltas, ['Agent Haven', ' — managed AI agents']);
});
test('attachAcpSession keeps incremental ACP message chunks unchanged', () => {
const child = new FakeAcpChild();
const events: Array<{ event: string; payload: unknown }> = [];
attachAcpSession({
child: child as never,
prompt: 'describe the project',
cwd: '/tmp/od-project',
model: null,
mcpServers: [],
send: (event, payload) => events.push({ event, payload }),
});
writeAcpResult(child, 1, {});
writeAcpResult(child, 2, { sessionId: 'session-1' });
writeAcpUpdate(child, {
sessionUpdate: 'agent_message_chunk',
content: { text: 'Agent Haven' },
});
writeAcpUpdate(child, {
sessionUpdate: 'agent_message_chunk',
content: { text: ' — managed AI agents' },
});
writeAcpResult(child, 3, { usage: { inputTokens: 1, outputTokens: 2 } });
const textDeltas = events
.filter((entry) => entry.event === 'agent' && (entry.payload as { type?: unknown }).type === 'text_delta')
.map((entry) => (entry.payload as { delta?: unknown }).delta);
assert.deepEqual(textDeltas, ['Agent Haven', ' — managed AI agents']);
});
test('attachAcpSession exposes abort and sends session cancel after session creation', () => {
const child = new FakeAcpChild();
const writes: string[] = [];
@ -328,6 +396,10 @@ function writeAcpResult(child: FakeAcpChild, id: number, result: unknown): void
child.stdout.write(`${JSON.stringify({ id, result })}\n`);
}
function writeAcpUpdate(child: FakeAcpChild, update: unknown): void {
child.stdout.write(`${JSON.stringify({ method: 'session/update', params: { update } })}\n`);
}
function agentModelStatuses(events: Array<{ event: string; payload: unknown }>): unknown[] {
return events
.filter((entry) => {