Assistant text blocks intermittently missing from session transcript JSONL (thinking/tool_use persist)
Summary
Assistant text blocks are sometimes not persisted to the session transcript JSONL, while thinking and tool_use blocks from the same turns are. When a text block is the load-bearing content of a turn (e.g. an announcement a hook or later tooling parses), losing it silently breaks anything that reads the transcript as the source of truth for what the assistant said.
Environment
- Claude Code
2.1.195 - Linux
- Long session (600+ assistant turns), heavy tool use, several compaction boundaries
What I observed
In one session's transcript (~/.claude/projects/<proj>/<uuid>.jsonl), aggregate block counts across all assistant lines:
assistant lines: 601 | thinking blocks: 244 | tool_use blocks: 299 | text blocks: 58
58 text blocks against 244 thinking / 299 tool_use is far below the number of turns in which the assistant emitted user-visible prose. Spot-checking a specific window where I know the assistant emitted a short prose line immediately before a tool call, the transcript shows the turn as thinking + tool_use with no text block — the prose was rendered in the UI but never written to the JSONL.
Representative window (each row is one JSONL entry: index type content-block-types):
977 assistant thinking
978 assistant text
979 assistant tool_use <- Skill call
...
984 assistant thinking
985 assistant thinking
986 assistant tool_use
989 assistant tool_use
992 assistant tool_use
995 assistant thinking
996 assistant thinking
997 assistant tool_use
1005 assistant tool_use
1009 assistant thinking
1010 assistant tool_use
Turns 984–1010 carry thinking/tool_use but the short prose lines the assistant emitted in several of them are absent. Earlier prose in the same session (e.g. index 978) was persisted, so it is intermittent, not a blanket omission. It does not appear to be a late-flush race: re-reading the file well after the session had moved on, the blocks are still absent.
Why it matters
Anything that treats the transcript as the record of what the assistant said can silently get the wrong answer. In my case a PreToolUse hook classifies the session mode by parsing a line-start [SPAD-MODE:…] token from assistant text after a triage tool call; when the announcement text block is dropped, the token is invisible and the hook resolves the wrong mode with no way to recover through the intended (text) channel. I worked around it by having the hook also read the token from the triage tool call's input.args (tool_use blocks persist reliably), but the underlying transcript loss affects any transcript consumer, not just hooks.
Expected
Every assistant text block that was produced in a turn is written to the transcript JSONL, with the same reliability as thinking and tool_use blocks.
Repro notes
I don't have a minimal deterministic repro — it surfaced in a long, tool-heavy session and is intermittent. The signal that reproduces it is the aggregate imbalance: in a session with hundreds of prose-bearing turns, grep-count "type":"text" assistant blocks vs "type":"tool_use" and compare against how often the assistant actually spoke. If text blocks are dramatically undercounted, this is the bug. Happy to share more structural detail (block-type sequences, not content) if useful.