claude-code - 💡(How to fix) Fix 2.1.154 regression: bridge/remote-control session replay re-sends persisted (empty-text, signed) thinking blocks -> 400 'thinking blocks cannot be modified', session unrecoverable

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…

On Claude Code 2.1.154, a session driven through the cloud bridge / remote control can return:

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.

The error is intermittent and self-recovering during live interactive use, but becomes a permanent, unrecoverable wedge when it fires during an unattended continuation (a scheduled wakeup, a backgrounded subagent returning, or a remote re-drive with no human reply). A wedged session returns the identical 400 on every subsequent turn — including new user messages — and only manual .jsonl surgery brings it back, after which it re-breaks on the next disk-reloaded turn.

Error Message

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.

Root Cause

Thinking blocks are persisted to disk as {"type":"thinking","thinking":"","signature":"..."}empty text, real signature — in every version. This is normal and is not the problem by itself.

The 400 fires when the latest assistant message is reconstructed from the stripped-on-disk transcript (empty text under a live signature) and re-sent. The API validates the signature against the now-empty content, the seal fails, and the request is rejected. A normal interactive turn instead rebuilds the request from the live in-memory message objects, which still hold the full thinking text, so the signature validates and the request passes.

What makes the difference between transient and terminal is whether a live, un-reloaded in-memory copy of the original thinking still exists in the process:

  • Process continuously alive, never reloaded from disk → full thinking text still in memory → every turn (human or automated) rebuilds from it and succeeds. The 400 may flicker but always recovers.
  • Process reloads from disk — a scheduled wakeup firing, --resume, or a remote/bridge re-drive — → the good in-memory copy is overwritten by the stripped on-disk version → there is no longer an un-stripped copy anywhere → permanent. From this point no turn of any kind can fix it, including a fresh human message, because the recovery target no longer exists.

This is why a human message does not rescue an already-wedged session: by the time it arrives, the disk reload has already discarded the only good copy. It also explains why truncating the transcript and resuming only buys a single turn — --resume is itself a disk reload, so the session comes back living off stripped state; the first fresh turn generates new in-memory thinking and works, then the next disk-rebuilt turn re-breaks.

Fix Action

Fix / Workaround

Recovery / workaround

  • Recovery: manual .jsonl surgery — truncate to the last record whose parentUuid chain has no dangling tool_use and no trailing thinking block, repoint the leaf, resume. Re-breaks on the next disk-reloaded turn.
  • Workaround: downgrade to 2.1.152, or do not leave a thinking-enabled 2.1.154 session running unattended over the bridge (drive it live; avoid scheduled wakeups and walk-away background work).

Code Example

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.
RAW_BUFFERClick to expand / collapse
<!-- internal -->

Note: this description has been consolidated to reflect the full diagnosis. The earlier comments are the investigation trail; this body is the current, accurate summary.

Summary

On Claude Code 2.1.154, a session driven through the cloud bridge / remote control can return:

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.

The error is intermittent and self-recovering during live interactive use, but becomes a permanent, unrecoverable wedge when it fires during an unattended continuation (a scheduled wakeup, a backgrounded subagent returning, or a remote re-drive with no human reply). A wedged session returns the identical 400 on every subsequent turn — including new user messages — and only manual .jsonl surgery brings it back, after which it re-breaks on the next disk-reloaded turn.

Environment

  • Claude Code 2.1.154, macOS, entrypoint: cli
  • Session mirrored through the cloud bridge (transcript contains bridge-session records) — i.e. remote-controlled from another device
  • Extended thinking enabled

Root cause

Thinking blocks are persisted to disk as {"type":"thinking","thinking":"","signature":"..."}empty text, real signature — in every version. This is normal and is not the problem by itself.

The 400 fires when the latest assistant message is reconstructed from the stripped-on-disk transcript (empty text under a live signature) and re-sent. The API validates the signature against the now-empty content, the seal fails, and the request is rejected. A normal interactive turn instead rebuilds the request from the live in-memory message objects, which still hold the full thinking text, so the signature validates and the request passes.

What makes the difference between transient and terminal is whether a live, un-reloaded in-memory copy of the original thinking still exists in the process:

  • Process continuously alive, never reloaded from disk → full thinking text still in memory → every turn (human or automated) rebuilds from it and succeeds. The 400 may flicker but always recovers.
  • Process reloads from disk — a scheduled wakeup firing, --resume, or a remote/bridge re-drive — → the good in-memory copy is overwritten by the stripped on-disk version → there is no longer an un-stripped copy anywhere → permanent. From this point no turn of any kind can fix it, including a fresh human message, because the recovery target no longer exists.

This is why a human message does not rescue an already-wedged session: by the time it arrives, the disk reload has already discarded the only good copy. It also explains why truncating the transcript and resuming only buys a single turn — --resume is itself a disk reload, so the session comes back living off stripped state; the first fresh turn generates new in-memory thinking and works, then the next disk-rebuilt turn re-breaks.

Version bisect

Count of bridge round-trips vs. count of thinking ... cannot be modified 400s, across sessions on one machine:

VersionBridge round-tripsthinking-400s
2.1.13100
2.1.1501220
2.1.150870
2.1.152570
2.1.154173 (recovered — interactive)
2.1.1547wedged (unattended wakeup)

Heavy bridge use on 2.1.150–2.1.152 (57–122 round-trips) never reproduces it. Both 2.1.154 bridge sessions do. This isolates the defect to a replay/reconstruction change in 2.1.154: earlier builds stripped or re-hydrated the latest-message thinking block before replay; 2.1.154 sends the empty-text + signature block verbatim.

Reproduction

  1. On 2.1.154, run a thinking-enabled session and enable the cloud bridge / remote control.
  2. Trigger an unattended continuation while the latest assistant turn contains a thinking block — e.g. a ScheduleWakeup/scheduled wakeup, a backgrounded subagent returning, or a remote re-drive with no human reply.
  3. The continuation rebuilds the latest assistant message from the stripped-on-disk transcript and the API returns the 400. With no live human turn in an un-reloaded process to fall back on, every subsequent turn repeats it.

Expected

The replay/reconstruction path should strip or re-hydrate persisted thinking blocks (as builds ≤ 2.1.152 did) so that re-sending a disk-loaded assistant message cannot fail signature validation. At minimum, a session should not be permanently unrecoverable from this state.

Recovery / workaround

  • Recovery: manual .jsonl surgery — truncate to the last record whose parentUuid chain has no dangling tool_use and no trailing thinking block, repoint the leaf, resume. Re-breaks on the next disk-reloaded turn.
  • Workaround: downgrade to 2.1.152, or do not leave a thinking-enabled 2.1.154 session running unattended over the bridge (drive it live; avoid scheduled wakeups and walk-away background work).

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING