Assistant text between tool calls is silently lost: not rendered, not in Ctrl+O, not persisted to session .jsonl (interleaved thinking)

Status Open
Maintainer reply None cached
Activity 9 comments · opened Jul 15, 2026

Environment

  • Claude Code v2.1.210, CLI TUI in macOS Terminal (Darwin 25.5.0)
  • Model: claude-fable-5 (produces interleaved thinking blocks)
  • Session type: claude.ai/code-connected session; evidence below is from the local session .jsonl under ~/.claude/projects/

Summary

When the assistant emits a text block followed by tool_use blocks in the same response, and the response also contains interleaved thinking blocks, the text block is silently lost: it is (a) never rendered in the main conversation view, (b) not visible in the Ctrl+O transcript viewer, and (c) absent from the session .jsonl on disk — verified with both raw grep and JSON-decoded search (jq '[.. | strings] | join(" ")'), so it is not an encoding/escaping artifact. This is distinct from the known AskUserQuestion overlay family (#23862, #30422, #58207), which is display-layer occlusion of the last line; here the content is not persisted at all, so the user has no way to ever see it and --resume from the local file permanently drops it from context.

Worst case observed: a ~1,800-char findings message written immediately before an AskUserQuestion call was completely lost — the user answered the multiple-choice question without ever seeing the analysis it was based on, and only noticed when the assistant later referred back to "the message above the question".

Evidence (sanitized structure dump of one session)

Per-record content-block types extracted with:

jq -r 'select(.type=="assistant") | .timestamp + "  " + ([.message.content[]? | if .type=="tool_use" then "tool_use(" + .name + ")" elif .type=="text" then "text[" + (.text|length|tostring) + " chars]" else .type end] | join(", "))' session.jsonl

In a ~1h session the model emitted roughly 30 mid-turn narration text blocks (confirmed live on screen-side context; the assistant restated several of them later, and the API turn continued normally). Only 11 text records exist in the .jsonl. Every dropped text shares one structural signature — the file shows two consecutive thinking records written milliseconds apart, then the tool_use(s), with the text gone:

Dropped example 1 — the lost ~1.8k-char message directly before AskUserQuestion:

2026-07-15T00:36:58.437Z  thinking
2026-07-15T00:36:58.465Z  thinking            <- 28 ms apart
2026-07-15T00:37:06.055Z  tool_use(AskUserQuestion)

Dropped example 2 — narration before an Edit batch:

2026-07-15T00:36:01.457Z  thinking
2026-07-15T00:36:01.459Z  thinking            <- 2 ms apart
2026-07-15T00:36:05.224Z  tool_use(Edit)
2026-07-15T00:36:10.445Z  tool_use(Edit)
2026-07-15T00:36:20.440Z  tool_use(Edit)

Survivor — a mid-turn text that DID persist; note the single thinking record directly before it:

2026-07-15T00:17:38.440Z  thinking
2026-07-15T00:17:38.492Z  text[37 chars]
2026-07-15T00:17:43.001Z  tool_use(Edit)
2026-07-15T00:17:59.316Z  tool_use(Edit)

Reliably persisted in every turn: the first text block of the turn (before any tool call) and the turn-final text block. Reliably dropped: mid-turn texts whose response carries an adjacent interleaved thinking block (the dominant case with this model).

Impact

  • Users act on AskUserQuestion dialogs without the analysis written for them, and cannot recover it via Ctrl+O (the transcript viewer reads the same incomplete file).
  • Local --resume reconstructs context without these blocks — permanent context loss, not just a rendering gap.

Repro (best effort)

  1. Claude Code v2.1.210 with a model that produces interleaved thinking (e.g. claude-fable-5).
  2. Run a multi-step task so the assistant narrates between tool batches, ideally ending in an AskUserQuestion.
  3. grep the session .jsonl for a distinctive phrase from the mid-turn narration → 0 matches; only turn-opening and turn-final texts are present.

🤖 Generated with Claude Code

https://claude.ai/code/session_01MKEA83HXWJrrMzCCt95FwD

View original on GitHub ↗

5 Comments

SaravananJaichandar · 1 month ago

The silent drop is what makes this hurt. The text between tool calls is often the exact thing that explains why the next call happened, and if the CLI does not persist it there is nothing to reconstruct from. Been capturing every prompt, tool call, and assistant turn into an append only log on the side. etch.systems. Not a fix but at least the trail exists while this gets sorted.

geoh · 1 month ago

Workaround until this is fixed: https://github.com/podlayer/message-drop-sentinel — a single-file MIT hook that detects the drop signature described here (adjacent thinking blocks where the text vanished) live at PostToolBatch/Stop and prompts the agent to restate the lost message while it still remembers it. Deterministic, no dependencies, designed to be deleted when this issue closes. Fuller notes on the detection and its limits are in the README and in the comment on #74260.

geoh · 27 days ago

Follow-up on the detector linked above: controlled send-verified runs show the adjacent-thinking signature this issue documents is roughly a 7× undercount — most drops occur where the text follows a tool result, merges into the leading thinking block, and leaves no seam at all. v2.1 of the sentinel detects beyond the scar (per-batch persisted-text receipts the agent reconciles against what it sent) and adds a drop-proof relay channel via tool arguments. /plugin marketplace add podlayer/message-drop-sentinel

kk1987 · 26 days ago

Update from the original report, still present on 2.1.222 (claude-fable-5, effort xhigh) — plus a correction to my own summary.

Correction. I wrote that "the first text block of the turn (before any tool call)" reliably persists. Counterexample from a new incident session: a turn-opening text dropped, persisting as [thinking, thinking, tool_use, …] with the seam before the turn's first tool_use. So position is not the invariant — the structural trigger described in #74260 (text followed by another interleaved thinking block merges/drops, wherever it sits in the turn) fits all my data better, including the survivor example in my original report, which was shaped [thinking, text, tool_use]. The only position that has never dropped for me is the turn-final message.

Incident data. In the affected turn the agent wrote six mid-turn prose messages. A forensic scan run inside the same turn (before any recovery) showed texts: 0, four adjacent-thinking scars, and 5 of 6 probed messages ABSENT from the .jsonl — the sixth survived only because it rode a tool-call argument. The agent then resent everything as a turn-final message, which persisted. Note for anyone re-scanning that transcript later: the turn now contains 2 text blocks (the post-incident resend and its verification), so the incident-time texts: 0 is only visible in the captured scan output, not in the file's final state.

On the sentinel (v2.1.0 from the comments above, installed and active): its per-batch receipts reported the shortfall correctly in real time — including while this very comment was being prepared, in a fresh session, where receipts again flagged multiple mid-turn texts as not persisted. One caveat for other users: the relay only accepts a strict single-line form, and the primer doesn't say so; a multi-line relay attempt fails silently and the content ends up hidden in the collapsed tool result. Filed upstream with details: podlayer/message-drop-sentinel#1.

🤖 Generated with Claude Code

geoh · 25 days ago

@kk1987 — folded into v2.2, and your repo issue is answered in full there: position claims retired, only turn-final treated as safe, relay near-misses warn explicitly. Thanks — exactly the failure data the hook needed.

Showing cached comments. Read the full discussion on GitHub ↗