From 8df7acc35eb3a36cc206a96a552ca6174a5efaa4 Mon Sep 17 00:00:00 2001 From: owieschon Date: Mon, 7 Sep 2026 00:00:26 -0400 Subject: [PATCH] fix(host-service): preserve consumed Claude prompts in handoffs --- .../src/terminal/harness-transcript.test.ts | 83 +++++++++++++++++++ .../src/terminal/harness-transcript.ts | 19 +++++ 2 files changed, 102 insertions(+) diff --git a/packages/host-service/src/terminal/harness-transcript.test.ts b/packages/host-service/src/terminal/harness-transcript.test.ts index 31fdd385960..6bc0e94cff2 100644 --- a/packages/host-service/src/terminal/harness-transcript.test.ts +++ b/packages/host-service/src/terminal/harness-transcript.test.ts @@ -35,6 +35,89 @@ afterEach(() => { }); describe("readHarnessTranscript", () => { + test("preserves a consumed busy-session follow-up in conversation order", () => { + // Claude Code 2.1.263: a prompt entered during a tool call is first + // queued, then recorded as queued_command when the turn consumes it. + const prompt = "Keep the existing API.\nAdd a regression for empty input."; + const { worktreePath, sessionId } = seedClaudeSession([ + JSON.stringify({ type: "user", message: { content: "Fix the parser." } }), + JSON.stringify({ + type: "queue-operation", + operation: "enqueue", + content: prompt, + }), + JSON.stringify({ + type: "queue-operation", + operation: "remove", + reason: "absorbed_mid_turn", + content: prompt, + }), + JSON.stringify({ + type: "attachment", + attachment: { + type: "queued_command", + commandMode: "prompt", + prompt, + source_uuid: "follow-up", + origin: { kind: "human" }, + }, + }), + JSON.stringify({ + type: "assistant", + message: { content: "Kept the API and added the regression." }, + }), + ]); + + expect( + readHarnessTranscript({ + agentId: "claude", + agentSessionId: sessionId, + worktreePath, + })?.text, + ).toBe( + `User: Fix the parser.\n\nUser: ${prompt}\n\nAssistant: Kept the API and added the regression.`, + ); + }); + + test("excludes unconsumed queue operations and unrelated attachments", () => { + const { worktreePath, sessionId } = seedClaudeSession([ + JSON.stringify({ + type: "user", + message: { content: "Keep this instruction." }, + }), + JSON.stringify({ + type: "queue-operation", + operation: "enqueue", + content: "Pending instruction", + }), + JSON.stringify({ + type: "queue-operation", + operation: "remove", + content: "Removed instruction", + }), + ...[ + undefined, + { type: "other", commandMode: "prompt", prompt: "Unrelated" }, + { type: "queued_command", commandMode: "bash", prompt: "echo shell" }, + { type: "queued_command", commandMode: "prompt", prompt: " " }, + { type: "queued_command", commandMode: "prompt", prompt: null }, + { + type: "queued_command", + commandMode: "prompt", + prompt: { text: "Malformed" }, + }, + ].map((attachment) => JSON.stringify({ type: "attachment", attachment })), + ]); + + expect( + readHarnessTranscript({ + agentId: "claude", + agentSessionId: sessionId, + worktreePath, + })?.text, + ).toBe("User: Keep this instruction."); + }); + test("reads the conversation out of Claude's own store", () => { const { worktreePath, sessionId } = seedClaudeSession([ JSON.stringify({ type: "mode", mode: "normal" }), diff --git a/packages/host-service/src/terminal/harness-transcript.ts b/packages/host-service/src/terminal/harness-transcript.ts index 31689d696ba..a5cd29f88d1 100644 --- a/packages/host-service/src/terminal/harness-transcript.ts +++ b/packages/host-service/src/terminal/harness-transcript.ts @@ -97,6 +97,11 @@ export function readFileTail(path: string, maxBytes: number): string | null { interface ClaudeEvent { type?: string; + attachment?: { + type?: string; + commandMode?: string; + prompt?: unknown; + }; message?: { role?: string; content?: string | Array<{ type?: string; text?: string }>; @@ -133,6 +138,20 @@ function readClaudeTranscript( } catch { continue; // a partially written final line while the session runs } + // Claude records consumed busy-session prompts as attachments. Queue + // operations alone can describe input the user later removes. + if (event.type === "attachment") { + const attachment = event.attachment; + if ( + attachment?.type === "queued_command" && + attachment.commandMode === "prompt" && + typeof attachment.prompt === "string" && + attachment.prompt.trim() + ) { + turns.push(`User: ${attachment.prompt.trim()}`); + } + continue; + } if (event.type !== "user" && event.type !== "assistant") continue; const text = textOf(event); if (!text) continue;