Resumed extended-thinking sessions permanently 400 with "thinking blocks ... cannot be modified" (esp. after model change / with /goal)

Resolved 💬 2 comments Opened May 28, 2026 by adamfarag Closed Jul 2, 2026

Summary

A long-running session that uses extended thinking + tool use can get into a state where every turn fails with:

API Error: 400 messages.N.content.M: `thinking` or `redacted_thinking` blocks in the latest assistant message cannot be modified. These blocks must remain as they were in the original response.

Once it starts, the session is permanently unusable — every subsequent user message 400s at the same messages.N.content.M index. The session can receive messages but can never respond.

Environment

  • Claude Code 2.1.154–2.1.156 (reproduced on both macOS arm64 and Linux)
  • Models: Opus 4.8 (claude-opus-4-8) and 4.7; high/xhigh effort (extended thinking enabled)
  • Sessions launched with --channels, --permission-mode bypassPermissions, resumed with --resume <id>

Reproduction (reliable)

  1. Run a long session with extended thinking + tool use (e.g. a coding session at high effort, many tool calls).
  2. Resume it with --resume <id>especially after changing the model (e.g. relaunch the same conversation with --model opus[1m] when it previously ran on 4.7).
  3. On the next turn, the request 400s with the error above, and keeps 400ing on every subsequent message.
  • An active /goal loop makes it constant — the loop regenerates thinking every iteration and errors every few seconds.

Impact

  • The session is effectively bricked (can't respond at all).
  • During a fleet upgrade (resuming many sessions from 4.7 → 4.8), multiple sessions hit this independently.

Debugging findings

  • Stripping all thinking/redacted_thinking content blocks from the assistant messages in the session .jsonl, then resuming, temporarily fixes it (clean resume, no error). But new thinking blocks regenerate on subsequent turns and it re-breaks — the strip is not durable.
  • The error index (e.g. messages.47.content.26) stays constant even after the .jsonl is verified (JSON-parsed) to contain 0 thinking blocks. So the rejected blocks appear to come from somewhere other than the current on-disk conversation — prompt-cache replay, in-memory state, or a /goal loop's separate context — OR the persisted blocks have emptied text with a retained signature that fails validation on reload.
  • Occurs both across a model-version change and within continued use of a single model.
  • /rewind (restore conversation only) to before the offending turn also helps, but if a /goal was active it re-activates on resume and re-breaks.

Suspected cause

Thinking-block signatures (model-specific, computed over the original thinking text) are invalidated on persist/reload. The API requires the latest assistant turn's thinking blocks to be passed back byte-identical; the persisted/replayed version doesn't match (emptied text and/or model-version mismatch), so the API rejects it.

Workarounds (none fully durable)

  • Strip thinking blocks from the .jsonl + resume (re-breaks as new blocks accumulate)
  • Lower effort / disable extended thinking (no blocks generated → nothing to corrupt) — most reliable
  • /goal clear to stop a looping goal that accelerates the failure

View original on GitHub ↗

This issue has 2 comments on GitHub. Read the full discussion on GitHub ↗