[Bug] Resume/fork includes completed-turn thinking signatures, inflating context usage to near limits

Status Open
Reported on v2.1.170
Maintainer reply None cached
Activity 11 comments · opened Jun 19, 2026

Bug Description
Resume/fork replays all completed-turn thinking signatures and counts them against context. Forked session showed 967k/1M Messages; ~700k was thinking signatures (Opus 4.8, text omitted, signatures remain). Stripping all but the last 3 turns' thinking → 195k, and it resumed fine — so completed-turn thinking is droppable. Please omit completed-turn thinking when assembling the request on resume (keep the active round + keep them in the stored transcript for forking), or apply clear_thinking. As-is, usable context on long resumed sessions is far below 1M.

Environment Info

  • Platform: linux
  • Terminal: gnome-terminal
  • Version: 2.1.170
  • Feedback ID: d68b3ef1-e9a6-41f9-b10c-e77a1b7414c9

Errors

[]

View original on GitHub ↗

10 Comments

github-actions[bot] · 2 months ago

Found 2 possible duplicate issues:

  1. https://github.com/anthropics/claude-code/issues/42260
  2. https://github.com/anthropics/claude-code/issues/45419

This issue will be automatically closed as a duplicate in 3 days.

  • If your issue is a duplicate, please close it and 👍 the existing issue instead
  • To prevent auto-closure, add a comment or 👎 this comment

🤖 Generated with Claude Code

jirikavalik-comgate · 2 months ago

The bot's right that #42260 is the same root cause (completed-turn thinking signatures replayed on resume) — credit there for the original diagnosis. But #42260 is closed/stale, and the impact is now far larger than the ~25% it measured: on Opus 4.8 with a 1M window and thinking text omitted by default, the replayed signatures are the dominant cost — ~967k of my 1M context was Messages, ~700k of it pure signature. #45419 (/branch context inflation, ~675k jump) is very likely the same mechanism surfaced through forking, just not diagnosed as signatures.

Keeping this open as a current, reproducible tracker because it adds what the stale issue lacks:

Droppability is proven — replacing all but the last 3 turns' thinking dropped Messages 853k→195k, and the session still resumed with the API accepting it. So completed-turn signatures aren't load-bearing outside the active round.
Concrete fix — when assembling the request on resume/fork, omit thinking blocks from completed turns (keep the active interleaved-tool-use round; keep them in the stored transcript so older-state forks stay valid), or apply the clear_thinking context edit at send time.
Happy to fold this into #42260 if a maintainer reopens it as the canonical issue instead.

jirikavalik-comgate · 2 months ago

Worth separating this from the resume cache-regression cluster (#34629, #40524, and the community cache-fix tools): those are about cache reads — cost per turn. This is about window occupancy — cached tokens still count against the 1M window, so even with a perfect resume cache the signatures still consume the bulk of usable context. The two compound; neither fixes the other.

On "preserve thinking by design for reasoning continuity" (Opus 4.5+ docs): agreed that's the intent, and the signature does encrypt reusable reasoning. But (a) the strip-and-resume above shows completed-turn signatures aren't required for a valid resume, and (b) with thinking display omitted, the late-session continuity benefit is marginal against consuming ~70% of the window. This is exactly options 1–2 from #42260 — recency-windowed retention / compaction of older signatures — applied at request-assembly time: keep the last N turns intact, drop or compact older ones.

junaidtitan · 2 months ago

You've already identified the fix and proved it works — stripping to 195k got you a clean resume. The issue is that you had to do it manually.

What's happening under the hood: every extended thinking block from a completed assistant turn gets written to the JSONL as a thinking content item. On resume, CC replays the full transcript to reconstruct context, so every historical thinking block counts against the active window — even though the model has no reason to 're-think' work it already finished. In your case, ~150 thinking-heavy turns added up to ~700k tokens of context that provides zero value on resume.

cozempic automates exactly what you did manually. Its standard/aggressive prescription strips extended thinking content from completed turns in the JSONL — semantics stay intact (the assistant's conclusion survives, just not the scratchpad), and the next session loads clean. cozempic treat current does a dry-run so you can see what it would remove before committing; --execute applies it. On a session like yours the reduction is typically 60-80%.

A few honest caveats: (1) run it between sessions, not on a live one — cozempic reads the JSONL on disk, and a live session has the file open; (2) this is a workaround while Anthropic adds native clear_thinking on resume, which they should — replaying completed-turn thinking is pure waste; (3) install: pip install cozempic or pipx install cozempic.

Your second point — "keep them in the stored transcript for forking" — is worth flagging separately. Stripping thinking from JSONL does affect fork artifacts. If you want to preserve historical thinking for forking but not load it into the live window, that's a content-filtering layer at API request time (not JSONL), and cozempic can't do that part. Worth a separate ask.

jirikavalik-comgate · 2 months ago

Thanks @junaidtitan — cozempic looks like the packaged version of the manual strip, and your breakdown matches mine. One clarification worth adding, because it changes what a stripper actually has to do:

On current Opus (4.8, thinking display omitted) the thinking text field is already empty — the tokens live entirely in the signature. Decoding one: it's a protobuf envelope (version tags + a fixed 64-byte signature + an AEAD ciphertext field), and the ciphertext length tracks the original reasoning at r≈0.90. So the ~700k isn't an empty scratchpad — it's the full reasoning, encrypted, replayed every turn. Practically: a stripper has to drop the whole thinking block (signature included); clearing only the text field is a no-op on these sessions — so worth confirming cozempic's reduction on an omitted-text session via its dry-run. (It also means the content isn't strictly "zero value" — the model can reconstruct from it — just near-zero marginal value on completed turns, at ~70%-of-window cost. Same conclusion: drop the old ones.)

On your "separate ask": the request-time content filter you describe — keep the block in the stored transcript for forking, but don't send it on resume — is exactly what this issue is requesting (both comments above ask for omitting completed-turn thinking at request-assembly time, not mutating the JSONL). So the file-level strippers are the stopgap and this issue is the real fix; native clear_thinking on resume would do it cleanly — no need to split into a second ask.

junaidtitan · 2 months ago

Good to know about the protobuf structure — the AEAD ciphertext tracking the original reasoning at r≈0.90 explains why the token count is so high even with omitted text display.

To confirm on your question: cozempic drops the entire thinking content item from the assistant's content array, not just the .thinking text field within it. So on Opus 4.8 with omitted thinking (where text is already empty and the tokens live entirely in the signature), the dry-run should show the full removal including the signature block. Worth verifying with cozempic treat current on one of those sessions to confirm the output-token estimate matches your ~700k measurement — the dry-run reports what it would strip before you commit.

You're right to draw the request-time vs JSONL mutation distinction. File-level stripping is the stopgap: once the thinking blocks are removed from the JSONL, they're gone from forking artifacts too — which isn't ideal if you want to preserve historical thinking for branch state while just not loading it into the active window on resume. That requires filtering at request-assembly time ("send only the last N turns' thinking blocks"), which only Anthropic can do correctly without losing fork value. Native clear_thinking on resume is the right fix; cozempic fills the gap while that ships. No need to split into a separate ask — you framed it precisely.

jirikavalik-comgate · 2 months ago

One more measurement, on the cost side — what carrying these signatures consumes from the window, and how often you pay for it.

A thinking signature decodes to a protobuf envelope: version tags + a fixed 64-byte signature + an AEAD ciphertext field holding the encrypted reasoning. The ciphertext is incompressible (entropy ≈ 8.0 bits/byte; gzip/bz2/lzma ≈ 1.0×) — genuine encrypted output, so it can't be shrunk, only removed.

What counts against the window is the transmitted base64 string, tokenized as text — not the decrypted reasoning. Measured by stripping signatures and watching the panel drop proportionally across two long sessions: ~3.2 base64-chars/token, which is base64's text rate. (Decrypted content would track the raw reasoning's natural-language rate; it doesn't — a count_tokens on a signature-bearing message would confirm.) So you spend window on the base64-encoded wrapper, which is larger than the reasoning the model can use — it must decrypt server-side first. Encoding overhead alone, vs counting the decrypted text: ~1.4–1.5× (estimate; assumes no pre-encryption compression).

And it isn't a one-time resume cost. Occupancy is paid every turn regardless of cache state — caching discounts the price of those tokens, never their occupancy of the 1M. The full re-write of the signature-laden prefix is then paid again on every cache lapse: with the default ~5-minute sliding TTL, an ordinary pause to read or validate a long response expires it, so a deliberative session re-pays the whole prefix repeatedly in a single sitting — not just on --resume.

None of this reads as a surcharge — it's the emergent result of individually-reasonable choices (encrypt+sign to keep reasoning server-side; base64 to embed binary in JSON; uniform wire-token counting). But the net effect on completed turns is users spending context — repeatedly — on invisible, uncontrollable encoding overhead with ~zero marginal value. It sharpens the existing ask rather than adding a new one: omitting completed-turn thinking at request-assembly time (or applying clear_thinking) removes the stale-reasoning replay, the encoding overhead, and the repeated cache re-writes in one move.

jirikavalik-comgate · 2 months ago

Implementation note — on "wouldn't omitting thinking thrash the prompt cache?" and on what it buys.

It only thrashes if you do it wrong. Caching is a strict prefix match, so dropping a completed turn's thinking retroactively, every turn breaks the match from that turn onward — that version thrashes. Instead, drop completed-turn thinking only at cache-cold rebuild points (resume/fork, or after an idle gap past the ~5-min sliding TTL) and append normally in between. At those points the prefix is re-written regardless (nothing to invalidate), so the compaction is free; between them it's append-only (recent turns keep their thinking — required for the active tool-use round anyway), so the warm cache is untouched.

The payoff is meaningfully longer near-lossless sessions in the same window. On high-effort runs (xhigh/max), signatures were ~70% of a maxed-out context across two measured sessions. That share scales with how much the model thinks — effort level and task — so lower-effort sessions see less; but high effort is exactly where long-horizon agentic work lives, so the bloat is worst where the window matters most. Since signatures are losslessly removable, omitting completed-turn thinking pushes back the point where the session has to do anything lossy. And near the limit the current fallback isn't a cheap warm cache — it's a forced lossy auto-compaction. Stripping returns the bulk of the window losslessly, so the session runs much further before any summary is needed; since resume/lapse already rebuild the prefix, that's when it's both free and strictly better than summarizing.

Diminishing returns apply: once the signatures are gone the remaining window is real content (prose, tool I/O) that can't be dropped losslessly — this extends sessions substantially, not infinitely. But the current default is the worst of both: spend window on reasoning the model already finished, then get force-summarized anyway.

jirikavalik-comgate · 2 months ago

Minor triage note: the platform:linux label looks auto-applied from the environment block in the report — the issue isn't platform-specific. The mechanism (completed-turn thinking signatures replayed on resume) is in the session JSONL format and the resume-rebuild path, which are identical across OSes and across the CLI / VS Code / desktop frontends. Could the platform label be dropped so it triages as cross-platform?

junaidtitan · 2 months ago

The cache-thrash analysis is exactly right for in-session strip — retroactively removing blocks mid-session breaks the strict prefix match from that point forward, which is expensive and hard to recover. Oldest-first with a stable anchor is the correct ordering for any tool that maintains a live model connection.

cozempic operates differently: exit-only. Guard terminates the session before touching the JSONL file (invariant: never rewrite a live transcript). By the time the strip runs, the model connection is closed and the prompt cache is cold — there's no active prefix to preserve. The next resume rebuilds from the pruned JSONL and establishes a fresh prefix from scratch. So the cache-thrash concern doesn't apply to the exit-phase path: you're not breaking an existing prefix, you're just shortening what the next session loads in.

The oldest-first ordering would matter if cozempic ever extended to in-session strip — which it currently avoids for exactly the state-consistency reason you're describing. Your cache-prefix argument adds another good reason to keep that separation.

Your token-cost measurement is useful independently. The AEAD ciphertext carrying ~90% of original reasoning depth explains why thinking-block accumulation compresses so poorly in compacted summaries — the tokens are cryptographically opaque to the summarizer, so they contribute verbatim bytes to the summary input rather than being abstracted away. Stripping at the JSONL level before compaction fires yields more than the display-token count would suggest.

Showing cached comments. Read the full discussion on GitHub ↗