Skip to content

fix: preserve thinking block signatures across three independent corruption paths#21860

Open
chan1103 wants to merge 1 commit intoanomalyco:devfrom
chan1103:fix/preserve-thinking-signatures-v2
Open

fix: preserve thinking block signatures across three independent corruption paths#21860
chan1103 wants to merge 1 commit intoanomalyco:devfrom
chan1103:fix/preserve-thinking-signatures-v2

Conversation

@chan1103
Copy link
Copy Markdown

@chan1103 chan1103 commented Apr 10, 2026

Issue for this PR

Closes #13286
Related: #18078, #16748, #14393

Type of change

  • Bug fix
  • New feature
  • Refactor / code improvement
  • Documentation

What does this PR do?

Extended thinking sessions fail intermittently with "thinking blocks cannot be modified." I traced the message pipeline and found three independent places where signatures get corrupted.

1. differentModel guard strips reasoning metadata (message-v2.ts)

When the current model differs from the stored message's model, toModelMessages drops providerMetadata from all parts — including reasoning parts that carry thinking block signatures. Common with plugin-based model routing.

Fix: reasoning parts always pass their metadata through, bypassing the guard. Text/tool parts keep the guard. This is safe because provider metadata is namespaced (anthropic: { signature }) — other providers ignore unknown keys.

2. trimEnd() mutates reasoning text (processor.ts)

The reasoning-end handler trims trailing whitespace. The signature was computed against the original text, so any mutation invalidates it.

Fix: remove the trimEnd(). Reasoning text arrives final from the stream.

3. normalizeMessages removes signed empty parts (transform.ts)

The Anthropic compat filter (c285304) removes parts where text === "". This catches redacted_thinking blocks — empty text but valid signatures in providerOptions. Removing them shifts positions, and Anthropic validates positionally (#16748).

Fix: keep reasoning parts that have providerOptions. Still remove truly empty ones.

Each cause triggers independently under different conditions, which is why fixing just one doesn't eliminate the error. PR #14393 covers cause 1, PR #12131 covers cause 2 — neither covers cause 3, and this PR covers all three.

How did you verify your code works?

Added 4 tests across 3 files. Each test fails against unpatched code and passes with the fix:

  • message-v2.test.ts: reasoning metadata survives cross-model sessions
  • processor-effect.test.ts: trailing whitespace in reasoning text is preserved
  • transform.test.ts: signed empty reasoning parts survive filtering
  • transform.test.ts: unsigned empty parts still removed (existing behavior)

Full suite: 1867 pass, 0 fail.

Screenshots / recordings

Not a UI change.

Checklist

  • I have tested my changes locally
  • I have not included unrelated changes in this PR

@github-actions github-actions bot added the needs:compliance This means the issue will auto-close after 2 hours. label Apr 10, 2026
@github-actions
Copy link
Copy Markdown
Contributor

The following comment was made by an LLM, it may be inaccurate:

Based on my search, I found several related PRs addressing similar issues. Here are the most relevant ones:

Directly Related (mentioned in the PR description):

  1. PR fix: preserve thinking block signatures and fix compaction headroom asymmetry #14393 - fix: preserve thinking block signatures and fix compaction headroom asymmetry #14393

    • "fix: preserve thinking block signatures and fix compaction headroom asymmetry"
    • Addresses cause 1 by removing the differentModel guard entirely. The current PR takes a narrower approach (reasoning only) and also covers causes 2 and 3.
  2. PR fix(provider): skip empty-text filtering for assistant messages in normalizeMessages (#16748) #16750 - fix(provider): skip empty-text filtering for assistant messages in normalizeMessages (#16748) #16750

Related to thinking block and reasoning content filtering:

  1. PR fix(opencode): filter empty text content blocks for all providers #17742 - fix(opencode): filter empty text content blocks for all providers #17742

    • "fix(opencode): filter empty text content blocks for all providers"
    • Addresses similar empty content filtering issues
  2. PR fix(provider): drop empty content messages after interleaved reasoning filter #17712 - fix(provider): drop empty content messages after interleaved reasoning filter #17712

    • "fix(provider): drop empty content messages after interleaved reasoning filter"
    • Related to empty reasoning parts handling
  3. PR fix(provider): preserve assistant message content when reasoning blocks present #21370 - fix(provider): preserve assistant message content when reasoning blocks present #21370

    • "fix(provider): preserve assistant message content when reasoning blocks present"
    • Directly relevant to preserving reasoning block integrity
  4. PR fix: strip reasoning parts when switching to non-interleaved models #11572 - fix: strip reasoning parts when switching to non-interleaved models #11572

    • "fix: strip reasoning parts when switching to non-interleaved models"
    • Related to model switching and reasoning content

The PR's description already acknowledges the relationship with #14393 and #12131, indicating these are complementary approaches to the same underlying issue.

@github-actions github-actions bot removed the needs:compliance This means the issue will auto-close after 2 hours. label Apr 10, 2026
@github-actions
Copy link
Copy Markdown
Contributor

Thanks for updating your PR! It now meets our contributing guidelines. 👍

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Claude Opus 4.5 (latest) eventually fails: thinking block cannot be modified

1 participant