You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
[BUG] stripReasoningFromMergedAssistants mutates thinking/redacted_thinking blocks in the last assistant message, causing Anthropic 400 #161
stripReasoningFromMergedAssistants has no guard for the last (most recent) assistant message. When the last assistant message is not firstInRun — i.e., the preceding message also has role: "assistant" — every thinking and redacted_thinking block in it is replaced with makeSentinel() ({ type: "text", text: "" }). The next API request carries these mutated blocks and Anthropic rejects the request with:
400: messages.1.content.80: `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
// dist/index.js ~line 185009functionstripReasoningFromMergedAssistants(messages,providerID){if(providerID!=='anthropic')return0;for(constmessageofmessages){// NO last-message guard — iterates every message including the most recent oneconstfirstInRun=prevRole!=='assistant';// if prevRole === 'assistant' → firstInRun = false → keepIndex = -1// → ALL thinking/redacted_thinking blocks get sentinelized:message.parts[i]=makeSentinel(part);// { type: 'text', text: '' }}}
Anthropic's constraint applies to blocks returned by the most recent API response — any mutation of those blocks in the subsequent request triggers the 400. The function must skip the last assistant message (or any message whose tag number is above the protected-tag watermark).
Reproduction
Use an Anthropic model with extended thinking enabled in OpenCode.
Run a prompt that produces a thinking block in the last assistant turn.
Trigger a slash command (e.g. /ctx_reduce, /handoff, any magic-context command) that causes experimental.chat.messages.transform to fire.
If the previous message in the conversation also has role: "assistant", the next API call fails with the 400 above.
Expected behaviour
stripReasoningFromMergedAssistants must never sentinelize thinking blocks in the most recent assistant message. A minimal fix:
for(letmsgIdx=0;msgIdx<messages.length;msgIdx++){constmessage=messages[msgIdx];constisLastAssistantMessage=msgIdx===messages.length-1&&message.info.role==='assistant';if(isLastAssistantMessage){prevRole=message.info.role;continue;// never touch the last assistant message's thinking blocks}// ...existing logic...}
A more precise fix would use the protectedTags watermark already threaded through other parts of the pipeline, so the guard is tag-based rather than position-based.
Severity
P0 — triggers on every slash command invocation when extended thinking is active and the last assistant message is not firstInRun.
Status update after a deep pass on this class: the practical bite of this report was addressed in v0.26.0 — reasoning-clearing was reworked (clearOldReasoning) so cleared thinking replays deterministically and redacted blocks are skipped, and several adjacent merged-run bugs have been fixed since (empty-sentinel position handling in the first-non-metadata rule, redacted-thinking preservation on Pi).
What remains of the exact scenario you describe (last assistant not first-in-run getting its thinking neutralized before the provider merge) is a wire-topology modeling problem: doing it perfectly in the TS plugin would mean re-implementing OpenCode's full provider serialization pipeline, which drifts across OpenCode versions. We've verified the failure is theoretical on current versions — we have never reproduced a live 400 from this path since v0.26 — and the accurate fix is landing in our serializer-aware transform core rather than as another TS patch.
Keeping this open until that lands. If you (or anyone) hits a live Anthropic 400 from this path on ≥v0.26, please post the doctor --issue output here — a real repro would raise its priority immediately.
Your diagnosis was correct. The workaround for merged assistant runs strips reasoning from all but the first assistant, and it was applying that to the newest assistant message too — which Anthropic rejects outright, since a signed thinking block in the live turn cannot be rewritten.
The newest assistant is now exempt from that path, on both the TypeScript and Rust legs. The two had drifted: the Rust side already carried the exemption while the TypeScript one did not, so the same session could behave differently depending on which transform was serving it. Both now share one predicate, with regression coverage on each.
Apologies this sat open as long as it did. Thanks for the precise report — naming the specific block types and the resulting 400 is what made it directly actionable.
Activity
fix(reasoning): gate typed reasoning clearing to canonical Anthropic (#…
fix(reasoning): gate typed reasoning clearing to canonical Anthropic (c…
alfonso-magic-context commentedon Jul 15, 2026
Status update after a deep pass on this class: the practical bite of this report was addressed in v0.26.0 — reasoning-clearing was reworked (
clearOldReasoning) so cleared thinking replays deterministically and redacted blocks are skipped, and several adjacent merged-run bugs have been fixed since (empty-sentinel position handling in the first-non-metadata rule, redacted-thinking preservation on Pi).What remains of the exact scenario you describe (last assistant not first-in-run getting its thinking neutralized before the provider merge) is a wire-topology modeling problem: doing it perfectly in the TS plugin would mean re-implementing OpenCode's full provider serialization pipeline, which drifts across OpenCode versions. We've verified the failure is theoretical on current versions — we have never reproduced a live 400 from this path since v0.26 — and the accurate fix is landing in our serializer-aware transform core rather than as another TS patch.
Keeping this open until that lands. If you (or anyone) hits a live Anthropic 400 from this path on ≥v0.26, please post the doctor --issue output here — a real repro would raise its priority immediately.
fix(#161): exempt the newest assistant's reasoning from merged-run mu…
docs: v0.36.1 notes — emergency-band reclaim fix, #161, #301
alfonso-magic-context commentedon Aug 12, 2026
Fixed in v0.36.1.
Your diagnosis was correct. The workaround for merged assistant runs strips reasoning from all but the first assistant, and it was applying that to the newest assistant message too — which Anthropic rejects outright, since a signed thinking block in the live turn cannot be rewritten.
The newest assistant is now exempt from that path, on both the TypeScript and Rust legs. The two had drifted: the Rust side already carried the exemption while the TypeScript one did not, so the same session could behave differently depending on which transform was serving it. Both now share one predicate, with regression coverage on each.
Apologies this sat open as long as it did. Thanks for the precise report — naming the specific block types and the resulting 400 is what made it directly actionable.