[Bug] Background (daemon) sessions drop assistant text blocks from transcript when response mixes text with tool_use (regression 2.1.160 → 2.1.161)

Status Open
Reported on v2.1.161
Maintainer reply None cached
Activity 10 comments · opened Jun 3, 2026

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.jsonl entry 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

  1. Install Claude Code 2.1.161 on Linux, daemon backend.
  2. Start a background job that runs multiple turns mixing assistant prose with tool calls (extended/interleaved thinking active, so responses contain 2 thinking blocks).
  3. Inspect ~/.claude/projects/<project>/<session>.jsonl: responses with thinking ×2 + tool_use have no text record; 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.)

View original on GitHub ↗

6 Comments

spiky02plateau · 2 months ago

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

  • Claude Code 2.1.162 (native install, ~/.local/share/claude/versions/), macOS (Darwin 25.5.0)
  • Model: claude-opus-4-8[1m] (interleaved thinking active)
  • Interactive terminal session (no backend/sessionKind fields in the transcript records)

Observed (transcript census of one ~5h session)

  • Assistant content blocks persisted to the session .jsonl: 67 thinking, 79 tool_use, only 11 text.
  • The session produced user-facing prose in most responses; nearly all of it is absent from the transcript. The pattern matches the OP: responses mixing text + tool_use (+ multiple interleaved thinking blocks) lose their text block; text-only responses persist fine, and a few short text+tool_use responses (apparently with a single thinking block) also survived.
  • User-visible effect identical to the OP: the conversation view shows only tool-call summaries and bare AskUserQuestion dialogs, none of the assistant's prose. Lost text is unrecoverable from disk.

Version timeline corroboration

  • Long-running sessions started on ≤2.1.154 render prose normally (still running, pre-update binaries in memory).
  • The auto-update flipped the claude symlink 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>).

combatwombat · 2 months ago

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 with run_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:

  1. In interactive mode the dropped text is never rendered in the TUI either — the turn visually starts at the tool call, so the user never sees the prose at all (not just a replay/viewer problem).
  2. The in-memory conversation still contains the text. In a later turn the model quoted its "lost" paragraphs verbatim, though they exist nowhere on disk. So generation is fine, the loss is on the shared render/persist path. (It also means the lost text silently diverges the API context from the transcript until the session ends, then it's gone.)
topp · 2 months ago

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.

jtiberio-sv · 1 month ago

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, assistant stream events, and even the content_block_delta partials).

Environment

  • Claude Code CLI 2.1.196 (win32-x64, VS Code extension bundle), Windows 11 Pro
  • Headless: claude -p --output-format stream-json --include-partial-messages --verbose (reproduces in both one-shot --resume and long-lived --input-format stream-json modes)
  • Model: claude-fable-5 (interleaved thinking), MCP stdio server registered via --mcp-config
  • Permission mode: bypassPermissions

Fingerprint

When an assistant message combines interleaved thinking + text + tool_use in the same message, the CLI sometimes drops the text block:

  1. NOT persisted to the session .jsonl — the message appears only as thinking + tool_use lines sharing the same message.id.
  2. NOT included in the emitted assistant stream-json event either (confirmed 2026-07-04).
  3. Usually the text still exists in the text_delta partial 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)

  • Most lost messages persisted as [thinking, thinking, tool_use(mcp tool)] — long answers (~2–3 KB of markdown each) gone.
  • But we also lost a [thinking, text→dropped, tool_use(Read)] with a built-in tool, so it is not MCP-specific.
  • We shipped a server-side mitigation that captures every text block seen in assistant stream 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 the assistant events too, not just from persistence.
  • Strongest occurrence: after we also shipped a text_delta-accumulation fallback, the bug hit again and the fallback captured nothing for the lost message.id (while capturing deltas for neighboring messages of the same turn). In that case the text was absent from .jsonl, assistant events and deltas — the user never saw the reply at all.
  • The trigger seems to be per turn rather than per message shape: in the worst turn, every text block of the turn was dropped (4 assistant messages in a row, including simple [thinking, text, tool_use(Read)] shapes), while other turns in the same session persist identical shapes fine. That worst turn was the first --resume turn after our host app restarted, in case that helps.

