diff --git a/docs/design/2026-07-14-chat-record-daemon-transcript-block-projection.md b/docs/design/2026-07-14-chat-record-daemon-transcript-block-projection.md index ee169121507..dd2e8a033ab 100644 --- a/docs/design/2026-07-14-chat-record-daemon-transcript-block-projection.md +++ b/docs/design/2026-07-14-chat-record-daemon-transcript-block-projection.md @@ -353,6 +353,7 @@ The first version stabilizes at least the following diagnostic codes. Codes are | malformed_part | true | A recognized malformed part was skipped | | unknown_record_or_part | true | An unknown extension may contain visible data | | ambiguous_tool_call_correlation | true | A tool result cannot be correlated uniquely | +| missing_tool_result | true | A tool call has no persisted result | | presentation_fallback | false | Presentation adapter failed; fallback used | | transcript_blocks_truncated | true | `maxBlocks` removed older blocks | | transcript_text_truncated | true | A text block exceeded the character limit | diff --git a/packages/acp-bridge/src/transcript-replay.test.ts b/packages/acp-bridge/src/transcript-replay.test.ts index 27ae8dff014..937701c940c 100644 --- a/packages/acp-bridge/src/transcript-replay.test.ts +++ b/packages/acp-bridge/src/transcript-replay.test.ts @@ -58,7 +58,8 @@ describe('createTranscriptReplayMachine', () => { }); it('uses stable synthetic ids and finalizes dangling calls once', () => { - const machine = createTranscriptReplayMachine(); + const onDiagnostic = vi.fn(); + const machine = createTranscriptReplayMachine({ onDiagnostic }); const projected = updates( machine, record('assistant-1', 'assistant', { @@ -88,6 +89,13 @@ describe('createTranscriptReplayMachine', () => { }, ], }); + expect(onDiagnostic).toHaveBeenCalledWith( + expect.objectContaining({ + code: 'missing_tool_result', + affectsCompleteness: true, + recordId: 'assistant-1', + }), + ); expect([...machine.finalize()]).toEqual([]); }); diff --git a/packages/acp-bridge/src/transcript-replay.ts b/packages/acp-bridge/src/transcript-replay.ts index ed099681b57..7f88e93cadb 100644 --- a/packages/acp-bridge/src/transcript-replay.ts +++ b/packages/acp-bridge/src/transcript-replay.ts @@ -425,6 +425,11 @@ class DefaultTranscriptReplayMachine implements TranscriptReplayMachine { let ordinal = 0; for (const pending of [...this.pendingToolCalls.values()]) { this.pendingToolCalls.delete(pending.callId); + this.report( + 'missing_tool_result', + 'A transcript tool call has no persisted result.', + pending.sourceRecordId, + ); yield { sourceRecordId: pending.sourceRecordId, ...(pending.sourceTimestamp diff --git a/packages/sdk-typescript/test/unit/daemon-transcript-projection.test.ts b/packages/sdk-typescript/test/unit/daemon-transcript-projection.test.ts index a2a4d1be3cb..171838b6d3c 100644 --- a/packages/sdk-typescript/test/unit/daemon-transcript-projection.test.ts +++ b/packages/sdk-typescript/test/unit/daemon-transcript-projection.test.ts @@ -277,7 +277,7 @@ describe('projectChatRecordsToDaemonTranscript', () => { expect(projection.complete).toBe(true); }); - it('finalizes dangling tools as failed', () => { + it('finalizes dangling tools as failed and marks the projection incomplete', () => { const projection = projectChatRecordsToDaemonTranscript([ record('tool-start', null, { type: 'assistant', @@ -293,6 +293,14 @@ describe('projectChatRecordsToDaemonTranscript', () => { status: 'failed', toolCallId: 'qwen-replay-tool:tool-start:0', }); + expect(projection.complete).toBe(false); + expect(projection.diagnostics).toContainEqual( + expect.objectContaining({ + code: 'missing_tool_result', + affectsCompleteness: true, + recordId: 'tool-start', + }), + ); }); it('preserves assistant usage when the record ends with a tool call', () => {