Assistant text between tool calls is silently lost: not rendered, not in Ctrl+O, not persisted to session .jsonl (interleaved thinking)
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
.jsonlunder~/.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
AskUserQuestiondialogs without the analysis written for them, and cannot recover it via Ctrl+O (the transcript viewer reads the same incomplete file). - Local
--resumereconstructs context without these blocks — permanent context loss, not just a rendering gap.
Repro (best effort)
- Claude Code v2.1.210 with a model that produces interleaved thinking (e.g.
claude-fable-5). - Run a multi-step task so the assistant narrates between tool batches, ideally ending in an
AskUserQuestion. grepthe session.jsonlfor a distinctive phrase from the mid-turn narration → 0 matches; only turn-opening and turn-final texts are present.
🤖 Generated with Claude Code
Showing cached comments. Read the full discussion on GitHub ↗
5 Comments
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.
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
thinkingblocks where the text vanished) live atPostToolBatch/Stopand 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.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-sentinelUpdate 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-timetexts: 0is 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
@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.