Impact beyond display

Since the .jsonl is 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_delta events per message.id into a sidecar and re-inject the text on load when the .jsonl message has tool_use but 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 .jsonl lines / stream-json captures for the affected message.ids if useful.

jtiberio-sv · 1 month ago

Raw-stream proof: the text block never reaches the CLI's stdout — it is replaced by an extra signed-but-empty thinking block (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.exe before any client-side parsing. Setup: CLI 2.1.196, Windows 11, model claude-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_delta chars on a thinking block that streamed zero thinking_delta chars):

call#03  [thinking(sig=1712), thinking(sig=1104), tool_use(mcp__voxask__vox_ask)]   <- text MISSING
call#10  [thinking(sig=6208), thinking(sig=1676), tool_use(mcp__voxask__vox_ask)]   <- text MISSING
call#38  [thinking(sig=656),  thinking(sig=1200), tool_use(mcp__voxask__vox_ask)]   <- text MISSING
call#41  [thinking(sig=3956), thinking(sig=1440), tool_use(mcp__voxask__vox_ask)]   <- text MISSING
call#48  [thinking(sig=5236), thinking(sig=1464), tool_use(mcp__voxask__vox_ask)]   <- text MISSING
call#56  [thinking(sig=2232), thinking(sig=1784), tool_use(mcp__voxask__vox_ask)]   <- text MISSING

-- healthy messages from the SAME session --
call#01  [thinking(sig=1856),  text(72 chars),   tool_use(PowerShell)]
call#12  [thinking(sig=12736), text(362 chars),  tool_use(Grep)]
call#40  [thinking(sig=532),   text(97 chars),   tool_use(PowerShell)]
call#50  [thinking(sig=3740),  text(165 chars),  tool_use(Grep)]
call#58  [thinking(sig=1288),  text(1481 chars)]                      <- text-only: always fine

Findings:

  1. The text block never exists in the CLI's stdout for the broken messages — no content_block_start of type text, zero text_deltas. The transcript .jsonl faithfully records what the stream delivered, so this cannot be recovered or fixed client-side.
  2. Every broken message carries an extra thinking block that streams 0 thinking chars and only a signature_delta — the user-visible answer appears to be repackaged as an encrypted/signed thinking block. Consistent with #65620.
  3. The model still has the lost text in context: asking "repeat your lost answer" in the next turn reproduces it verbatim — further evidence of server-side repackaging rather than the text never being generated.
  4. Trigger correlation in this session: answer text preceding an MCP 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 -p mode it is intermittent.

Happy to share the full (redacted) raw ndjson tap if useful.

0xdhx · 1 month ago

Confirming this is still present on 2.1.212 (current latest/next; stable 2.1.205 also affected — the 2.1.205–2.1.212 changelog claims no related fix) — Linux (WSL2 Ubuntu), interactive TUI session, Anthropic API, model claude-fable-5. Same fingerprint as the OP and the #65620 shape table: any assistant API message persisted with ≥2 thinking blocks has zero text records, 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.id shared across the JSONL lines):

jq -s '[.[] | select(.type=="assistant") | select((.message.content|type)=="array")] | group_by(.message.id)
  | map({think: ([.[].message.content[] | select(.type=="thinking")] | length),
         text:  ([.[].message.content[] | select(.type=="text")] | length)})
  | {multi_think: [.[] | select(.think>=2)] | length,
     multi_think_with_text: [.[] | select(.think>=2 and .text>0)] | length,
     single_think_with_text: [.[] | select(.think<=1 and .text>0)] | length,
     single_think_total: [.[] | select(.think<=1)] | length}' <session>.jsonl
# → {"multi_think":34,"multi_think_with_text":0,"single_think_with_text":19,"single_think_total":43}

(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_tokens on the textless messages is consistent with substantial text having been generated. Two incident messages that persisted only thinking ×2 (empty, signature present) + one small tool_use billed 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 is thinking ×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.

Showing cached comments. Read the full discussion on GitHub ↗