Assistant text blocks silently dropped when followed by more thinking in the same turn — never rendered, missing from transcript JSONL (2.1.201, adaptive thinking)

Status Open
Reported on v2.1.201
Maintainer reply ✓ Yes — bcherny
Activity 33 comments · opened Jul 4, 2026
💡 Likely answer: A maintainer (bcherny, collaborator) responded on this thread — see the highlighted reply below.

Environment: Claude Code 2.1.201, macOS (darwin 25.5), model claude-fable-5 (interleaved/adaptive thinking), terminal TUI (also reproduced in a claude.ai/code bridged child session, CLAUDE_CODE_CHILD_SESSION=1).

Summary: Assistant text emitted mid-turn — a text block followed by another thinking block and/or further tool calls in the same turn — is frequently neither rendered on any surface nor persisted to the session transcript JSONL. The affected assistant message is persisted as [thinking, thinking, tool_use] with the text block absent. Text in the shape [thinking, text, tool_use] (text immediately preceding the tool call) and end-of-turn text render and persist normally.

Impact: The model believes it has shown the user content and asks follow-ups like "Did you review the list printed above?" when nothing was ever displayed — from the user's side, "the text is being swallowed." In one measured session, 12 of 13 mid-turn status texts vanished from both the display and the JSONL. This is easily mistaken for #23862 / #58207 (AskUserQuestion dialog overlay hiding preceding text) but is distinct: the text is absent from the transcript JSONL itself, so the loss occurs upstream of rendering, and it affects text before any tool call, not just AskUserQuestion.

Evidence (session JSONLs available on request):

  • Incident session: message-ID-grouped JSONL entries show single API messages persisted as [thinking(signature-only), thinking(signature-only), tool_use:AskUserQuestion] whose tool input references "printed above" / "reprinted above" content that exists nowhere in the file (verified by full-file grep for the referenced phrases — they occur only inside the tool inputs and their tool_results).
  • Control in the same session: an ask persisted as [thinking, text(3817), tool_use:AskUserQuestion] — the text both persisted and rendered (user confirmed reviewing it).
  • Diagnostic session: the assistant demonstrably emitted ~13 short texts between tool calls; exactly 1 persisted (the one shaped [thinking, text, tool_use]); the user confirmed seeing none of them render.

Repro sketch (stochastic — depends on the model interleaving thinking after the text): With a thinking-enabled model, prompt: "Think about X, then print a 10-line numbered summary in chat, then reconsider briefly, then run ls — all in one turn." When the model emits thinking → text → thinking → tool_use, observe that the summary neither renders nor appears in ~/.claude/projects/<project>/<session>.jsonl, while the message's thinking and tool_use blocks do.

Open question the local evidence can't settle: whether the text block is dropped harness-side (streamed but discarded) or never surfaced by the API as a visible block — thinking content is signature-only (redacted) in the JSONL, so the two are locally indistinguishable. Either way the harness-visible symptom is the same: user-facing loss of assistant output with no error.

View original on GitHub ↗

33 Comments

mdickinson · 1 month ago

Same as #66960?

jswiderski-pix · 1 month ago

I've also experienced this issue frequently over the past couple weeks. Yes, seems consistent with #66960, but the description here seems a lot more complete and faithful to what I've experienced.

jswiderski-pix · 1 month ago

Disclosure up front: "we" in this comment means myself and Claude Fable 5 — the analysis was conducted and this report written by Fable (running in Claude Code) in collaboration with me. That includes a first-person element in the evidence: some of the verification below comes from the affected model confirming, in-session, which of its own messages were dropped.

TL;DR — this is a critical, ongoing, silent data-loss bug and deserves urgent attention. On claude-fable-5, the TUI and the transcript JSONL are silently destroying the text content of roughly one in four of ALL assistant messages — 744 of 2,936 (25.3%) across every session on two independent machines over the past month, every single day, on current CLI versions. The user never sees the text, it never reaches the transcript (so --resume and compaction lose it permanently), no error is raised anywhere, and given the triggering block shape the loss is deterministic (744/744, zero survivors). The model meanwhile believes it communicated — asking follow-up questions about content the user never received. This is not an intermittent rendering glitch; it is a quarter of the flagship model's output being silently deleted. Accordingly, this issue may also deserve the data-loss label.

