[Bug] Background (daemon) sessions drop assistant text blocks from transcript when response mixes text with tool_use (regression 2.1.160 → 2.1.161)
Environment
- Claude Code: 2.1.161 (npm install, Linux)
- OS: Ubuntu 24.04 (kernel 6.17)
- Session type: background job, daemon backend (
"backend": "daemon",sessionKind: "bg"in the transcript records) - Model: claude-opus-4-8
Summary
In background (daemon) sessions, assistant text blocks are not persisted to the session transcript (~/.claude/projects/<project>/<session>.jsonl) whenever the response also contains tool_use blocks and two thinking blocks (interleaved thinking). The transcript ends up with only thinking records (empty thinking: "", signature present) plus the tool_use records — the text record between them is missing entirely.
This is a regression from 2.1.160: the same session has turns from the previous day on 2.1.160 where every text block was persisted correctly. After the daemon updated to 2.1.161 and the job restarted, only the very first response of the run (which had a single thinking block) kept its text; every subsequent response lost its text block.
Observed transcript pattern (sanitized)
# first response after restart — text persisted (single thinking block)
record 93 assistant thinking (thinking:"", signature present) req_A
record 94 assistant text[107 chars] req_A
record 95 assistant tool_use: ToolSearch req_A
# every later response — text block missing
record 99 assistant thinking (thinking:"", signature present) req_B
record 100 assistant thinking (thinking:"", signature present) req_B
record 101 assistant tool_use: Agent req_B
record 102 assistant tool_use: Agent req_B
# the response's text block was streamed live but never written
record 123 assistant thinking (thinking:"", signature present) req_C
record 124 assistant thinking (thinking:"", signature present) req_C
record 125 assistant tool_use: AskUserQuestion req_C
# a ~150-word answer preceding AskUserQuestion is absent from the jsonl
Scanning the whole file: after the restart on 2.1.161 there are zero assistant text records other than the first response, across ~10 consecutive responses that all contained user-facing text. Text-only final responses (no tool_use) DO persist fine.
Impact
- The background job viewer replays the conversation from the jsonl, so the user sees only tool-call summaries and a bare AskUserQuestion dialog with none of the assistant's prose (including the main deliverable the job was asked to produce).
- The job state extractor / timeline gets
text: ""for the turn, so the job list shows a stale state and detail (timeline.jsonlentry recorded with empty text). - The lost text is unrecoverable from disk — it only exists in the daemon's memory until the session produces a final text-only response.
Steps to reproduce
- Install Claude Code 2.1.161 on Linux, daemon backend.
- Start a background job that runs multiple turns mixing assistant prose with tool calls (extended/interleaved thinking active, so responses contain 2 thinking blocks).
- Inspect
~/.claude/projects/<project>/<session>.jsonl: responses withthinking ×2 + tool_usehave notextrecord; reopening the job in the viewer shows no assistant prose.
Expected behavior
Assistant text blocks are persisted to the transcript for every response, as in 2.1.160. (Thinking blocks being persisted with empty content + signature looks intentional; the regression is specifically the dropped text blocks.)
Showing cached comments. Read the full discussion on GitHub ↗
6 Comments
Confirming this regression on macOS in a regular interactive TUI session — it is not specific to Linux or the daemon backend, and it is still present in 2.1.162.
Environment
~/.local/share/claude/versions/), macOS (Darwin 25.5.0)backend/sessionKindfields in the transcript records)Observed (transcript census of one ~5h session)
.jsonl: 67thinking, 79tool_use, only 11text.Version timeline corroboration
claudesymlink to 2.1.162; every session started after that exhibits the bug, across different projects — so it reproduces consistently on 2.1.162 interactive sessions, not just 2.1.161 background jobs.Workaround we're using: relaunching panes with the cached previous binary (
~/.local/share/claude/versions/2.1.160 --resume <session-id>).Also reproducible in interactive (non-daemon) sessions, on macOS, in 2.1.163, so it's broader than the daemon backend.
Environment: Claude Code 2.1.163, macOS 15 (Darwin 24.6.0), Terminal.app, Anthropic API, model
claude-opus-4-8[1m].Identical fingerprint: an assistant response of shape
thinking → text → thinking → text → tool_use(the tool_use was a Bash call withrun_in_background: true) was persisted to the session jsonl as:{"type": "thinking", "thinking": "", "signature": "CAESzAUK..."}
{"type": "thinking", "thinking": "", "signature": "CAESzQgK..."}
{"type": "tool_use", "name": "Bash", "input": {"command": "...", "run_in_background": true}}
Both text blocks missing; grep for distinctive phrases across the whole
~/.claude/projects/<project>/dir: 0 hits. Responses in the same session with a single thinking block kept their text, matching the pattern from above.Two more impacts:
Cross-reference: same regression analysed on #65620 (interleaved-thinking pattern, version census, date-onset data); also the original report on #64129. Linking here so the relation is visible if/when triage consolidates.
We're hitting what looks like exactly this bug, and we have some additional evidence that may help narrow it down — including one occurrence where the text block was absent from all three surfaces (session
.jsonl,assistantstream events, and even thecontent_block_deltapartials).Environment
claude -p --output-format stream-json --include-partial-messages --verbose(reproduces in both one-shot--resumeand long-lived--input-format stream-jsonmodes)--mcp-configFingerprint
When an assistant message combines interleaved thinking + text + tool_use in the same message, the CLI sometimes drops the text block:
.jsonl— the message appears only asthinking+tool_uselines sharing the samemessage.id.assistantstream-json event either (confirmed 2026-07-04).text_deltapartial events — so it renders live and then vanishes when a client reconciles with the.jsonl.Nondeterministic: messages with the same shape sometimes persist fine.
Evidence across ~8 lost messages (2026-07-03/04)
[thinking, thinking, tool_use(mcp tool)]— long answers (~2–3 KB of markdown each) gone.[thinking, text→dropped, tool_use(Read)]with a built-in tool, so it is not MCP-specific.assistantstream events into a sidecar store: for the lost messages the sidecar has no entry at all, while short text messages in the same turns were captured normally → the text block was absent from theassistantevents too, not just from persistence.text_delta-accumulation fallback, the bug hit again and the fallback captured nothing for the lostmessage.id(while capturing deltas for neighboring messages of the same turn). In that case the text was absent from.jsonl,assistantevents and deltas — the user never saw the reply at all.[thinking, text, tool_use(Read)]shapes), while other turns in the same session persist identical shapes fine. That worst turn was the first--resumeturn after our host app restarted, in case that helps.Impact beyond display
Since the
.jsonlis the source of truth for--resume, the model itself permanently loses that part of its own context on resume. Summaries, handoffs and any tooling reading the transcript also miss it.Workaround we ship (partial)
We accumulate
text_deltaevents permessage.idinto a sidecar and re-inject the text on load when the.jsonlmessage hastool_usebut no text block. That recovers display in most cases — but not the all-three-surfaces occurrences, and the model context on resume is still missing the text.Happy to provide the raw
.jsonllines / stream-json captures for the affectedmessage.ids if useful.Raw-stream proof: the text block never reaches the CLI's stdout — it is replaced by an extra signed-but-empty
thinkingblock (server-side, not a client parsing bug)Following up on my earlier comment: I instrumented a tap that records every raw stdout line of
claude.exebefore any client-side parsing. Setup: CLI 2.1.196, Windows 11, modelclaude-fable-5, headless--output-format stream-json --include-partial-messages --verbose, long-lived process via--input-format stream-json, MCP stdio tool (vox_ask).Block structure of the assistant messages in one session (content redacted — only block types and streamed char counts;
sig=signature_deltachars on a thinking block that streamed zerothinking_deltachars):Findings:
textblock never exists in the CLI's stdout for the broken messages — nocontent_block_startof typetext, zerotext_deltas. The transcript.jsonlfaithfully records what the stream delivered, so this cannot be recovered or fixed client-side.thinkingblock that streams 0 thinking chars and only asignature_delta— the user-visible answer appears to be repackaged as an encrypted/signed thinking block. Consistent with #65620.tool_use(interleaved thinking on) lost the text 6 out of 6 times, while text preceding built-in tools (PowerShell/Grep/Read) streamed fine 6/6. With--input-format stream-json(persistent process) it reproduces every time; in one-shot-pmode it is intermittent.Happy to share the full (redacted) raw ndjson tap if useful.
Confirming this is still present on 2.1.212 (current
latest/next;stable2.1.205 also affected — the 2.1.205–2.1.212 changelog claims no related fix) — Linux (WSL2 Ubuntu), interactive TUI session, Anthropic API, modelclaude-fable-5. Same fingerprint as the OP and the #65620 shape table: any assistant API message persisted with ≥2 thinking blocks has zerotextrecords, while single-thinking messages from the same session persist text normally.Three data points the thread doesn't have yet:
1. Corpus scale — zero counterexamples. Sweep of 26 recent session transcripts on one machine: 477 assistant messages with ≥2 thinking blocks, none with a persisted text block (all fable-5). One live session in detail, re-run at the time of this comment (grouping key is the API
message.idshared across the JSONL lines):(Fittingly, the session that authored this comment reproduced it too: 9 multi-thinking messages at the time of posting, zero persisted text blocks.)
2. The dropped text is billed.
usage.output_tokenson the textless messages is consistent with substantial text having been generated. Two incident messages that persisted onlythinking ×2 (empty, signature present) + one small tool_usebilled 4767 and 3278 output tokens respectively; a comparable single-thinking control from the same session (thinking + 78-char text + tool_use, text persisted fine) billed 1162. The token accounting says the prose existed; the store says it never did.3. On 2.1.212 the loss is at (or before) the client — the text never renders in the TUI either. Reproduced textless multi-thinking messages ahead of three different tool types (
Bash,Skill,AskUserQuestion), so it is not widget-specific. In our incident turns the prose was absent from the live viewport at incident time as well as from the JSONL — matching the raw-stream tap finding above (text block never reaches the CLI's stdout) rather than the earlier "rendered on screen but not persisted" reports from 2.1.162–2.1.165. The worst-case shape isthinking ×2 + AskUserQuestion: the dialog's own question/option text renders completely, but the reasoning laid out for the decision is gone, so the user answers the gate blind.The fingerprint is checkable in any interleaved-thinking transcript with the jq above; happy to run further forensics on request.