Pre-tool-call assistant text never emitted (prose stays in thinking) — onset ~2026-06-04, not CLI-version-bound
Summary
Since ~v2.1.162 (still present on 2.1.165), assistant text blocks are silently dropped from the session transcript (~/.claude/projects/…/<session>.jsonl) whenever the model emits another (interleaved) thinking block after the text in the same turn:
| generated turn | persisted to JSONL |
|---|---|
| [thinking, text, tool_use…] | [thinking, text, tool_use…] ✅ |
| [thinking, text] (final reply) | [thinking, text] ✅ |
| [thinking, text, thinking, tool_use…] | [thinking, thinking, tool_use…] ❌ text gone |
The prose is rendered on screen but appears nowhere in the file — not as text, not inside thinking, not in any other line type. The two persisted thinking blocks carry distinct signatures (both genuine); only the sandwiched text vanishes. v2.1.158 transcripts from the same machine/model persist every text block.
Environment
- claude 2.1.165 (also observed on 2.1.162); v2.1.158 unaffected
- macOS (Darwin 25.5.0), native installer
- Model: claude-opus-4-8 (1M context) — Opus thinks between text and tool calls constantly, so it's hit hardest
Evidence (two sessions analyzed line-by-line)
Session A (2.1.165, 35 API turns): every turn shaped thinking+text+tool_use… kept its text (10/10); every turn shaped thinking+thinking+tool_use… (15) corresponds to an on-screen prose message that is absent from the JSONL — verified by grepping the raw file for exact on-screen sentences (0 hits). Session B (2.1.162, 217 turns): 41 double-thinking turns, 48 kept texts, and — the tell — zero persisted …text→thinking… shapes across both sessions, a shape that is routine in interleaved-thinking output.
Per-turn grouping confirmed via shared requestId on the JSONL lines; the dropped turns are single API messages whose text block was never written between the two thinking lines.
Note: {"thinking":"","signature":"…"} (empty thinking, signature-only) is normal and appears in healthy 2.1.158 transcripts too — that is NOT the bug; only the text loss is.
Repro
- On ≥2.1.162, run any multi-step task with an Opus model that produces prose followed by more reasoning before tool calls ("investigate X" style tasks reproduce within a few turns).
- Note an assistant prose message on screen, then grep the session JSONL for its exact text.
- The message is missing whenever the turn's persisted shape is
[thinking, thinking, tool_use…].
Impact
--resume/ session-restore rebuilds context from the transcript → the model loses its own mid-task explanations and any in-band content in them- Every transcript consumer (analytics, observability tools, transcript viewers, custom tooling reading
~/.claude/projects) sees roughly half the assistant prose missing in tool-heavy Opus sessions - Hook-style integrations that rely on in-band assistant text (e.g. progress markers) silently break
Happy to provide redacted JSONL excerpts of broken turns (thinking/thinking/tool_use with matching requestIds) on request.
21 Comments
Analysis: Assistant text blocks not persisted to transcript JSONL when followed by interleaved thinking
1. How streaming events are processed
The Anthropic streaming API emits a sequence of SSE events for each assistant message:
Claude Code accumulates these events into
message.content[]arrays. Onmessage_stop, the full content array is written as a single JSONLassistantentry with all content blocks nested insidemessage.content.Evidence from transcript analysis: in v2.1.158 sessions, turns with shape
[thinking, text, tool_use]produce a JSONL entry whosemessage.contentis[{type: "thinking", ...}, {type: "text", ...}, {type: "tool_use", ...}]. In affected versions, only[{type: "thinking", ...}, {type: "thinking", ...}, {type: "tool_use", ...}]appears -- the{type: "text"}block at index 1 is absent.2. Where in the persistence pipeline text blocks get dropped
The regression window is v2.1.158 (unaffected) to v2.1.162 (affected). The CHANGELOG for v2.1.159 contains the prime suspect:
This fix addressed a problem where thinking blocks with their cryptographic signatures needed to be preserved exactly as the API returned them, else subsequent API calls (which include previous thinking blocks in history) would fail with 400 errors.
The likely root cause is in the content block accumulation logic that builds the
content[]array during streaming. Specifically, one of these scenarios:Scenario A (most likely) -- coalescing by type instead of by index:
The fix for v2.1.159 may have introduced normalization that merges adjacent thinking blocks. If the accumulation tracks blocks by type rather than by stream index, the handler for the second thinking
content_block_start(index 2) would match the existing thinking block (index 0) and either:In pseudocode, a buggy implementation might look like:
Scenario B -- index-tracked accumulator reset on second thinking:
The blocks may be tracked by sequential index, but the accumulator buffer that collects text deltas is prematurely cleared or overwritten when a second
content_block_startof typethinkingarrives. If the text accumulator is shared (not per-block), the second thinking delta overwrites the text accumulated so far before it is committed.Scenario C -- write ordering / async flush:
When blocks are written to JSONL individually (separate
assistantlines per block), the text block's JSONL write may be queued but then skipped or cancelled by a state transition caused by the subsequent thinking block.The fact that text renders on screen but never appears in the JSONL (as confirmed by the reporter grepping for exact on-screen sentences) rules out upstream API issues -- the text deltas arrive and are displayed. The loss is purely in the persistence path.
3. Evidence from transcript structure
Examining v2.1.162+ session transcripts confirms the pattern:
assistantJSONL entry contains amessage.contentarray[{type: "thinking"}],[{type: "text"}], or[{type: "tool_use"}][{type: "thinking"}, {type: "tool_use"}]occur (1 case in 1728 messages)[{type: "thinking"}, {type: "text"}, {type: "thinking"}]across ~5000 analyzed assistant messages from v2.1.161-v2.1.165 sessions, despite this shape being a routine Opus output patternThis absence is statistically impossible if the code were functioning correctly, given that Opus interleaves thinking between text and tool calls as standard behavior.
4. Suggested fix
The fix should ensure that the content block accumulator is index-addressed, not type-addressed, and that the accumulation buffer is properly flushed per-block before the next
content_block_start:indexfield oncontent_block_start/content_block_stop), not by block typecontent_block_stopbefore starting the next block, so text accumulated for block index 1 is committed to the content array before block index 2 (second thinking) beginsindexvalues are consecutive AND both are thinking type[thinking, text, thinking, tool_use]and asserts that all four blocks appear in the persisted content array with correct types and contentThe key invariant to preserve:
content[stream_index].type === event_type_at_that_indexfor everycontent_block_start/content_block_stoppair received during streaming.Windows confirmation + version census; evidence the missing text was never emitted as a text block (supports #65751's diagnosis)
Environment: Claude Code 2.1.165 (native installer), Windows 11 Home 10.0.26200, Windows Terminal, model
claude-opus-4-8(plain, not [1m]), interleaved thinking, effort xhigh.Census: parsed all local session transcripts (11 sessions, 414 unique assistant message ids after cross-file dedup, CC v2.1.145 → v2.1.165), counting messages whose persisted content contains a
thinking→thinkingadjacency:| CC version | assistant msgs | msgs with
thinking→thinking||---|---|---|
| 2.1.145–2.1.158 | 347 | 0 |
| 2.1.162 | 15 | 8 |
| 2.1.165 | 52 | 22 |
Every affected message persists as
[thinking, thinking, tool_use]; trailing tools include AskUserQuestion, Skill, Bash, Grep, and Workflow — not AskUserQuestion-specific. In each case multi-paragraph user-facing prose was composed at that point and never rendered (user-confirmed on screen) and no text block exists in the JSONL.Why we believe the text block was never generated (the #65751 reading, rather than "generated but dropped"):
.jsonl, per-session subdirectory, the~/.claudetree,~/.claude.json, AppData caches — zero hits (tool-input self-contamination accounted for). No text block exists anywhere on disk./exitand resume, i.e. across a process restart — it quotes the lost paragraphs verbatim. Local thinking storage is signature-only (emptythinkingcontent in every version we have), so the only channel that survives a restart is thinking-block replay. The prose therefore lived inside a thinking block.[thinking, text, tool_use(AskUserQuestion)]intact (3 examples, 339–1099-char texts);thinking→thinkingnever occurs in our corpus before 2.1.162 (Jun 3).Confound: in our data CC version and calendar date are inseparable (≤2.1.158 ≡ ≤May 31; ≥2.1.162 ≡ ≥Jun 3) — a server-side model/sampling change in early June would produce this exact census with no client change. (2.1.159's changelog is "Internal infrastructure improvements", also consistent with a client-side change.) We can't disentangle locally.
Ruled out: user hooks (drops occur on tool calls matched by no hook; hook-matched
text→toolmessages survived), double-counting (414 unique ids, 0 cross-file dupes), picker occlusion (#62493-style — the text is absent from the JSONL, not hidden).Workaround that holds: prose emitted as the final block of a turn always arrives as a real text block; only pre-tool-call prose is affected. We now have the model deliver must-read content end-of-turn only.
Related: #65662, #65603, #65751 — if this read is right, #65751 (auto-flagged duplicate) is the accurate diagnosis, and this issue's "not persisted" symptom is the same phenomenon: there was no text block to persist.
🤖 Generated with Claude Code
Adding a date-onset angle that complements the version-census in this thread
@AnnaThyme's census (2.1.145–158=0/347, .162=8/15, .165=22/52) reads naturally as a client regression introduced in 2.1.162. Below is one piece of data from my own transcripts that doesn't fit that read cleanly — message-level counts grouped by (date, CC version, model); "lost" = assistant message with any empty thinking block:
| date | client | model | lost / total msgs | rate |
|---|---|---|---|---|
| 2026-05-26 → 06-03 | 2.1.150 / .156 / .160 / .161 | claude-opus-4-8 | 0 / 2074 | 0% |
| — incl. 2026-06-03 alone | 2.1.161 | claude-opus-4-8 | 0 / 426 | 0% |
| 2026-06-04 | 2.1.161 | claude-opus-4-8 | 22 / 124 | 17.7% |
| 2026-06-04 | 2.1.162 | claude-opus-4-8 | 89 / 399 | 22.3% |
| 2026-06-05 | 2.1.161 | claude-opus-4-8 | 22 / 122 | 18.0% |
The decisive row pair: same client (2.1.161), same model (Opus 4.8) — 0/426 on June 3 → 22/124 on June 4. That overnight change is not a client update on my side, not a settings change, and not a model switch. On my account the onset reads as time-correlated more than version-correlated. (Caveat: n=1 account, so the absolute date is "when my account first saw it", not necessarily a fleet-wide event. Clean messages still occur after the first loss, so it isn't a hard all-or-nothing switch either.)
Model is ruled out as the proximate trigger as well: all 133 lost messages on my account are
claude-opus-4-8, and the same Opus 4.8 produced 0 lost / 1925 messages from 2026-05-29 through 2026-06-03 on clients 2.1.156–161. Same model + same client, 0% → ~18% overnight.Implication worth weighing for the fix model: if the trigger is at least partly time-correlated rather than purely a client streaming regression, a fix that only changes how the client parses interleaved-thinking streams might be necessary but not sufficient — users on older versions who have not yet been exposed will start losing content the moment exposure reaches them, and the reverse (pinning back to a previously-clean version) does not mitigate after exposure begins, as my June-3-clean / June-4-affected 2.1.161 row shows.
(I had a more detailed analysis posted in two earlier issues that were deleted; happy to share the methodology privately if useful.)
Empirical bisect on my account: regression localized to v2.1.160 → v2.1.161 (and persists through current latest v2.1.167)
Following up on my date-onset comment above, I ran a three-version control today: identical realistic workload (text-before-tool_use turns with Bash/Edit/Write/AskUserQuestion calls), same account, same day, Opus 4.8 throughout. Detection: empty-thinking blocks whose signature decodes to reference the narration content channel (signature-verified, so distinguishable from benign thinking-summary blocks).
| client version | narration-lost / total assistant msgs | rate |
|---|---|---|
| 2.1.160 | 0 / 32 | 0% |
| 2.1.161 | 12 / 30 | 40% |
| 2.1.167 (current latest) | 11 / 29 | 37% |
The .160 test had 22 of 32 turns in the trigger pattern (text-before-tool_use, including 7 AskUserQuestion calls). The 0/32 result has p ≈ 0.0008 under a same-rate-as-.161 hypothesis; the .160 vs .161 difference is not workload variance.
Two takeaways:
~/.local/share/claude/versions/2.1.160/) have a usable temporary downgrade path while a proper fix is in progress.(My earlier date-onset comment still stands as an independent signal — the two readings together — date-correlated onset and version-correlated client behavior at the same time — are consistent with a server-side change interacting with a client-side parser regression. Either side moving back to its prior behavior would resolve it; both fix paths are worth considering.)
Thanks all — the data in this thread has changed my read of my own report, so let me consolidate for triage:
The "regression ~2.1.159–2.1.162" framing in my title is probably wrong. @topp's census is decisive: same client (2.1.161), same model (Opus 4.8), 0/426 affected messages on June 3 → 22/124 on June 4, with no client update in between. Onset is time-correlated, not version-correlated — which points server-side, and means pinning back to an older CLI (my suggested workaround) does not actually mitigate.
The symptom is also misdescribed by my title: per @AnnaThyme's analysis (matching #65751), the text block isn't generated-then-dropped at persistence time — it's never emitted at all. The prose lives inside a thinking block: the model quotes the "missing" text verbatim after a full
/exit+ resume, which only thinking-block replay survives, and the prose appears nowhere on disk.So the accurate statement appears to be: since ~2026-06-04, Opus 4.8 with interleaved thinking intermittently composes user-facing prose inside a thinking block and never emits it as a text block when a tool call follows — independent of CLI version.
[thinking, text, thinking, tool_use]shapes that were routine through June 3 have effectively vanished from transcripts; affected turns persist as[thinking, thinking, tool_use].If maintainers agree, this likely wants
area:model(like #65751) rather than a CLI streaming/persistence investigation — though @Oxygen56's accumulator-invariant suggestion (index-addressed blocks + a[thinking, text, thinking, tool_use]round-trip test) would still be worthwhile hardening to rule the client path out conclusively.Confirming @AnnaThyme's workaround from our side: only pre-tool-call prose is affected — content emitted as the final block of a turn, or carried in tool inputs, survives reliably.
I've retitled the issue accordingly (was: "Assistant text blocks not persisted to transcript JSONL when followed by interleaved thinking (regression ~2.1.159–2.1.162)").
🤖 Generated with Claude Code
@ingo-eichhorst — agree with the diagnosis re-framing (text never emitted; lives in the thinking block). One small tension worth flagging though: my v2.1.160 vs v2.1.161 same-day bisect (0/32 vs 12/30 on identical workload, ~3 hours apart) doesn't quite square with "independent of CLI version".
A reconciliation that fits both reads: the trigger is server-side (date-correlated onset June 4) but the server's prose-in-thinking-block emission is gated on something the client signals — a beta header, capability flag, or how the client requests interleaved thinking. Whatever changed in the .160 → .161 release flips the server into the affected behavior. AnnaThyme's data (v2.1.158 unaffected, v2.1.162+ affected) is consistent: anything before the v2.1.159 "thinking blocks modified" fix wasn't sending the trigger either.
If maintainers want to test this: diff the API request (headers + body) that v2.1.160 and v2.1.161 send for an Opus 4.8 interleaved-thinking turn — that's a 1-step localization of where the client-side switch is. Doesn't change the workaround (pin to .160, or end-of-turn-only prose emission), just narrows the investigation surface.
The current title says "not CLI-version-bound" — but a controlled A/B on the same machine, same day (2026-06-06, after the ~06-04 onset), same account/model/server, changing only the client version, points the other way:
Prompt: 4 labeled paragraphs, each followed by a chained file read in the same turn.
| Version | Result |
|---|---|
| 2.1.158 | 5/5 paragraphs persisted ✅ |
| 2.1.165 | 4/4 intermediate paragraphs destroyed ❌ |
| 2.1.167 (latest) | 3/4 destroyed ❌ |
Corroborated by a long session on the same machine that auto-updated mid-flight: 0 losses across 627 tool-bearing turns on 2.1.156–2.1.158, then ~40% on 2.1.165. So the client build is at least a necessary factor, and downgrading to 2.1.158 reliably avoids it (whatever server-side component may also exist).
JSONL signature: turns shaped
[thinking → text → tool_use]are persisted as[thinking(""), thinking(""), tool_use]— the text block is gone and the thinking blocks are stored empty (they carry content on 2.1.158). The API still bills the text and the in-memory history stays intact, so the model "remembers" prose the user never saw.Environment: Windows 11 Pro, native installer, fullscreen TUI, Opus + interleaved thinking, focus view OFF.
Workaround:
claude install 2.1.158+DISABLE_AUTOUPDATER=1(otherwise it re-updates within minutes).Root cause: the
narration_summariesbeta, gated bytengu_pewter_owl_headerConfirmed by static analysis of the shipped binaries. The "second thinking block" that swallows the pre-AUQ text isn't an ordinary thinking block — it's a
narrationblock. Anyone can verify this by base64-decoding the block'ssignaturefield in the JSONL: ordinary reasoning decodes to athinkingtag, the text-swallowing one decodes to anarrationtag.Mechanism (from the binary):
tengu_pewter_owl_headeris ON, the client adds thenarration_summariesbeta (summarize-connector-text-2026-03-13) to the request.narrationblock instead of atextblock.narrationblocks render like thinking (collapsed/transient) and persist with empty content, so the prose is lost from both the TUI and the JSONL.This explains the position correlation noted in #65751 and here: in my sessions, text emitted as the first block after a real user message stays
text(200 vs 2 narration), while text emitted mid-turn after tool calls is ~50/50 — i.e. connector text between tools is exactly what gets converted.Re "not CLI-version-bound" — confirmed at the code level. I pulled the 2.1.163 darwin-arm64 native binary from npm and compared the gate against 2.1.165: the push site (
if (O && gate()) _.push(narration_summaries)) and the gate function are logically identical, both evaluatingtengu_pewter_owl_headerviastatsig(...) || cachedGrowthBookFeatures[...]. So pinning to an older client does not help while the server gate is on — the apparent "version boundary" is just that older clients were only used before the gate flipped (first narration block in my data:2026-06-05T06:31Z).The client-side env override
CLAUDE_CODE_PEWTER_OWL=false(which the gate function checks first) did not stop the narration blocks in practice, which suggests the emission may be driven server-side independently of the beta header — worth confirming internally.Net: this is a data-loss bug (prose gone from the transcript, not just pushed off the viewport), distinct from the earlier rendering issues #23862 / #62493.
Multi-day version×date data from my own transcripts: the loss is server-gated (onset ~Jun 4) and the gate appears keyed on the reported client version — which makes the "downgrade helps / doesn't help" disagreement in this thread consistent
Signature-verified narration-loss (the empty-thinking block whose signature decodes to a
narrationtag, per @Nharu's method), one account, grouped by date × CLI version:| date | version | narration-lost / total msgs |
|---|---|---|
| 2026-05-29 → 06-03 | 2.1.156 | 0 / 1127 |
| 2026-06-02 | 2.1.159 | 0 / 26 |
| 2026-06-02 / 03 / 06 / 07 | 2.1.160 | 0 / 277, 0 / 92, 0 / 82, 0 / 24 |
| 2026-06-03 | 2.1.161 | 0 / 426 |
| 2026-06-04 / 05 / 06 | 2.1.161 | 22 / 124, 50 / 225, 14 / 35 |
Two things this pins down:
This reconciles the thread's apparent contradiction. "Pinning back doesn't help" (mine/@ingo-eichhorst, staying on an on-bucketed version), "downgrade to .158 helps" (@oskar-gm), and "downgrade to .160 helps" (my data above) are all simultaneously true if the trigger is a version-keyed server gate: whether a downgrade helps depends on whether your target version currently sits in the server's off-bucket. The only universally safe floor is ≤2.1.158, which lacks the narration beta entirely — and it still supports Opus 4.8 including the 1M-context beta, so it's a usable floor, not a feature sacrifice.
Correcting my own earlier comment: my ".160→.161 client-side regression range" framing was wrong — those builds share the narration code. The .160-clean/.161-lossy behavior is the server gate bucketing the two reported versions differently, not a client-code difference.
Fix implication: a client-only interleaved-thinking parser fix is necessary-but-not-sufficient while the server emits narration blocks to clients that drop them; the durable fix is server-side (gate off, or capability-gated on a client that renders/persists
narration), or a client that handles the block instead of storing it empty.Two updates to my earlier comment, both supporting @topp's server-version-keyed-gate reconciliation
1. The beta header is not the trigger — now confirmed, not just suspected.
In my earlier comment I noted the
CLAUDE_CODE_PEWTER_OWL=falseoverride "did not stop the narration blocks... suggests the emission may be driven server-side independently of the beta header." I've since confirmed that directly: with the override set, the outgoing request no longer carried thenarration_summariesbeta (summarize-connector-text-2026-03-13) in itsanthropic-betaheader — so the client-side suppression worked exactly as written — yetnarrationblocks still appeared in those same sessions. So the server emitsnarrationindependently of whether the client requests the summary beta; the beta opt-in is not what gates the block. That's a direct datapoint for the "server-side gate, not client-requested" read.2. Correcting my own "pinning to an older client does not help."
That was over-generalized from the 2.1.163 binary alone (which carries the beta code and happened to be on-bucket). It does not hold for older floors.
stringsover the 2.1.158 native binary returns zero matches for bothnarration_summariesandpewter_owl_header— the beta is entirely absent from that build, consistent with @oskar-gm / @AnnaThyme's behavioral 158-clean and @topp's census. So ≤2.1.158 is a genuine floor at the code level, while 163 is not — the "downgrade doesn't help" caveat applies only to versions that still ship the beta and sit on-bucket.Corroborating @topp's "still supports Opus 4.8 + 1M" point at the binary level: 2.1.158 ships the model definition inline —
"display_name": "Claude Opus 4.8","id": "claude-opus-4-8","max_input_tokens": 1000000— so the floor is usable, not a feature sacrifice.Net: the durable fix is server-side (gate off, or a client that renders/persists
narrationinstead of storing it empty), as @topp says. But as a stopgap today, ≤2.1.158 is the code-level-verified floor; stripping the beta header client-side is not a viable mitigation, since the server emits the block regardless.Escalation: this graduated to the GA Fable 5 stack on 2026-06-10, and a controlled
local bisect shows the client version string is the deciding input — pinning to 2.1.160
fully avoids it while still serving Fable 5.
Two new datapoints on top of the root cause already established in this thread (the
narrationblock /summarize-connector-text-2026-03-13beta, gated via thetengu_pewter_owl_*family):1. Now on the GA model, not just the A/B bucket. Through ~2026-06-09 my affected
transcripts carried the deployment tag
fable-v5-ab-high-prod8(the pre-GA A/B). As of2026-06-10, with Fable 5 GA, the dropped blocks are tagged
claude-fable-58— i.e. thenarration emission is now part of the production Fable serving path, not an experiment
bucket. Expect report volume to rise from here (#66816 was filed the morning of GA).
2. Controlled version bisect, one account, same server/days — version string is the
gate. Signature-verified narration loss (empty thinking block whose signature decodes to
a
narrationtag), grouped by CLI version over 7 days:| CLI version | sessions | narration-lost |
|---|---|---|
| 2.1.160 (pure) | 10 | 0 (large samples, e.g. 34 text blocks/session, 0 lost) |
| 2.1.162 / .165 / .167 / .168 / .170 | many | every session affected (5–37 blocks lost) |
| mixed (e.g.
2.1.160,2.1.167resume) | — | loses only on the newer-version turns |This matches @topp's earlier same-day .160-vs-.161 bisect. The server decides whether to
emit
narrationbased on the reported client version; 2.1.160 is on the clean side.Confirming the client request flag is NOT the lever (so env overrides don't fix it).
Per the shipped binary,
isPewterOwlHeader()/isPewterOwlTool()/isPewterOwlBrief()all funnel through one gate that (a) honors
CLAUDE_CODE_PEWTER_OWL, (b) checks apewter_owl_modelallow-list against the current model, then (c) a GrowthBook flag orserver-pushed
clientDataCache.pewter_owl_header. SettingCLAUDE_CODE_PEWTER_OWL=falsedoes remove the
summarize-connector-textbeta from the outgoing request — but the serverstill emits
narrationblocks (confirmed by @Nharu upthread). So the emission is gatedserver-side on the version string + account, independent of what the client requests.
Concrete asks (any one closes the user-facing bug):
narrationblocks in the TUI and persist their content to the transcript JSONL(today they render as nothing and persist empty), or
narrationfor interactive sessions until rendering lands, orCLAUDE_CODE_PEWTER_OWLpath is ignored server-side).Impact for the GA user: on current builds (2.1.161–2.1.170) the model's mid-turn prose —
"here's what I found, now checking X" — is invisible on screen and absent from disk; only
the first-after-user-message text and the final reply survive. For orchestration-style
usage where the operator reads the running narration, this makes the latest versions
effectively unusable for following the model's reasoning. Pinning to 2.1.160 is the only
known mitigation, and it still serves Fable 5 (model availability is server-side; the
2.1.160 binary has no Fable strings yet runs
claude-fable-5[1m]fine).Additional evidence from an independent setup (CC 2.1.170, macOS,
claude-fable-5, consumed via--output-format stream-json) — possibly connecting this issue to #67164:In our affected turns the mid-turn prose never arrives as a
textblock at all — it goes down an encrypted narration path:[thinking(empty), tool_use, thinking(empty), tool_use, text(short final)]— the long pre-tool explanation appears nowhere in the file.{"type":"thinking","thinking":"","signature":"..."}. Base64-decoding the signature envelope reveals a plaintext label: genuine thinking blocks contain...claude-fable-5...thinking..., while the blocks corresponding to the lost user-facing prose contain...claude-fable-5...narration....--include-partial-messages: for those narration-flavored blocks the entire lifecycle iscontent_block_start(type=thinking)→ a singlesignature_delta→content_block_stop. No thinking_delta/text_delta ever carries the prose, so for stream-json consumers the content is unrecoverable client-side — not merely dropped at persistence time.claude -p, no customization). Nondeterministic: in one run the first one-line mid-turn status arrived as a normaltextblock while the second became an encrypted narration block — same turn shape, same prompt.Happy to share full stream captures / signature samples if useful.
**Onset corroborated and the loss has since cleared on one account — evidence the gate is account-bucketed, not version-keyed**
Third independent account (macOS, Opus 4.8,
userType=external), signature-decoded narration detection per @Nharu/@kiyor (decode the empty thinking block'ssignature;…thinking…= genuine,…narration…= the swallowed connector text). Census over 141 local transcripts:| CC ver | dates | text-before-tool turns | narration blocks |
|---|---|---|---|
| ≤2.1.160 | →Jun 3 | many | 0 |
| 2.1.161 | Jun 3 eve | 15 | 15 |
| 2.1.162–.168 | Jun 4–7 | ~650 | heavy (221/273/101/68…) |
| 2.1.169 | Jun 9 | 20 | 0 |
| 2.1.170 | Jun 10 | 7 | 0 |
| 2.1.173 | Jun 11–12 | 253 | 0 |
| 2.1.174 | Jun 12 | 19 | 0 |
Onset is sharp at 2.1.161/Jun 3: 2.1.160's last session (ended 17:03) is clean, 2.1.161's first (started 17:01, same afternoon, same account) already loses — corroborating @topp's same-day .160-vs-.161 bisect on a third account.
New: the loss has since cleared for us. 2.1.169–.174 (Jun 9–12) show 0 narration blocks across ~300 text-before-tool_use turns, including a 2.1.173 session with 253 such turns and 0 loss — behaviorally identical to the pre-beta 2.1.158 floor (270/0). Confounds ruled out: it's not absence of the trigger pattern (253 turns), and not usage-mode (
userType=externalon both lossy and clean sessions).This breaks the version-vs-account tie. @spiky02plateau reports 2.1.170 lossy (~Jun 10); our 2.1.170 session (Jun 10 21:32Z) is clean (0/13). Same version, same day, opposite outcome ⇒ the server gate is keyed on account bucket, not just the reported client version — and at least one bucket was flipped off around Jun 8–9. So "pin to ≤2.1.158" and "the version string is the deciding input" both hold only per-bucket; a durable fix still needs the server to stop emitting
narrationto clients that persist it empty (or to render/persist it), as already noted upthread.Follow-up from Windows (native), 2026-06-13 — corroborating the rollback @ingo-eichhorst reported, from a different account and OS.
On my account the gate is now OFF on 2.1.177: a controlled test (labeled pre-tool paragraphs + chained file reads per turn, Opus 4.8 + interleaved thinking), repeated across several runs, persisted everything — 23
textblocks across 19 tool calls, 20/20thinkingblocks with content, and 0narrationblocks (all five[TEST-A..E]labels present).Version × narration census across my local transcripts (signature-decoded per @Nharu's method — field 8 of the
signaturereadsthinkingvsnarration):narration+ 9 empty-thinkingout of 16 empty blocks).So the off-bucket now spans at least macOS (@ingo-eichhorst, 2.1.169–174) and native Windows (here, 2.1.177), across separate accounts and dates — consistent with a server-side rollback rather than a client fix. The durable ask still stands: render/persist
narration, or stop emitting it to clients that store it empty.Corroborating the rollback from a fourth account (macOS, 2.1.177) — under a forced trigger load — and a note that this is "off," not "fixed"
Adding a datapoint to @ingo-eichhorst's and @oskar-gm's rollback reports, from a separate macOS account.
I built a small signature-decode canary (decode the empty thinking block's
signature; field readsthinkingfor genuine reasoning vsnarrationfor the swallowed connector text, per @Nharu / @kiyor) and ran it across my transcripts:| CC version (my account) | sample | narration-lost |
|---|---|---|
| 2.1.161 (Jun 4–6) | 100 assistant msgs | 27 (27%) — historical, pre-rollback |
| 2.1.177 (today, forced-trigger smoke test) | 41 assistant msgs, 20 empty-thinking blocks | 0 |
The 2.1.177 run was a deliberate worst case: Opus 4.8 with interleaved thinking, many
text-before-tool_useturns (the exact trigger shape). 20 benign thinking-summary blocks, 0 narration — on an on-bucket account a chunk of those would have decoded asnarration. So this is a meaningful negative, not absence-of-trigger.This matches the picture upthread: the off-bucket now spans macOS (here + @ingo-eichhorst) and Windows (@oskar-gm), across separate accounts and versions 2.1.169–2.1.177.
Caveat worth stating plainly: this is a server-side rollback, not a client fix. The 2.1.177 binary still ships the narration machinery (the beta strings are present; nothing in the 2.1.159→2.1.177 changelog addresses rendering/persistence), and the gate is account-bucketed and server-controlled — so it can flip back on at any time, especially now that Fable 5 is GA and narration sits in the production serving path (@spiky02plateau). "Currently not emitting on my bucket" ≠ "resolved."
The durable ask from upthread still stands: render
narrationblocks in the TUI and persist their content, or stop emitting them to clients that store them empty. Until one of those lands, every consumer is one bucket-flip away from silent data loss again — so a local canary on the transcript (signature-decode for thenarrationtag) remains the only reliable early warning.The bucket flipped back on — same account as my June-14 "0 on 2.1.177" negative, now emitting again on
claude-fable-5at v2.1.199.This is the scenario I flagged closing my last comment ("server-side rollback, not a client fix… one bucket-flip away from silent data loss again… a local canary remains the only reliable early warning"). It's back, and the canary caught it.
Signature-verified narration loss (empty thinking block whose
signaturedecodes to anarrationtag, per @Nharu / @kiyor), same detection I ran on 2.1.177:| CC version (my account) | model | sample | narration-lost (signature-verified) | empty-thinking (upper bound) |
|---|---|---|---|---|
| 2.1.177 (Jun 14, forced-trigger smoke test) | claude-opus-4-8 | 41 msgs | 0 | 20 |
| 2.1.199 (Jul 3, one ordinary session) | claude-fable-5 | 120 msgs | 46 (38%) | 80 |
Not the benign empty-thinking superset — the 46 are signature-decoded to
narration. So on my bucket the gate went from off (mid-June) back to on, and it's now biting on the Fable 5 GA serving path @spiky02plateau warned about — mid-turn prose ("here's what I found, now checking X") invisible on screen and absent from the JSONL.Nothing structural has changed since: v2.1.199 still ships the narration machinery, and no entry in the 2.1.159→2.1.199 changelog addresses rendering or persisting
narrationblocks. So this remains a server-side gate re-emitting a block the client stores empty — the durable ask from upthread is unchanged and, ~3 weeks after the rollback, now clearly needed as a fix rather than a rollback:narrationblocks in the TUI and persist their content to the transcript JSONL, ornarrationto clients that store it empty, orCLAUDE_CODE_PEWTER_OWLis still ignored server-side).Until one of those lands, "currently off on my bucket" was exactly as fragile as it looked in June. Happy to share the (redacted) signature-decode canary if it's useful for others to self-check their bucket.
Datapoint from the VSCode native extension (not CLI TUI), current build, Fable 5 — bucket is ON as of 2026-07-08. Also:
MAX_THINKING_TOKENScap does NOT mitigate.anthropic.claude-codev2.1.204 (darwin-arm64), modelclaude-fable-5, macOS.text; affected turns persist as[thinking, thinking, tool_use]/[thinking, tool_use, …, text(final)]. Turn-final text blocks always survive.env.MAX_THINKING_TOKENS=1024(verified live in-process) shortens genuine thinking but does not reroute the narration — mid-turn prose still lost after the cap.alwaysThinkingEnabled: false; will report whether the narration path survives thinking-off.Filed #75916 before finding this thread — marked it duplicate, consolidating here.
🤖 Generated with Claude Code
Follow-up (promised above):
alwaysThinkingEnabled: falsedoes NOT stop the narration path — and it produces the cleanest isolation of the mechanism yet.Setup: extension v2.1.204,
claude-fable-5, settingsalwaysThinkingEnabled: false+env.MAX_THINKING_TOKENS=1024, both verified live in the relaunched process. Controlled canary turn: one paragraph → tool call → second paragraph → tool call.Result:
textblock.thinking-typed block — in a session with thinking disabled, so no genuine reasoning block should exist at all. Signature decode (per @Nharu/@kiyor's method) readsnarration.So with thinking off, every
thinking-typed block that appears is the narration thief — no genuine-thinking false positives. Useful as a high-precision detector, but as a mitigation it's a negative: the connector-text emission is independent of the client's thinking configuration, consistent with the server-side gating established in this thread.Client-side mitigation scoreboard from our testing: beta-header suppression ❌ (earlier in thread), thinking-budget cap ❌, thinking disabled ❌. Version pin to 2.1.160 remains the only reported client-side avoidance.
🤖 Generated with Claude Code
This bug has existed for over a month. Fable just drops messages en masse, and Anthropic doesn't care? Do they even use this product? What is going on?
Datapoint: Windows CLI 2.1.206 (Cursor integrated terminal),
claude-fable-5— bucket is ON as of 2026-07-10. Two AskUserQuestion-adjacent deliverables lost in one session; session-data fingerprint matches OP (emission-side, nothing to recover).claude-fable-5(subscription login).AskUserQuestioncall in the same turn, never rendered — the user saw only the question dialog both times, and had to decide from the dialog fields alone..jsonlevidence: neither message exists as an assistanttextblock anywhere in the file (distinctive phrases from both messages grep to 0 outsidetool_use/tool_resultentries). The session contains 26/26 empty, plaintext-free, signature-onlythinkingblocks. So this is the emission-side failure described here — NOT the display-drop variant of #76032, where the text is still greppable in the session data. Nothing is recoverable.[thinking, tool_use, …, text(final)]observation that only turn-final blocks survive.AskUserQuestionespecially often for us — decision-heavy workflows are hit worst, because the model naturally writes its explanation immediately before opening the dialog, which is exactly the pre-tool position this bug eats. A PreToolUse hook reading the transcript cannot even detect it (the prose never reaches the transcript).This is definitely something they should have caught by some regression tests.
This is pathetic. How can this go on for over a month without a proper response or fix?