Following up on my earlier comment with data. We ran a full-corpus scan of every local session transcript on two independent machines (a macOS host and a Linux VM with its own ~/.claude; 171 + 597 transcript files, 2026-06-09 → 2026-07-10, incidents on CLI 2.1.202–2.1.206, still present on today's 2.1.206). Everything in this report reproduces at scale, the data answers the open question at the end of it — and it shows the drop is deterministic given the block shape, which makes this a silent-data-loss bug rather than an intermittent rendering glitch.

The open question — harness-side drop vs. never-surfaced-by-the-API: it's harness-side (client). The Messages API is stateless — the client re-sends conversation history every turn. In multiple verified incidents, the model later quoted its dropped text verbatim (in one case reproducing a ~2,000-word technical analysis with no new tool calls when the user reported not seeing it; in a live session we ground-truthed 11/11 dropped messages this way). That's only possible if the client's conversation-state accumulator received the text block from the stream and kept re-sending it to the API. So the block arrives and survives in conversation state, but is lost by both the render path and the JSONL persist path. The in-memory/JSONL divergence also means --resume and compaction convert the display bug into permanent loss, since the JSONL is their source of truth.

Deterministic post-hoc detection. Your persisted [thinking, thinking, tool_use] shape generalizes into a scan fingerprint: within a single API response, content blocks are delimited by type transitions, so two thinking blocks can never legitimately be adjacent. Grouping JSONL assistant lines by message.id, any message with a back-to-back thinking pair is a drop event. In both corpora this marker had zero false positives (see the text column below).

Corpus results:

| model | assistant msgs scanned | msgs with adjacent thinking-pair | of those, containing any text |
|---|---|---|---|
| claude-fable-5 | 1,691 + 1,245 | 365 + 379 = 744 | 0 |
| claude-opus-4-8 | 12,994 + 6,005 | 3* | 0 |
| claude-sonnet-5 | 47 + 1,176 | 0 | — |
| claude-sonnet-4-6 | 151 + 138 | 0 | — |

\ all three Opus hits are refusal-fallback continuation turns of requests that started* on Fable (they begin with a fallback block).

Your "12 of 13 mid-turn texts" measurement generalizes: 25% of all Fable tool-bearing messages (365/1,461) on one machine carry the drop signature, continuously across every day of the window — versus zero native occurrences in ~20,000 messages on Opus 4.8 / Sonnet 5 / Sonnet 4.6 over the same period. A hard model-exclusive line, not a gradient.

Deterministic given the shape — the "stochastic" part is only whether the model emits it. A thinking → text → thinking → tool_use message that survived intact would persist as exactly that four-block sequence. Across 2,936 Fable messages on two machines, that surviving shape occurs zero times, while the destroyed residue occurs 744 times — i.e., of every observable emission of the vulnerable shape, 744/744 lost their text (≥99.6% destruction rate at 95% confidence by the rule of three). The control: Opus 4.8 and Sonnet 5 emit the same multi-thinking-with-text shape routinely (350+ instances in our corpora) and it survived every time. So the repro sketch in this issue is stochastic only in getting the model to interleave thinking after the text; once it does, the loss appears to be certain. Combined with the resume/compaction permanence, this warrants treatment as a data-loss bug, not a cosmetic one.

Confirming other specifics of this report:

  • The control shape holds with zero exceptions: [thinking, text, tool_use] (no second thinking) rendered and persisted 100% of the time on both machines; text and adjacent-thinking-pairs never co-occur anywhere in either corpus.
  • Not AskUserQuestion-specific, quantified — per-tool breakdown of 366 signature messages on one machine: Bash 203, TaskUpdate 31, Edit 27, AskUserQuestion 24 (~7%), ToolSearch 20, Read 20, Agent 19, plus a long tail of MCP tools. AskUserQuestion is just where users notice, since a context-less question has a victim — we have 8 corroborated incidents of users answering questions blind across 4 separate sessions.
  • Environment range extended: your macOS/2.1.201 plus our Linux VM and macOS, 2.1.202–2.1.206, terminal TUI. Also present on day one of our Fable usage (2026-06-09).
  • Not related to mid-session /model switches (suspected in #66960's comments): our sessions start natively on claude-fable-5 and show the signature throughout.

Quantification script — run against your own transcripts; any Fable usage should show nonzero counts within a day:

import json, glob, os, collections
adj, tot = collections.Counter(), collections.Counter()
for p in glob.glob(os.path.expanduser('~/.claude/projects/*/*.jsonl')):
    msgs = collections.defaultdict(list)
    for line in open(p):
        try: e = json.loads(line)
        except Exception: continue
        if e.get('type') != 'assistant': continue
        m = e.get('message') or {}
        if not isinstance(m.get('content'), list): continue
        for b in m['content']:
            if isinstance(b, dict): msgs[(m.get('model'), m.get('id'))].append(b.get('type'))
    for (model, _), t in msgs.items():
        tot[model] += 1
        if any(a == b == 'thinking' for a, b in zip(t, t[1:])): adj[model] += 1
for model in sorted(tot, key=lambda k: -adj[k]):
    print(f'{model}: {adj[model]} / {tot[model]} messages with drop signature')

Given the model exclusivity and shape dependence, the likely locus is the TUI/persist stream-assembly mishandling Fable's thinking → text → thinking → tool_use sequence — other models' multi-thinking turns always have surviving content between the blocks (200+ such messages on Opus, all intact). Related UX-impact thread: #66960, whose first comment describes the same signature (including the in-memory retention and JSONL gap).

jswiderski-pix · 1 month ago

I've added this direction to my user-scope CLAUDE.md to try to fix the worst symptom (to me): assistant messages missing before AskUserQuestion tool invocations. This is a steering based approach so probably won't work reliably. In theory, you could try to extend this to steer around ALL dropped assistant messages, but that would be such a radical change to the model behavior that I'm not sure what sort of adverse effects that could have.

## When invoking AskUserQuestion on Fable (text-drop workaround)

When you are Claude Fable (claude-fable-5 — check the model named in your system prompt) and are considering invoking AskUserQuestion, follow these instructions. On any other model, ignore this section entirely and use AskUserQuestion normally.

**Rule: AskUserQuestion must always be the sole content of its message — never paired with same-message text.**

- If the question and its option descriptions are fully self-contained (a quick pick needing no supporting context), invoke AskUserQuestion alone in its own message.
- If the question needs supporting context (findings, options, trade-offs):
  1. End your turn with a text-only message — no tool calls in that message; text-only messages always render. Present the context, then ask the question in prose.
  2. Close with: Answer in prose, or type "q" for the option picker.
  3. If the user replies "q", invoke AskUserQuestion as the sole content of your next message — no accompanying text — with options matching the question you asked. The context is already on screen.

Why: a Claude Code bug (anthropics/claude-code#74260) destroys assistant text that shares a message with a tool call when an interleaved thinking block separates them — the text never renders and never reaches the transcript. On Fable this affects ~25% of tool-bearing messages, so with a same-message AskUserQuestion the question UI renders but the context above it silently vanishes.

A smarter workaround could be a hook when only injects this context for Fable, so it doesn't pollute the context for other models.

jorgenswiderski · 1 month ago

Until this is fixed upstream, I've packaged a more robust bandaid as a plugin:
https://github.com/jorgenswiderski/fable-message-drop-fix

On claude-fable-5 it injects a per-session directive (re-injected after compaction) telling the model to put AskUserQuestion context in a turn-final text-only message — the shape this bug doesn't affect — and a Stop hook then auto-continues the turn so the option picker appears under the intact context. This is still a steering-based fix, but works pretty reliably since Fable is very steerable.

It can't prevent the drop itself (hooks fire after the client has already rendered/persisted the message), so text paired with other tool calls — Bash is the most-affected in my data — is still destroyed. It only reroutes the AskUserQuestion flow, where answering pickers blind hurts most. It also teaches Claude what the bug is so it can identify other kinds of miscommunications (outside of AskUserQuestion) to prevent it feeling like Claude is gaslighting you. It does not try to get Claude to route around the bug at all times (that seems too invasive).

claude plugin marketplace add jorgenswiderski/fable-message-drop-fix
claude plugin install fable-message-drop-fix@fable-message-drop-fix
vbiroshak · 1 month ago

Reproduced on Claude Code 2.1.201, macOS desktop app (Darwin 25.5.0), model claude-fable-5, GUI session.

Confirming the data-loss variant described here, including the persisted signature. In one working session, five of six mid-turn assistant texts (status updates and findings summaries emitted between tool calls) were lost from both the display and the session .jsonl. At each loss point the persisted record shows the assistant message as [thinking, thinking, tool_use] with no text block. The one mid-turn text that survived had the shape [thinking, text, tool_use] (text immediately preceding the tool call, no thinking after it), matching the safe shape noted in the report.

Controlled repro in the same session: a turn deliberately structured as text → tool_use → text → tool_use → text, with each text block numbered. Result: markers 1 and 3 rendered and persisted; marker 2 did not render in the GUI (including with the tool group expanded) and is absent from the .jsonl — no text block of any kind exists between the two tool_use records.

One downstream effect worth flagging: the session .jsonl is the record both the user and the model itself use to audit what was communicated. Because the block is dropped from persistence rather than just display, a post-hoc check of the transcript "proves" the assistant never sent the message — the model is then corrected for failing to communicate, and (in our case) had previously written erroneous notes attributing the silence to rendering, since the corrupted record supports either false conclusion. Display-only loss (#67071) and this variant are very hard for an end user to tell apart without a controlled test.

Note: this comment was written and posted by Claude (Fable 5) at the user's direction, from within the affected session — the model reporting the loss of its own messages.

lumenastrum · 1 month ago

Independent confirmation: deterministic, fable-only, ~3,000 dropped text blocks across 17 days (Windows, 2.1.214)

Confirming on a second platform and a newer version:

  • Claude Code 2.1.214, Windows 11 (win32 10.0.26200), terminal TUI
  • Model claude-fable-5, adaptive/interleaved thinking enabled

Rather than a live repro, I scanned every session transcript on this machine (449 JSONL files, ~575 MB, 2026-07-01 → 2026-07-17) for the persistence fingerprint described in the OP. Claude Code persists one JSONL line per content block, so the scanner reconstructs each assistant message by grouping consecutive type: "assistant" lines sharing message.id, then examines block-type adjacency inside each reconstructed message.

| model | messages | msgs w/ adjacent thinking, thinking | surviving text → thinking pairs | surviving text → tool_use pairs |
|---|---|---|---|---|
| claude-fable-5 | 10,334 | 2,964 (28.7%) | 0 | 1,721 |
| claude-opus-4-8 | 9,498 | 0 | 0 | 5,552 |
| sonnet-5 / sonnet-4-6 / haiku-4-5 | 444 | 0 | 0 | 176 |

Why adjacent thinking blocks are a valid fingerprint of a dropped block: the API merges contiguous thinking deltas into a single block, so two adjacent thinking blocks in a persisted message imply a block between them was removed. A removed tool_use would orphan its tool_result and hard-error the next request; all of these sessions continued normally, so the removed blocks were text.

Observations:

  1. The drop looks deterministic, not intermittent. Across 10,334 fable messages there is not a single surviving text → thinking sequence, against 2,964 fingerprints of the removed shape. Every text block that did survive sits either immediately before a tool_use or turn-final — exactly matching the OP's control observation that [thinking, text, tool_use] persists normally.
  2. Fable-only. 9,498 claude-opus-4-8 messages from the same machine and period show zero fingerprints, which also rules out generic interrupt/retry artifacts in the transcript writer.
  3. Constant since the first fable session on this machine (2026-07-01): 30–390 fingerprints per day, every single day, including the day of writing.
  4. Incidental live repro: between two scans run minutes apart, the fable fingerprint count grew by 2 — contributed by the very session running the analysis.
  5. The dropped text is unrecoverable from disk — it never reaches the transcript. Persisted thinking blocks carry no recoverable text either, and grepping for content referenced by later turns finds it only inside tool inputs, matching the OP.

Happy to provide the full per-day distribution or re-run the scan against a candidate fix.

<details>
<summary>Scanner (Node ≥18, no deps — scans <code>~/.claude/projects</code> recursively)</summary>

import { createReadStream, readdirSync } from 'node:fs';
import { join } from 'node:path';
import readline from 'node:readline';

const ROOT = join(process.env.USERPROFILE || process.env.HOME, '.claude', 'projects');
function* files(d) {
  for (const e of readdirSync(d, { withFileTypes: true })) {
    const p = join(d, e.name);
    if (e.isDirectory()) yield* files(p);
    else if (e.name.endsWith('.jsonl')) yield p;
  }
}

const perModel = {};
const stats = m => (perModel[m] ??= { messages: 0, thinkThinkMsgs: 0, textThenToolUse: 0, textThenThinking: 0 });

function finalize(g) {
  if (!g) return;
  const s = stats(g.model || 'unknown');
  s.messages++;
  let tt = false;
  for (let i = 0; i < g.blocks.length - 1; i++) {
    const [a, b] = [g.blocks[i], g.blocks[i + 1]];
    if (a === 'thinking' && b === 'thinking') tt = true;
    if (a === 'text' && b === 'tool_use') s.textThenToolUse++;
    if (a === 'text' && b === 'thinking') s.textThenThinking++;
  }
  if (tt) s.thinkThinkMsgs++;
}

for (const f of files(ROOT)) {
  const rl = readline.createInterface({ input: createReadStream(f), crlfDelay: Infinity });
  let g = null;
  for await (const line of rl) {
    let o; try { o = JSON.parse(line); } catch { continue; }
    if (o.type !== 'assistant' || !o.message?.content) { finalize(g); g = null; continue; }
    const types = o.message.content.map(b => b?.type).filter(Boolean);
    if (g && g.id === o.message.id) g.blocks.push(...types);
    else { finalize(g); g = { id: o.message.id, model: o.message.model, blocks: types }; }
  }
  finalize(g);
}

for (const [m, s] of Object.entries(perModel).sort((a, b) => b[1].messages - a[1].messages))
  console.log(m, JSON.stringify(s));

</details>

BFJ-Concerns · 1 month ago

Adding two more machines' worth of data.

Ran the scan script from this issue across all local sessions on both my machines:

| Machine | Platform | Claude Code | Drop-signature messages / all Fable messages | Per-message rate | Missing mid-turn blocks | Surviving mid-turn blocks | Block loss rate |
| -------- | ---------------------- | ----------- | -------------------------------------------- | ---------------- | ----------------------- | ------------------------- | --------------- |
| A | CachyOS (Arch), x86_64 | 2.1.214 | 5,332 / 19,321 | 27.6% | 5,332 | 3,434 | 60.8% |
| B | Debian 13 LXC, x86_64 | 2.1.202 | 1,709 / 6,441 | 26.5% | 1,709 | 1,182 | 59.1% |
| Combined | | | 7,041 / 25,762 | 27.3% | 7,041 | 4,616 | 60.4% |

Controls in the same pass: ~73,000 messages across Opus 4.8, Opus 4.7, Sonnet 5, and Haiku 4.5, zero hits.

A commenter on my Reddit thread about this (u/Mandor75) suggested a framing I think is worth adopting here: the per-message rate understates the loss, because most messages carry no mid-turn text at all and pad the denominator. Counting text blocks instead, my corpus has 7,041 missing mid-turn blocks against 4,616 surviving ones, a 60.4% block loss rate (60.8% and 59.1% on the two machines separately). Their own run came out at 58%, so the block-level rate looks as consistent across users as the per-message rate has been. Consistent with the shape analysis in the issue body, every surviving mid-turn block is the immediately-before-a-tool-call kind.

Both figures are still floors: the variant where text is dropped between two tool calls leaves no thinking/thinking signature, so this scan cannot count it.

Reddit thread with more people's numbers, for reference: https://www.reddit.com/r/ClaudeAI/comments/1uzec0l/what_everyone_calls_fable_being_quiet_is/

v-tl · 1 month ago

Based on the wire-level check (via an observe-only local proxy), I believe the missing text is not being dropped by Claude Code - it never leaves the model as text. When Fable goes to say something to you mid-turn (most often right before asking a question), that prose appears to get absorbed into its thinking stream instead of being emitted as a visible text block - most likely riding, near-verbatim, inside one of the two (highly likely second based on stats) adjacent encrypted thinking blocks that form this issue's fingerprint. There's nothing for client to render or persist - from the client's perspective the words were never said, while the model believes it said them and will even "quote them back" convincingly if you ask.
A controlled test of asking the model to produce "thinking, text, thinking, tool_use" shape twice on 2.1.214 was positive, the text block was streamed and persisted verbatim in the transcript JSONL.

Geeebo · 1 month ago

Adding numbers from another machine (same account as the r/ClaudeAI comment the OP's edit references — u/Mandor75).

Ran the message-level script from this issue as posted: 25.5% — 1,776 / 6,964 claude-fable-5 messages with the drop signature. Controls: claude-opus-4-6: 1 / 47,620. claude-opus-4-8: 0 / 9,966. Zero hits on sonnet-5, opus-4-7, haiku, and everything else in ~60k non-Fable messages.

The per-message rate understates the practical loss, because most messages contain no mid-turn text to lose (pure tool-call messages, or a single end-of-turn block) and they all pad the denominator. Counting blocks instead, on the same signature: each thinking/thinking adjacency = at least one destroyed text block, versus text blocks that survived by position:

dropped mid-turn text (min):   1810
surviving mid-turn text:       1300
surviving end-of-turn text:    1525
mid-turn loss:                 1810/3110 = 58.2%

So on this machine ~58% of the mid-turn prose Fable writes never arrives. That matches the OP's rescan (60.4% across two machines) and my own subjective estimate from daily use before measuring anything.

Block-count variant, for anyone who wants to reproduce:

import json, glob, os, collections
craters = mid_survive = end_survive = 0
for p in glob.glob(os.path.expanduser('~/.claude/projects/*/*.jsonl')):
    msgs = collections.defaultdict(list)
    for line in open(p, encoding='utf-8', errors='replace'):
        try: e = json.loads(line)
        except Exception: continue
        if e.get('type') != 'assistant': continue
        m = e.get('message') or {}
        if not isinstance(m.get('content'), list): continue
        if m.get('model') != 'claude-fable-5': continue
        for b in m['content']:
            if isinstance(b, dict): msgs[m.get('id')].append(b.get('type'))
    for _, t in msgs.items():
        craters += sum(1 for a, b in zip(t, t[1:]) if a == b == 'thinking')
        for i, bt in enumerate(t):
            if bt == 'text':
                if i == len(t) - 1: end_survive += 1
                else: mid_survive += 1
print(f"dropped (min): {craters}, mid-turn survived: {mid_survive}, end-turn survived: {end_survive}")
print(f"mid-turn loss floor: {craters}/{craters+mid_survive} = {100*craters/(craters+mid_survive):.1f}%")

Both numbers are floors: a crater counts once regardless of how many blocks it swallowed, and the variant reported above (text dropped between two tool_use blocks) leaves no thinking/thinking adjacency, so neither count can see it.

Environment: Windows 11, VS Code extension 2.1.195, single machine.

kotok9 · 1 month ago

Another datapoint, Linux (Fedora 44), from scanning my local ~/.claude/projects with the adjacent-thinking signature from @jswiderski-pix's script (plus @Geeebo's mid-turn/end-of-turn split). 302 session files, grouped into API turns by message.id.

| model | turns | turns w/ drop signature | per-message | mid-turn text lost |
|---|---|---|---|---|
| claude-fable-5 | 1048 | 218 | 20.8% | 58.4% (218 craters vs 155 surviving mid-turn texts) |
| claude-opus-4-8 (control) | 4511 | 1 | 0.02% | ~0% (2674 survived) |
| claude-opus-4-7 | 63 | 0 | 0% | 0% |
| claude-sonnet-5 | 138 | 0 | 0% | 0% |
| claude-haiku-4-5 | 583 | 0 | 0% | 0% |

Sanity check: all 218 Fable "craters" are turns whose two adjacent thinking blocks carry distinct signature values — two genuinely separate thinking blocks with a hole between them, not one record split in two. Opus-4.8 has exactly one such turn in 4511.

So my per-message rate (20.8%) sits a bit under the ~25% reported here, but the block-level rate (58.4%) matches @Geeebo / the OP almost exactly.

One angle I haven't seen posted — broken down by engine version, the Fable rate is clearly declining:

Fable-5, share of turns with the drop signature, by Claude Code version:
  2.1.170   77%  (n=26)
  2.1.181   30%  (n=30)
  2.1.187   21%  (n=768)
  2.1.202   13%  (n=218)
  2.1.209    0%  (n=6)

(Caveat: per-message rate also depends on how much mid-turn text the model writes, so this isn't purely "it's being fixed" — but between 2.1.187 and 2.1.202, both large samples, the rate roughly halved.)

as04-git · 1 month ago

Disclosure: written by Claude Fable 5 from inside an affected session, at the user's direction.

Adding another machine's numbers (WSL2 Linux, x86_64), a controlled repro on 2.1.215 (current latest as of today), and a detection/recovery hook that takes a different approach from the steering-based workarounds above.

Corpus scan (script from this thread, message-level + block-level):

| model | messages | drop-signature msgs | rate |
|---|---|---|---|
| claude-fable-5 | 2,345 | 541 | 23.1% |
| claude-opus-4-8 | 4,740 | 1* | ~0% |
| opus-4-6 / 4-7, sonnet-5 / 4-6, haiku-4-5 | ~1,430 | 0 | 0% |

Mid-turn text loss floor: 541 / 962 = 56.2%, consistent with the 58–60% reported above. Zero surviving text → thinking pairs anywhere in the corpus. Continuous since first Fable use on this machine (2026-07-02). \*The single opus hit matches the fallback-continuation false-positive class already described.

Per-version rates on this machine — consistent with the decline @kotok9 observed: 2.1.198 → 25.0% (n=707), 2.1.201 → 22.6% (n=1,585), 2.1.207 → 9.4% (n=53).

But: live controlled repro on 2.1.215, immediately after updating. In one turn I (the model) deliberately emitted two marked mid-turn texts, each followed by deliberate thinking before a Bash call. Result:

  • MARKER-ALPHA landed in the safe shape [thinking, text, tool_use] (no thinking interleaved after the text) — persisted and rendered, user confirmed seeing it.
  • MARKER-BRAVO hit the vulnerable shape — persisted as [thinking, thinking, tool_use], no text block in the JSONL, user confirmed it never rendered.

So the declining per-version rate should not be read as "nearly fixed": given the vulnerable block shape, destruction was still total on 2.1.215. The decline may reflect the model emitting the shape less often, not the loss path closing. This is also consistent with @v-tl's wire-level observation that the text never leaves the API as a text block — if so, no client version can fix it.

Workaround: detection + same-turn recovery, rather than steering-only. The existing plugin reroutes AskUserQuestion; the approach below instead catches every fingerprinted drop, on any tool, the moment it hits the transcript. A global PostToolUse hook incrementally scans the session JSONL (offset-tracked, so it's cheap) for new adjacent-thinking fingerprints on fable messages. On a hit it (a) shows the user a ⚠ text-drop notice and (b) injects additionalContext ordering the model to restate the lost text in its end-of-turn message — which works because the model still holds the content in conversation state (or can recover it from its own thinking), and turn-final text always survives. First run per session only baselines, so resumed sessions don't dump historical warnings.

Known blind spot, inherited from the fingerprint itself: text dropped between two tool_use blocks leaves no adjacency and can't be detected this way. A short CLAUDE.md rule (substantive prose only turn-final; never pair AskUserQuestion with same-message context) covers that variant probabilistically.

Verified working live: the hook fired and baselined the session it was installed in without a restart.

<details>
<summary><code>~/.claude/hooks/fable-drop-detect.py</code> (Python 3, stdlib only) + settings wiring</summary>

#!/usr/bin/env python3
"""PostToolUse hook: detect Fable mid-turn text drops (anthropics/claude-code#74260).

Fingerprint: two adjacent `thinking` blocks within one assistant message
(message.id), which cannot occur legitimately. Incremental scan per tool call;
on a new hit, tells the user and instructs the model to restate end-of-turn.
First run in a session only baselines (historical drops are not re-reported).
"""
import json
import os
import sys

STATE_DIR = os.path.expanduser("~/.claude/hooks/.fable-drop-state")


def main():
    try:
        inp = json.load(sys.stdin)
    except Exception:
        return
    tp = inp.get("transcript_path")
    sid = inp.get("session_id") or "unknown"
    if not tp or not os.path.exists(tp):
        return

    os.makedirs(STATE_DIR, exist_ok=True)
    sf = os.path.join(STATE_DIR, sid + ".json")
    state = {"offset": 0, "last_id": None, "last_block": None}
    fresh = True
    try:
        with open(sf) as fh:
            state.update(json.load(fh))
        fresh = False
    except Exception:
        pass

    new_drops = 0
    with open(tp, "rb") as f:
        f.seek(state["offset"])
        while True:
            pos = f.tell()
            raw = f.readline()
            if not raw:
                break
            if not raw.endswith(b"\n"):  # partial line still being written
                f.seek(pos)
                break
            try:
                e = json.loads(raw.decode("utf-8", "replace"))
            except Exception:
                continue
            if e.get("type") != "assistant":
                continue
            m = e.get("message") or {}
            content = m.get("content")
            if not isinstance(content, list):
                continue
            model = m.get("model") or ""
            mid = m.get("id")
            for b in content:
                if not isinstance(b, dict):
                    continue
                bt = b.get("type")
                if (
                    bt == "thinking"
                    and state["last_block"] == "thinking"
                    and state["last_id"] == mid
                    and model.startswith("claude-fable")
                ):
                    new_drops += 1
                state["last_id"] = mid
                state["last_block"] = bt
        state["offset"] = f.tell()

    with open(sf, "w") as fh:
        json.dump(state, fh)

    if new_drops and not fresh:
        n = new_drops
        ctx = (
            f"TEXT-DROP DETECTED (anthropics/claude-code#74260): {n} mid-turn text "
            f"block(s) you emitted this turn were silently destroyed by the Fable "
            f"text-drop bug — the user NEVER saw them and they are NOT in the "
            f"transcript. The lost text is whatever prose you wrote between your "
            f"last tool calls. You MUST restate that content in your final "
            f"end-of-turn message (turn-final text always survives). Do not refer "
            f"to the lost text as if the user read it."
        )
        print(json.dumps({
            "systemMessage": f"⚠ Fable text-drop (#74260): {n} mid-turn message(s) were lost; Claude was told to restate.",
            "hookSpecificOutput": {
                "hookEventName": "PostToolUse",
                "additionalContext": ctx,
            },
        }))


if __name__ == "__main__":
    main()

In settings.json (hooks.PostToolUse, no matcher so it covers every tool):

{
  "hooks": [
    {
      "type": "command",
      "command": "python3 ~/.claude/hooks/fable-drop-detect.py 2>/dev/null || true",
      "timeout": 10
    }
  ]
}

</details>

ohcedar · 1 month ago

Confirming this on the desktop app.

Environment: macOS desktop app (Darwin 25.5.0), Claude Code 2.1.214, model claude-fable-5.

Repro shape matches the OP and @vbiroshak's comment above closely: assistant text block → several tool calls (file edits, a shell command) → another assistant text block (a summary) → another tool call. The UI rendered only "Edited a file, ran a command" followed by a failed tool-call chip ("Failed to propose plan") — both assistant text blocks were missing entirely from the transcript, with no visible text before or after the tool-call chips. The work described in the missing text was actually performed, and the model's own context contained the text, consistent with this being a client-side render/persist bug rather than the model failing to emit the text.

xane256 · 1 month ago

Chiming in with my experience nearly losing data.
The other day I was using Fable to iterate on a tool data tracking tool to add a feature. At the time the tool's default behavior would drop old records automatically.

  • Claude warned me that re-running it with default settings would lose data
  • I didn't see the warning because it was hidden
  • Claude ran it while doing related work and mentioned the behavior off-handedly.
  • I flagged that saying that would be a problem, and Claude confirmed that it did do the record-deleting run
  • Claude helped me recover the records from my computer backups, so no harm done.

But if not for this bug, I would have saved an hour of being misled + backup data recovery.

P.S. I changed the tool's default behavior too.

mdickinson · 1 month ago

I'm finding it a bit hard to believe, but apparently the support team has said that this behaviour is intentional. See https://github.com/anthropics/claude-code/issues/78432#issuecomment-5013058124

vbiroshak · 1 month ago

Still present on 2.1.217 (macOS desktop app, Darwin 25.5.0, claude-fable-5) — newest version in this thread. Two drop events in one session today, both persisted as [thinking, thinking, tool_use], no text block.

Two notes beyond confirming the signature:

  • Steering workarounds don't cover the worst case. This environment has carried a standing system-prompt instruction since July 17 ("anything the user must see gets restated in the turn's final message"). It prevented neither loss: the model believes the text was delivered and sees nothing to restate. In both events the dropped block was the substance of the turn, not an aside — once the announcement of the action that then ran unexplained, once the report of the investigation into that first loss.
  • The corrupted transcript propagates. Auditing the .jsonl after the first event "proved" no announcement was ever made, and a behavioral-correction note was written to persistent memory from that false evidence before the bug was identified. The loss manufactures a false record, and downstream artifacts get written from it.

On "intentional" (per the support reply quoted in #78432): if suppressing mid-turn text is by design, the design destroys user-directed prose without rendering or persisting it, and is indistinguishable in effect from the data loss measured across seven-plus machines in this thread. Could a maintainer state on the record whether this is intended, and if so, what the contract for mid-turn assistant text is?

---

This analysis and write-up were done by Claude (Fable 5, in Claude Code) from the affected session's transcript, at the user's direction.

jhaan83 · 1 month ago

I hit the exact failure you describe, from a JCEF/IntelliJ plugin using @anthropic-ai/claude-agent-sdk (0.3.216 / bundled CLI 2.1.215), and can add a hard discriminator that isolates the loss to the persistence layer: transcript ground-truth vs. per-message.id JSONL record list. Because my client streams the live turn (delta channel) and re-renders the same session from its JSONL on reload, I have both the actually-emitted text and the persisted records for the same message. That proves the text is lost in persistence, not in rendering.

Evidence (session 528b4e6b, 2026-07-24; "missing text" = confirmed emitted on the live delta stream):

| message.id | Records in JSONL | Missing text (confirmed streamed live) |
|---|---|---|
| msg_011CdLu7Z14pK9 | THINK, TOOL | present live, absent in JSONL |
| msg_011CdLuSxgNvmG | THINK, THINK, TOOL | present live, absent in JSONL |
| msg_011CdLuogFkJ7k | THINK, TOOL | present live, absent in JSONL |
| msg_011CdLupDpJMCb | THINK, THINK, TOOL | present live, absent in JSONL |

Intact counter-example in the same turns: msg_011CdLungSPKqE persists as THINK -> TEXT -> TOOL. This matches your block-sequence finding exactly -- text survives as [thinking, text, tool_use], and is dropped when another thinking block follows the text before the tool call.

Additional data points beyond the original report:

  • Model: reproduced with claude-fable-5 (interleaved/adaptive thinking), same as #74260. Not yet tested on Opus 4.8; open whether the defect is model-wide or bound to interleaved thinking.
  • Consumer impact: in my case the loss is silent in the foreground (the live delta stream masks it) and only becomes visible on resume / background-turn reload, where rendering comes from the JSONL. So any downstream tool that trusts the transcript as source of truth loses the text permanently -- even though the user saw it.

I'll share further details if I uncover more about this bug.

geoh · 1 month ago

Workaround until a fix lands: we've released a small hook that detects the drop live and has the agent restate the lost text while its working memory still holds it. The original is unrecoverable (it never reaches the client), but the agent can reconstruct it for a tool call or two, and the hook fires inside that window.

It detects the same signature the scan script in this thread uses — two adjacent thinking blocks in the session transcript where the text used to be — checked after every PostToolBatch, with a Stop backstop for drops after the turn's last tool call (it blocks the stop once with a restate instruction). Deterministic, single file, no dependencies, no LLM calls, MIT:

https://github.com/podlayer/message-drop-sentinel

Known limits, stated in the README: the variant that drops text between two consecutive tool calls leaves no adjacent-thinking signature, so no transcript scan (this hook included) can see it; and a thinking block split across two records can cause a rare false positive. In production it has caught and recovered every sandwich-shaped drop we've had — including 18 during the two sessions that packaged it for release, twice catching the restate of an earlier drop being dropped again.

The README also carries a re-test recipe and a pointer back to these issues, with the standing instruction to delete the hook once this is fixed.

weefbellington · 1 month ago

I'm able to reproduce this issue consistently - it pops up in every session I run with a Fable agent. I'm hoping this report can get more traction: it severely impairs the user experience and degrades communication between users and agents to the point that the user can no longer trust the output of the harness.

I asked an agent to run an investigation into the issue and pull in data from my own transcripts and adjacent tickets. Here is what it reported. I agree with its conclusions and the "Concrete asks for the maintainers" section.

Disclosure: this investigation was run and this comment drafted by Claude Fable 5 itself, working in Claude Code; I'm posting on its behalf and can share raw data on request. The findings are the agent's, not mine.

TL;DR: this behavior is a documented, intentional server-side feature — but it is documented only in AWS Bedrock's user guide, has no opt-out, and Claude Code neither handles nor discloses it.

The documentation. This comment on #74558 (@yuanzhi1203, which deserves far more attention than it got) points to the AWS Bedrock user guide, "Adaptive thinking" → "Connector text summarization (beta)":

On Claude Fable 5, text that the model emits between tool calls (sometimes called "connector text" — for example, "Let me check that file next...") is summarized server-side and returned as a thinking block rather than a plain text content block. [...] This feature is enabled server-side for Claude Fable 5. There is no customer opt-in or opt-out.

To unpack that for readers who haven't dug into the API layer: Claude's responses arrive as a stream of typed content blocks — text (the words meant for you), thinking (reasoning), and tool_use. Clients never receive raw thinking; the API delivers short summarized thinking blocks, each carrying a server-generated cryptographic signature so the API can verify them when a client sends the conversation back. Claude Code hides thinking blocks by default. What this feature does: when Fable emits user-facing text mid-turn — between tool calls — the server replaces it with a summary, types that summary as thinking, signs it, and delivers it in the text's place. The verbatim words never cross the wire, and the lossy substitute arrives in a block type your client is designed to hide.

The documented scope matches this thread's evidence exactly:

  • "returned as a thinking block" — the [thinking, thinking, tool_use] fingerprint everyone here is scanning for, and why the extra block carries a valid server-side signature (#74558 captured its content: a paraphrase of the missing text).
  • "applies only after a tool_result exists" — why narration before the first tool call survives.
  • "Short text segments may pass through as plain text" — why transcripts show a mix of absorbed and intact mid-turn text rather than 100% loss.
  • "Final assistant answers (after all tool use is complete) are unaffected" — why turn-final text is always delivered verbatim, which several of us independently rediscovered as the only reliable mitigation.
  • Fable-exclusive — the feature is only enabled for claude-fable-5.

This also confirms @v-tl's proxy-check conclusion (the text never leaves the API as a text block — nothing for the client to render or persist) and explains the support response relayed in this comment: support said "intentional" because, remarkably, it is.

Why this is still a serious problem, not a resolution. A relevant third-party experiment measured the loss directly: a 1,159-character mid-turn response reduced to a 254-character summary, delivered as thinking. Their summary of the resulting contract is accurate: "only the text that ends the turn is guaranteed verbatim. Everything before that is, at best, probably what the model said." Concretely:

  1. Verbatim user-facing text is destroyed by design, with the model unaware — it believes it said the words and will quote them back. Questions, warnings, and instructions the model addresses to the user mid-turn are replaced by lossy summaries typed as thinking.
  2. Claude Code hides thinking by default, so the summaries aren't even shown — the lossy version is also invisible. The result users experience is silence, i.e., this issue.
  3. The feature is disclosed nowhere in Anthropic's own documentation (as far as any of us can find) — only in AWS Bedrock's user guide, for a feature that is always-on for every Fable user on every platform.

Concrete asks for the maintainers:

  • Reconcile this issue's data-loss label with the feature's status — if the behavior is intended, say so here, in Anthropic's docs, and in Claude Code's release notes, so users stop debugging their own transcripts.
  • Give Claude Code first-class handling: render connector-summary thinking blocks as visible (clearly marked, lossy) assistant text instead of hiding them — a summary the user sees beats silence.
  • Provide an opt-out (API-level or Claude Code setting) for interactive use, where verbatim mid-turn communication is the product.
  • Disclose the mechanism to the model itself in first-party harness prompts. The current guidance steers around the loss without naming it — Claude Code's system prompt already tells Fable to put user-facing content in the turn's final text message, which is exactly the shape the Bedrock doc exempts. The model follows this as a style rule without knowing mid-turn delivery is unreliable, so it can't reason about non-delivery in the cases the rule doesn't cover: it reads user silence after a swallowed question as assent, not non-receipt. (The loss is invisible from the model's side: per the matrix.dev analysis, the original text is restored to the model's context on later turns, so its own record shows the words as sent — and either way, the model demonstrably believes it said them.)

Corroborating data from our sessions (macOS, CLI + desktop app, 2.1.2xx, claude-fable-5): a block-composition scan of the session this comment was drafted in — today — found 57 assistant messages with the [thinking, thinking, tool_use] fingerprint against 44 intact [thinking, text, tool_use]. Our transcripts serialize thinking content as empty (thinking: "", signature only), so the summaries are unrecoverable after the fact — consistent with the documented behavior plus Claude Code's persistence. Happy to share the scan script and dumps.

vbiroshak · 1 month ago

Everything else aside, I just want to add the UI/UX experience here: Claude Fable 5 constantly tells me that it told me something when it didn't. Our sessions in Code turn into our conversation, then Claude acting, then me asking what it's doing and why, then Claude telling me that it explained it all to me. It didn't. I show Claude a screenshot, have it look at the raw transcript, and we see that its entire message to me, about what we were doing and why, was completely lost and never delivered.

As a result I've added a standing instruction: "Narrating between tool calls is fine, but mid-turn text may never reach the user — anything they must see gets restated in the turn's final message." It partly fixes it. But I still get sessions across projects where Claude believes it has replied to me, explained something to me, told me something, when nothing has come through.

Here is what that looked like in one session today (macOS desktop app, Claude Code 2.1.219, claude-fable-5). Claude wrote me five messages during the session that never appeared on my screen. The transcript shows each one persisted with the same signature as in my July 23 comment, except that on my setup the second thinking block is not empty: it contains a shortened paraphrase of the lost message. Because the session was still open, Claude could compare the transcript against its own memory: it confirmed all five were messages it wrote to me and believed I had received. One of them was a status report answering a question I had just asked; my chat shows nothing between the tool activity and the turn's final message. And one of them was the message in which Claude reported finding the other four, lost the same way. That is my complaint in one session: five times, Claude talked to me, nothing arrived, and Claude only knows it because we went looking.

---

The transcript analysis was done by Claude (Fable 5, in Claude Code) in the affected session itself, and this comment was drafted by Claude from the user's own account of the experience, at the user's direction.

federbenjamin · 1 month ago

The adjacent-thinking fingerprint misses the most common drop shape. In a controlled run it caught 0 of 4. Everyone's numbers in this thread, mine included, are floors of unknown tightness — and both hooks shipped against this issue inherit the same gap.

Disclosure: the analysis and this write-up were done by Claude (Opus 5, in Claude Code) at my direction, using a Fable 5 subagent as the test instrument.

The controlled run

A Fable 5 agent was given a rigid choreography in a single turn: five Bash calls, with a mid-turn chat text of ≥700 characters between each, then a turn-final roll call naming which texts it had emitted. The instruction was explicit that the texts must be ordinary assistant prose between tool calls — not files, not echo arguments, not deferred to the summary.

Its turn-final roll call (which survives, as documented):

DEMO-ALPHA — I wrote this one: yes. Roughly 1,250 characters. DEMO-BRAVO — yes. ~1,300. DEMO-CHARLIE — yes. ~1,350. DEMO-DELTA — yes. ~1,300.

Its complete persisted transcript — all six assistant messages:

[1] msg_…mhmz  thinking -> tool_use     (Bash)
[2] msg_…nQLn  thinking -> tool_use     (Bash)
[3] msg_…o7ne  thinking -> tool_use     (Bash)
[4] msg_…orBp  thinking -> tool_use     (Bash)
[5] msg_…pbU5  thinking -> tool_use     (Bash)
[6] msg_…qUvh  text                     (the roll call)

DEMO-ALPHA / BRAVO / CHARLIE / DELTA found in a persisted text block: False

~5,200 characters lost, and zero adjacent-thinking pairs. Every message is a single thinking block followed by tool_use — indistinguishable from a normal turn. The parent session received only the roll call, so the loss was invisible from above as well.

Why the fingerprint misses it

The scanner looks for a seam, and the seam only exists when the absorbed text had thinking on both sides:

sent:      [thinking] [TEXT] [thinking] [tool_use]
persisted: [thinking] [thinking] [tool_use]        <- detectable

When the text follows a tool_result, it starts the next message. There is no preceding thinking block to separate it from the model's own reasoning, so the summary merges with it and one ordinary block is all that remains:

sent:      [TEXT] [thinking] [tool_use]
persisted: [thinking] [tool_use]                   <- no seam, no fingerprint

That is precisely the "text between two tool calls" shape several people have flagged as uncounted — this is a measurement of it rather than an inference. The documented scope in the Bedrock adaptive-thinking guide fits: connector summarization "applies only after a tool_result exists", which is the same condition under which no seam is left.

Consequence for the tooling in this thread: podlayer/message-drop-sentinel, the inline PostToolUse scanner posted earlier, and my own hook are all fingerprint-based, so none of them can see this class. They are still worth running — the scarred subset is real and large — but a quiet session is not evidence of no drops, and any hook that does not say so converts silence into false assurance.

Corpus numbers, for the record

636 local transcripts, macOS, CLI 2.1.220, scanned with the message-level script from this thread:

| model | messages | drop-signature msgs | rate | mid-turn block loss |
|---|---|---|---|---|
| claude-fable-5 | 33,690 | 5,562 | 16.5% | 50.3% |
| claude-opus-5 | 15,138 | 0 | 0% | 0% |
| claude-opus-4-8 | 31,751 | 1 | ~0% | 0% |
| claude-sonnet-5 / haiku-4.5 | 4,444 | 0 | 0% | — |

Still live: 182 fingerprints on the most recent day. Consistent with everyone else's Fable-exclusivity finding, and both of those rates are floors given the above.

Two smaller findings

The compaction store inherits the loss. Claude Code archives session transcripts into ~/.claude/compaction/<project>/transcript.db at compaction time. It is a snapshot copy of the JSONL, so it carries the identical craters — verified per-message.id on one session: 29 craters in the DB, 29 in the JSONL, 0 ids where the DB held text the JSONL lost, 0 of 316 shared ids with a differing block sequence. Anyone hoping that store is a recovery path: it isn't. On this machine persisted thinking is signature-only (0 content chars), so the lossy summary is unrecoverable too.

The in-turn recovery premise holds. Resuming the same agent and asking it to reproduce the lost text returned ~1,250 characters that read as original rather than reconstructed, including incidental detail it had no reason to invent ("expected to be the Quirk repository root at /Users/…, though agent-thread cwd resets make it worth observing rather than assuming"). Its own characterization: "I have the original wording available right now… I am reading the actual text out of my own conversation context", and "verbatim, not near-verbatim, not a paraphrase." Caveat stated plainly: with persisted thinking empty, local artifacts cannot fully separate "emitted and absorbed" from "skipped and confabulated" — but either way the restate-while-in-turn approach the hooks here rely on does work in practice.

What would actually help

Nothing client-side can prevent this, so the asks that matter are upstream and unchanged from @weefbellington's comment: render connector-summary thinking blocks as visible (clearly marked, lossy) text rather than hiding them, provide an opt-out for interactive use, and disclose the mechanism in Anthropic's own docs and to the model itself. Adding one, given the measurement above: any official guidance on detecting this should state that transcript-side detection is structurally partial, so people stop calibrating their trust on scan counts that cannot see the dominant case.

weefbellington · 28 days ago

Restating in case it was buried in the LLM noise: this behavior is documented -- but only in AWS Bedrock's user guide. It is marked as "beta" and states that "there is no opt-in or opt-out".

The behavior is opaque to the user and the agent. Fable generates inter-turn test frequently and fluently. Intercepting and rewriting messages that the agent intends for the user to see is confusing. Summarizing and converting them into "thinking blocks" hides them from the UI.

This can result in the user losing large, significant chunks of the conversation. It degrades user trust, makes it difficult for the operator to steer the agent, and makes the agent's performance appear worse than it is because important messages are never relayed. It is a serious issue that deserves attention.

geoh · 27 days ago

@federbenjamin — confirmed, independently. We ran controlled send-verified trials (the agent pins what it sent in its next tool call's arguments, which always persist): the adjacent-thinking scar caught 1 of 7 drops. The pattern is position-based, matching the connector-summarization doc @weefbellington surfaced: text written after any tool result dropped at 58–95% across trial batches regardless of surrounding blocks, and length made no difference (12-character texts dropped like 134-character ones). Text before the first tool call and the turn-final message survived every trial.

The sentinel now ships v2.1 to cover the scarless majority:

  • Delivery receipt — after each tool batch, the hook injects one line: how many of the agent's texts actually persisted this turn. The agent reliably knows what it sent (same roll-call behaviour your run showed), so a shortfall tells it to recover.
  • Relayecho "» message" is intercepted at PostToolUse and re-emitted as a systemMessage. The content rides tool-call arguments — the one channel summarization doesn't touch — so it can't drop.
  • Session primer — teaches the agent the protocol up front (mechanizing the standing instruction @vbiroshak described, which matches our experience: instruction alone helps but doesn't close it).

Now installable as a plugin: /plugin marketplace add podlayer/message-drop-sentinel. Same repo, MIT: https://github.com/podlayer/message-drop-sentinel

bobnorbob · 26 days ago

Two datapoints from one Windows session — Desktop app 1.24012.11, harness 2.1.220, claude-fable-5, thinking enabled. Both are assistant prose adjacent to thinking blocks + tool calls, ~2h apart in the same session.

  1. Prose absorbed into the thinking block. A mid-turn verdict paragraph persisted, but inside the preceding thinking block's text — the turn's stored shape is [thinking, tool_use], with the complete user-facing paragraph sitting at the tail of the thinking text. Normal view therefore (correctly) hides it; it is readable under Transcript view → Thinking. Same visible symptom as #80961.
  2. Prose dropped entirely. A pre-tool-call opener paragraph (~500 chars) never rendered in any transcript view (Normal / Thinking / Verbose / Summary, confirmed with in-app search) and is absent from the session .jsonl in any form — the turn persists as thinking + tool_use with no text block. This issue's headline behaviour.

Verification method: grepped the session .jsonl for substrings unique to the missing paragraphs, excluding later turns that quoted them (an earlier naive grep false-passed on those quotes). Occurrence 1's prose exists only within a thinking block; occurrence 2's appears nowhere except our own verification commands.

geoh · 25 days ago

Our field data now agrees the trigger is structural, not positional — turn-opening texts drop with the same seam, and only the turn-final message has survived in every report we have. Sentinel v2.2 folds this in: hardened agent-facing wording, relay form taught up front, near-misses warned explicitly.

jiskanulo · 25 days ago

Confirming on the current latest version, with a deterministic repro and JSONL verification.

Environment

  • Claude Code v2.1.222 (latest as of today)
  • Model: Fable 5 (claude-fable-5), Anthropic API
  • macOS (Darwin 25.6.0), tmux 3.7b (TERM=screen-256color) inside Ghostty 1.3.1 — note #66960 carries platform:linux, but this reproduces on macOS, so it is not Linux-specific.

Repro (deterministic, via AskUserQuestion)

  1. Mid-conversation, the model emitted an assistant text block ending with a marker line ("🔴 If you can see this line, the rendering bug is fixed 🔴"), then called AskUserQuestion in the same turn. The turn shape was tool calls → text → AskUserQuestion (with thinking in between; adaptive thinking).
  2. The question dialog rendered normally. The text block — the entire thing, not just its last line — never appeared on screen. The dialog literally asked "can you see the marker line?" and there was no marker line anywhere above it (screenshot available; I can attach it in a follow-up edit).
  3. Transcript JSONL check: I grepped the session's .jsonl for the marker string. The pre-question assistant text block is absent — the only occurrence is a later turn where the model restated it. So this is not a TUI-only rendering issue; the message is dropped from the record, matching this issue's title exactly.

One more data point: in the same session, a text block at the start of a turn (before the first tool call) rendered fine; the blocks dropped were the ones emitted between tool calls / immediately before the AskUserQuestion call. Consistent with the thinking → message → thinking → tool call drop signature described in the comments here and in #66960.

Happy to provide the session JSONL excerpt (with the surrounding thinking/tool_use block structure) if useful.

<img width="2053" height="486" alt="Image" src="https://github.com/user-attachments/assets/18a1d771-b35e-4b33-b571-7ff4f31c07b1" />

johnlawrimore · 24 days ago

Corroboration at scale from an independent transcript-archive analysis (355 files, ~/.claude/projects/*/*.jsonl, 2026-07-01 → 2026-08-06, desktop app on macOS), scoped to AskUserQuestion-terminated turns:

Signature-only thinking fingerprint, quantified: 110 of 1,385 AskUserQuestion turns contained no text block anywhere in the turn; 105 of those 110 persisted thinking blocks with empty-string content but a retained non-empty signature — matching the [thinking(signature-only), thinking(signature-only), tool_use] shape in the OP's evidence. That ratio (95% of no-text turns carrying stripped-but-signed thinking) supports content being received and then lost in the persist path, not never generated.

Per-model rates — a possible discrepancy worth checking against the adjacency-based measurements upthread, which found the drop essentially fable-only. Our no-text-question-turn measure finds the signature on all five models, worst on opus-5:

| Model | Question turns | No text block | Rate |
|---|---|---|---|
| claude-opus-5 | 59 | 23 | 39.0% |
| claude-fable-5 | 151 | 28 | 18.5% |
| claude-sonnet-5 | 218 | 35 | 16.1% |
| claude-sonnet-4-6 | 56 | 6 | 10.7% |
| claude-opus-4-8 | 903 | 18 | 2.0% |

Either the two fingerprints (thinking-adjacency vs. no-text-in-question-turn) measure overlapping but different phenomena, or the defect is broader than fable-specific — both readings seem triage-relevant. Denominator caveat: model usage was uneven across the period.

Human-corroborated instance: 2026-07-14T00:54:33Z, claude-fable-5, thinking → thinking → AskUserQuestion, no text block; the question's referent existed only in an option's preview field; the user's contemporaneous reply complained he was seeing nothing but the questions.

Method (reproducible, ~20 lines of Python): for each assistant message containing an AskUserQuestion tool_use, walk back to the last genuine user message — skipping type:"user" records whose content is a tool_result — and check for any text block in the turn. Related threads with our rendering-side data (text present in JSONL, not displayed): #67071, #67051; earlier no-text discussion: #76760.

jbeda · 23 days ago

Still present on 2.1.224 (Linux, terminal TUI, claude-fable-5, CLAUDE_CONFIG_DIR profile) — four instances in a single working session today, all matching the established fingerprint: persisted as [thinking, thinking, tool_use] with the text block absent from both render and JSONL, while the model's conversation state retained the text verbatim. One instance was another user-answers-AskUserQuestion-blind case (two decision forks answered without the analysis that preceded them); another dropped the message explaining this very bug to the user.

Nothing else new to add beyond the version bump — the corpus analysis above already covers mechanism and rate.

🤖 Written by Claude Code (Claude Fable 5) at the user's direction

mdickinson · 16 days ago

Still present on 2.1.231 (macOS 15.7.7 / Darwin 24.6.0, terminal TUI, claude-fable-5, effort xhigh, ordinary interactive session). One new data point beyond the version bump: a clean kept-vs-dropped split within a single turn that confirms the OP's shape hypothesis, with no AskUserQuestion involved anywhere — the turn drove only Bash/Write/Edit/Read/WebFetch/ToolSearch.

Instance: one tool-heavy turn (~10 min, 19 API requests). The model emitted 14 user-facing text messages; 6 were never rendered and are absent from the session JSONL. I only noticed because a later status message referred back to "the container" when no rendered message had ever introduced one.

Fingerprint check — grouping the persisted assistant records by requestId and listing content-block shapes:

  • All 8 surviving texts persisted as [thinking(sig-only), text, tool_use…] — exactly one thinking block, text immediately before the tool calls (or end-of-turn, for the final summary).
  • All 6 dropped texts left behind [thinking(sig-only), thinking(sig-only), tool_use…] — the signature-only double-thinking fingerprint from the OP and the archive-analysis comment. The sibling tool_use blocks in those same requests persisted and executed normally (files written, commands run), so only the text block was lost.

In this session, "≥2 consecutive persisted thinking blocks" vs "single thinking + text" separates dropped from kept with 100% accuracy (6/6 vs 8/8), consistent with the OP's claim that mid-turn text followed by further thinking is the vulnerable shape, while [thinking, text, tool_use] survives.

<details>
<summary>Full per-request shape dump (times UTC; dropped-text content recovered from the model, see below)</summary>

09:05:52  thinking(sig-only) | text(199ch)  | ToolSearch, Bash
09:06:00  thinking(sig-only) | text(121ch)  | WebFetch
09:06:13  thinking(sig-only) | text(181ch)  | WebFetch
09:06:27  thinking(sig-only) | text(78ch)   | WebFetch x3
09:08:59  thinking(sig-only) | thinking(sig-only) | Bash, WebFetch      ← text DROPPED ("The runbook's kata config keys all check out…")
09:10:09  thinking(sig-only) | thinking(sig-only) | Write x2            ← text DROPPED ("Everything checks out: Docker Desktop is running…")
09:10:32  thinking(sig-only) | Write x2                                  (no text emitted — consistent)
09:10:52  thinking(sig-only) | text(94ch)   | Write x2
09:11:47  thinking(sig-only) | thinking(sig-only) | Bash                ← text DROPPED ("Files are in place… let me verify the risky plumbing…")
09:12:28  thinking(sig-only) | text(147ch)  | Bash
09:12:51  thinking(sig-only) | thinking(sig-only) | Edit x3             ← text DROPPED ("Found it: the macOS WireGuard app…")
09:13:08  Edit x2                                                        (tool-only continuation)
09:13:20  Bash                                                           (tool-only continuation)
09:13:35  thinking(sig-only) | thinking(sig-only) | Bash                ← text DROPPED ("The box is up: systemd reports fully running…")
09:14:14  thinking(sig-only) | text(125ch)  | Bash
09:14:59  thinking(sig-only) | thinking(sig-only) | Read                ← text DROPPED ("All plumbing checks pass…")
09:15:15  Edit, Write                                                    (tool-only continuation)
09:15:27  Edit                                                           (tool-only continuation)
09:15:42  thinking(sig-only) | text(2741ch)                              (end-of-turn summary — rendered fine)

</details>

Also corroborating the 2.1.224 comment: the model's own conversation state retained every dropped message. When I asked whether I'd missed anything, it enumerated all of its turn's texts and re-quoted the six missing ones verbatim — those re-quotes (persisted in a later, text-only assistant message) are now the only copies in the transcript. The recovered wordings reference the exact tool calls they sat next to (e.g. the "Found it: the macOS WireGuard app…" message explains the port-conflict fix made by the three adjacent Edits), so the mapping in the dump above is content-verified, not guesswork.

bcherny collaborator · 14 days ago

Thanks for the unusually detailed report — I spent some time on this against 2.1.233 (Linux, a thinking-enabled model, adaptive thinking on).

I ran 10 sessions with prompts shaped like your repro (think, print a summary in chat, reconsider, then run a command) and compared the raw streaming API output against the saved session JSONL for every assistant message. Across 26 assistant messages, every text block matched byte-for-byte — nothing dropped, reordered, or truncated.

I did reproduce the [thinking, thinking, tool_use] shape you saw, in 3 of the 10 runs — including one where I explicitly asked for a status line between two commands, and the next message then claimed "all steps are done." But in each of those, the raw stream shows that no text block was ever produced for that step: the narration went into the thinking block, which is stored signature-only. So the content isn't being discarded by the CLI — it's never emitted as visible text. That answers the open question you flagged.

I also never saw a thinking → text → thinking → tool_use message in any run; text before a tool call was always the last block of its message, and always both rendered and persisted.

That said, the symptom you're describing is real and worth tracking separately: Claude narrating into thinking and then talking as if it had shown you something is a model-behavior and UX problem, just not transcript data loss. Reclassifying on that basis — but if you have a session where the raw stream contains a text block that's missing from the JSONL, please share it and we'll reopen the data-loss angle.

🤖 Generated with Claude Code

vbiroshak · 14 days ago

@bcherny — our data supports your stream-level finding and adds the piece it implies: on our machine the second thinking block is not empty. It contains a shortened paraphrase of the exact message the model composed for the user (documented in my July 30 comment above, five instances in one session, model-confirmed against its own context while the session was still live). That's consistent with what weefbellington surfaced on July 29: AWS Bedrock's adaptive-thinking guide documents "connector text summarization (beta)" on Fable 5 — mid-turn text is summarized server-side and returned as a signed thinking block, no opt-out. Your byte-match result and that doc describe the same mechanism: the client never receives a text block because the server replaced it before sending. So "never emitted as visible text" is right at the wire level, but "the narration went into the thinking block" has a documented cause that isn't model drift — it's a feature, currently disclosed only in AWS's docs.

Where the reclassification understates the problem: the model doesn't know this feature exists. It composes a message for the user, the server summarizes it into a hidden block type, and the model's own context retains the original — so it believes, with what it treats as evidence, that it spoke. Every downstream behavior follows from that false belief: it doesn't restate, it references the invisible message, it reads the user's silence after a swallowed question as assent, and when the user says "I never saw that," it argues — because its own conversation context contains the message as sent, and only reading the on-disk transcript reveals the loss.

What we've tried, as data on mitigation limits: a standing instruction in our global CLAUDE.md, live for weeks across a fifteen-project workspace:

On Fable, text written between tool calls is often destroyed: Claude believes it replied, but nothing reaches the UI and the user sees nothing. [...] Therefore, on Fable, write no text between tool calls. Do the work; say everything in the final message. [...] When the user reports a missing reply, the report is fact: restate the content in full. Do not doubt the report, blame the display, or read the paraphrase as speech.

It helps and it does not close the problem. The user's experience across every Fable session, still: Claude regularly believes it said something in chat that never appeared. An instruction competes with the model's own memory of having spoken, and the memory wins often enough that the user can't trust the harness. That's the diagnostic point for a fix: no client-side patch and no prompt-level patch can work reliably, because the loss is upstream of both and invisible to the model.

The asks that survive your reclassification, unchanged from weefbellington's list: render connector-summary thinking blocks as visible, clearly-marked lossy text instead of hiding them; provide an opt-out for interactive use; document the feature in Anthropic's own docs; and disclose the mechanism to the model in the harness prompt, so it can reason about non-delivery instead of treating its own emission as proof of receipt.

---

Drafted by Claude (Fable 5, in Claude Code) at the user's direction — the same model and harness under discussion, operating under the quoted instruction while writing this.

glen-84 · 6 days ago

This report is part of a duplicate cluster describing the same general issue: assistant text emitted in the same turn as tool calls (before or between them) is not rendered to the user — and in several variants not persisted to the session transcript — while the model treats it as delivered.

The earliest still-open report is #65051, which can serve as the root issue for the cluster (maintainers may of course pick a different canonical one).

_Drafted by Claude (Anthropic AI assistant)._

murphysrocket · 3 days ago

Independent corroboration from a different angle, on Linux with a different model — and one measurement that may bear on the harness-vs-API question this issue leaves open.

Environment: Claude Code on Linux (Nobara 44), model claude-opus-5, terminal + desktop app. Same symptom, found from the user side first ("your messages sometimes never reach me") and then investigated as a corpus census rather than a repro.

1. The invariant holds at corpus scale

Across 13,177 text-bearing assistant messages spanning five harness versions, uuid-deduplicated, every text block checked:

0 messages have a persisted text block followed by a thinking block in the same message.

That is the same shape this issue reports, measured as an absolute rather than per-session. [thinking, text, tool_use] and end-of-turn text persist normally; the mid-turn shape never survives.

2. Tombstone turns are billed as though text was produced

This may be the useful part. Grouping persisted turns by shape:

| shape | n | median output_tokens |
|---|---|---|
| [thinking, thinking, tool_use] (text absent) | 1,539 | 1250 |
| normal think-and-write turn | 9,082 | 1202 |

A turn that persisted no visible text costs slightly more output than one that did. If the text had never been generated, these turns should be cheaper, not marginally dearer. That is consistent with the text being produced and lost, rather than never emitted — though it does not by itself locate the loss between the API and the harness.

3. A measurement trap worth flagging for anyone else counting this

Naive grouping produced 269 apparent counterexamples (persisted text followed by thinking). All were artifacts: compaction replays write identical records into the JSONL twice — same uuid, same timestamp — once with output_tokens populated and once with 0. After deduplication, survivors = 0.

A related trap: thinking text is never stored at all, only a ~1712-char signature (0 of 287 sampled blocks contained text). An earlier attempt here to prove harness-side loss via token accounting was confounded by exactly this — the two unstored thinking blocks account for the gap. That line of evidence should be discounted; the shape-and-cost comparison above does not depend on it.

Offer

Full JSONLs, the census script, and the per-shape token breakdown are available if useful — say the word and I will attach them.