Pre-tool-call assistant text never emitted (prose stays in thinking) — onset ~2026-06-04, not CLI-version-bound

Open 💬 21 comments Opened Jun 5, 2026 by ingo-eichhorst

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

  1. 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).
  2. Note an assistant prose message on screen, then grep the session JSONL for its exact text.
  3. 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.

View original on GitHub ↗

21 Comments

Oxygen56 · 1 month ago

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:

message_start
  content_block_start  {type: "thinking", index: 0}
  content_block_delta  {type: "thinking_delta", thinking: "..."}
  ...
  content_block_stop   {index: 0}

  content_block_start  {type: "text", index: 1}
  content_block_delta  {type: "text_delta", text: "..."}
  ...
  content_block_stop   {index: 1}

  content_block_start  {type: "thinking", index: 2}
  content_block_delta  {type: "thinking_delta", thinking: "..."}
  ...
  content_block_stop   {index: 2}

  content_block_start  {type: "tool_use", index: 3}
  ...
  content_block_stop   {index: 3}
message_stop

Claude Code accumulates these events into message.content[] arrays. On message_stop, the full content array is written as a single JSONL assistant entry with all content blocks nested inside message.content.

Evidence from transcript analysis: in v2.1.158 sessions, turns with shape [thinking, text, tool_use] produce a JSONL entry whose message.content is [{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:

Fixed an issue when using Opus 4.8 where thinking blocks were modified, leading to API errors.

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:

  • Overwrite it, replacing the accumulated state that should have included the text block, or
  • Merge into it, but the merge path never inserts the text block that was at index 1

In pseudocode, a buggy implementation might look like:

// BUG: blocks are stored keyed by type, not by stream index
const blocksByType = new Map();

function onContentBlockStop(event, accumulatedContent) {
  if (event.type === 'thinking') {
    // v2.1.159 fix: coalesce thinking blocks (solves duplicate-thinking error)
    const existing = blocksByType.get('thinking');
    if (existing) {
      existing.thinking += accumulatedContent;  // merges but drops text in between
      return;
    }
  }
  blocksByType.set(event.type, { type: event.type, ...accumulatedContent });
}

function onMessageStop() {
  // BUG: content only has 3 entries [thinking, thinking, tool_use]
  // instead of 4 entries [thinking, text, thinking, tool_use]
  return [...blocksByType.values()];
}

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_start of type thinking arrives. 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 assistant lines 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:

  • Each assistant JSONL entry contains a message.content array
  • Content arrays are typically single-block: [{type: "thinking"}], [{type: "text"}], or [{type: "tool_use"}]
  • Multi-block entries like [{type: "thinking"}, {type: "tool_use"}] occur (1 case in 1728 messages)
  • Zero occurrences of [{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 pattern

This 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:

  1. Track blocks by stream index (the index field on content_block_start/content_block_stop), not by block type
  2. Flush the per-block accumulator on content_block_stop before starting the next block, so text accumulated for block index 1 is committed to the content array before block index 2 (second thinking) begins
  3. Do not coalesce non-adjacent thinking blocks -- two thinking blocks separated by a text block are NOT adjacent and must remain separate entries in the content array. Only coalesce when index values are consecutive AND both are thinking type
  4. Add a unit test that simulates the streaming event sequence [thinking, text, thinking, tool_use] and asserts that all four blocks appear in the persisted content array with correct types and content

The key invariant to preserve: content[stream_index].type === event_type_at_that_index for every content_block_start/content_block_stop pair received during streaming.

AnnaThyme · 1 month ago

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→thinking adjacency:

| 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"):

  1. We probed every local store for the missing prose using distinctive verbatim phrases — session .jsonl, per-session subdirectory, the ~/.claude tree, ~/.claude.json, AppData caches — zero hits (tool-input self-contamination accounted for). No text block exists anywhere on disk.
  2. Yet the model retains the "missing" prose: in later turns — including after a full /exit and resume, i.e. across a process restart — it quotes the lost paragraphs verbatim. Local thinking storage is signature-only (empty thinking content 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.
  3. Same model on CC 2.1.158 (May 31) persisted [thinking, text, tool_use(AskUserQuestion)] intact (3 examples, 339–1099-char texts); thinking→thinking never 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→tool messages 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

topp · 1 month ago

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.)

topp · 1 month ago

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:

  • The client-side regression range is .160 → .161, narrowing where to look.
  • "Bug fixes and reliability improvements" in 2.1.163–.167 have not addressed this. Users with the v2.1.160 binary still on disk (~/.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.)

ingo-eichhorst · 1 month ago

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

topp · 1 month ago

@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.

oskar-gm · 1 month ago

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).

Nharu · 1 month ago

Root cause: the narration_summaries beta, gated by tengu_pewter_owl_header

Confirmed 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 narration block. Anyone can verify this by base64-decoding the block's signature field in the JSONL: ordinary reasoning decodes to a thinking tag, the text-swallowing one decodes to a narration tag.

Mechanism (from the binary):

  • When the statsig gate tengu_pewter_owl_header is ON, the client adds the narration_summaries beta (summarize-connector-text-2026-03-13) to the request.
  • The model then emits "connector text" (the prose it would normally put before a tool call) as a narration block instead of a text block.
  • narration blocks 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 evaluating tengu_pewter_owl_header via statsig(...) || 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.

topp · 1 month ago

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 narration tag, 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:

  1. Onset is a point in time (~Jun 4), not a client update. The same 2.1.161 build was 0/426 on Jun 3 and then lossy from Jun 4 on — no client change on my side in between.
  1. Post-onset, same account, same days: 2.1.160 stays clean while 2.1.161 loses. That split is stable across four days. Since these two builds carry the same narration code (consistent with @Nharu's .163↔.165 comparison, and with the beta having been introduced around 2.1.159 — @Oxygen56's changelog suspect, with @oskar-gm and @AnnaThyme showing 2.1.158 behaviorally clean), a client-side code regression cannot explain .160 ≠ .161. A server-side gate evaluated against the reported client version can.

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.

Nharu · 1 month ago

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=false override "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 the narration_summaries beta (summarize-connector-text-2026-03-13) in its anthropic-beta header — so the client-side suppression worked exactly as written — yet narration blocks still appeared in those same sessions. So the server emits narration independently 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. strings over the 2.1.158 native binary returns zero matches for both narration_summaries and pewter_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 narration instead 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.

spiky02plateau · 1 month ago

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
narration block / summarize-connector-text-2026-03-13 beta, gated via the
tengu_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 of
2026-06-10, with Fable 5 GA, the dropped blocks are tagged claude-fable-58 — i.e. the
narration 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 narration tag), 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.167 resume) | — | loses only on the newer-version turns |

This matches @topp's earlier same-day .160-vs-.161 bisect. The server decides whether to
emit narration based 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 a
pewter_owl_model allow-list against the current model, then (c) a GrowthBook flag or
server-pushed clientDataCache.pewter_owl_header. Setting CLAUDE_CODE_PEWTER_OWL=false
does remove the summarize-connector-text beta from the outgoing request — but the server
still emits narration blocks (confirmed by @Nharu upthread). So the emission is gated
server-side on the version string + account, independent of what the client requests.

Concrete asks (any one closes the user-facing bug):

  • Render narration blocks in the TUI and persist their content to the transcript JSONL

(today they render as nothing and persist empty), or

  • Stop emitting narration for interactive sessions until rendering lands, or
  • Provide a client-honored opt-out that the server actually respects (the current

CLAUDE_CODE_PEWTER_OWL path 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).

kiyor · 1 month ago

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 text block at all — it goes down an encrypted narration path:

  1. Affected turns persist to JSONL as [thinking(empty), tool_use, thinking(empty), tool_use, text(short final)] — the long pre-tool explanation appears nowhere in the file.
  2. The empty blocks are {"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....
  3. We captured the raw stream with --include-partial-messages: for those narration-flavored blocks the entire lifecycle is content_block_start(type=thinking) → a single signature_deltacontent_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.
  4. Reproduces with the stock system prompt (bare claude -p, no customization). Nondeterministic: in one run the first one-line mid-turn status arrived as a normal text block while the second became an encrypted narration block — same turn shape, same prompt.
  5. The model's own context does retain the content (later turns reference it accurately), so it never re-states it — the loss is silent and one-sided: the user never saw it, the model believes they did.

Happy to share full stream captures / signature samples if useful.

ingo-eichhorst · 1 month ago

**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's signature; …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=external on 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 narration to clients that persist it empty (or to render/persist it), as already noted upthread.

oskar-gm · 1 month ago

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 text blocks across 19 tool calls, 20/20 thinking blocks with content, and 0 narration blocks (all five [TEST-A..E] labels present).

Version × narration census across my local transcripts (signature-decoded per @Nharu's method — field 8 of the signature reads thinking vs narration):

  • 2.1.165 / 2.1.167: narration present — real pre-tool prose lost (e.g. one session: 7 narration + 9 empty-thinking out of 16 empty blocks).
  • 2.1.158: 0 narration across dozens of sessions (the beta strings are absent from that binary).
  • 2.1.177 (today): 0 narration.

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.

topp · 1 month ago

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 reads thinking for genuine reasoning vs narration for 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_use turns (the exact trigger shape). 20 benign thinking-summary blocks, 0 narration — on an on-bucket account a chunk of those would have decoded as narration. 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 narration blocks 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 the narration tag) remains the only reliable early warning.

topp · 13 days ago

The bucket flipped back on — same account as my June-14 "0 on 2.1.177" negative, now emitting again on claude-fable-5 at 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 signature decodes to a narration tag, 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 narration blocks. 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:

  • render narration blocks in the TUI and persist their content to the transcript JSONL, or
  • stop emitting narration to clients that store it empty, or
  • provide a client-honored opt-out the server actually respects (CLAUDE_CODE_PEWTER_OWL is 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.

gearghost-tyler · 7 days ago

Datapoint from the VSCode native extension (not CLI TUI), current build, Fable 5 — bucket is ON as of 2026-07-08. Also: MAX_THINKING_TOKENS cap does NOT mitigate.

  • Extension anthropic.claude-code v2.1.204 (darwin-arm64), model claude-fable-5, macOS.
  • Symptom identical to OP/@kiyor: mid-turn pre-tool prose never arrives as text; affected turns persist as [thinking, thinking, tool_use] / [thinking, tool_use, …, text(final)]. Turn-final text blocks always survive.
  • 403/403 thinking blocks in one long session are empty-plaintext + signature only.
  • New mitigation negative: capping the thinking budget via settings 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.
  • Currently A/B-testing alwaysThinkingEnabled: false; will report whether the narration path survives thinking-off.
  • In the native extension UI this is brutal: the panel renders "Thought for Ns → tool box" sequences with no words at all, and (as established above) the blocks decode to nothing user-recoverable.

Filed #75916 before finding this thread — marked it duplicate, consolidating here.

🤖 Generated with Claude Code

gearghost-tyler · 7 days ago

Follow-up (promised above): alwaysThinkingEnabled: false does NOT stop the narration path — and it produces the cleanest isolation of the mechanism yet.

Setup: extension v2.1.204, claude-fable-5, settings alwaysThinkingEnabled: 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:

  • Turn-opening paragraph survived as a text block.
  • Between-tools paragraph was swallowed by a 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) reads narration.

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

benkingcode · 7 days ago

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?

CPaulBeroe · 5 days ago

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 Code CLI 2.1.206, Windows 11, Cursor integrated terminal, model claude-fable-5 (subscription login).
  • Two long deliverables (~6–8k chars each, markdown research tables), each written immediately before an AskUserQuestion call in the same turn, never rendered — the user saw only the question dialog both times, and had to decide from the dialog fields alone.
  • Session .jsonl evidence: neither message exists as an assistant text block anywhere in the file (distinctive phrases from both messages grep to 0 outside tool_use/tool_result entries). The session contains 26/26 empty, plaintext-free, signature-only thinking blocks. 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.
  • Turn-final text in the same session (no tool call after it) rendered and persisted every time — consistent with @gearghost-tyler's [thinking, tool_use, …, text(final)] observation that only turn-final blocks survive.
  • Corroborates "onset ~2026-06-04, not CLI-version-bound": this user logged 5 incidents of "long prose + AskUserQuestion in one turn → prose gone" beginning 2026-06-04/06 (at the time attributed to the #24691/#23862 TUI-render family; in light of this thread, some or all were likely this bug). Recurrence on 2.1.206 today means it is still live on the newest CLI.
  • Workflow-impact note: the loss co-occurs with AskUserQuestion especially 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).
nApucco · 2 days ago
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?

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?