Skip to content

fix(provider): preserve assistant message content when reasoning blocks present#21370

Open
edevil wants to merge 1 commit intoanomalyco:devfrom
edevil:fix/preserve-thinking-block-signatures
Open

fix(provider): preserve assistant message content when reasoning blocks present#21370
edevil wants to merge 1 commit intoanomalyco:devfrom
edevil:fix/preserve-thinking-block-signatures

Conversation

@edevil
Copy link
Copy Markdown
Contributor

@edevil edevil commented Apr 7, 2026

Issue for this PR

Closes #16748

Type of change

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

What does this PR do?

normalizeMessages() in transform.ts unconditionally filters out empty text parts from all message roles, including assistant. When Anthropic adaptive thinking (Opus 4.6, Sonnet 4.6) emits a whitespace-only text part between two reasoning blocks, processor.ts trims it to "", and then normalizeMessages removes it. This shifts the block arrangement from [thinking, text, thinking, text, tool_use] to [thinking, thinking, text, tool_use], invalidating the positionally-sensitive cryptographic signatures on the thinking blocks. The Anthropic API then rejects with: "thinking blocks in the latest assistant message cannot be modified".

The fix is one line: skip the empty-text filter for assistant messages that contain reasoning blocks. These messages must be replayed verbatim since thinking block signatures encode positional context. Assistant messages without reasoning blocks (e.g. plain text responses, compaction summaries) continue to be filtered normally.

if (msg.role === "assistant" && msg.content.some((part) => part.type === "reasoning")) return msg

The empty-text filter was added on Jan 5 (c285304a) before adaptive thinking existed (Feb 13, 0d90a22f9), so it was correct at the time. The bug is an emergent interaction with Opus 4.6's signature-sensitive thinking blocks.

How did you verify your code works?

  • All 123 transform tests pass (0 failures)
  • Typecheck passes across all 13 packages
  • New test reproduces the exact issue scenario: [reasoning(sig1), text(""), reasoning(sig2), text("answer"), tool_use] — asserts all 5 parts are preserved
  • 3 existing tests updated to match the corrected behavior
  • 1 new test added for non-assistant message filtering (regression guard)

Screenshots / recordings

N/A — backend logic change, no UI impact.

Checklist

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

…ks present

normalizeMessages() unconditionally filtered empty text parts from all
message roles, including assistant. When Anthropic adaptive thinking
(Opus 4.6, Sonnet 4.6) emits an empty text part between two reasoning
blocks, removing it shifts thinking block positions and invalidates
the cryptographic signatures, causing the API to reject with:
'thinking blocks in the latest assistant message cannot be modified'

Skip the empty-text filter for assistant messages that contain reasoning
blocks. These messages must be replayed verbatim since thinking block
signatures encode positional context. Assistant messages without
reasoning blocks continue to be filtered normally.

Closes anomalyco#16748
@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Apr 7, 2026

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

Potential Duplicate Found:

This PR appears to be directly related or a potential duplicate. It addresses the same issue (#16748) with the same root cause (empty-text filtering in normalizeMessages affecting assistant messages with reasoning blocks). Both PRs target the same problem with Anthropic's adaptive thinking signatures.

@edevil
Copy link
Copy Markdown
Contributor Author

edevil commented Apr 7, 2026

It seems this error message predates adaptive thinking so it must be something else.

@edevil edevil closed this Apr 7, 2026
@edevil edevil reopened this Apr 7, 2026
@edevil
Copy link
Copy Markdown
Contributor Author

edevil commented Apr 8, 2026

I managed to confirm with a past session that this does indeed fix the issue in question.

@github-actions github-actions bot mentioned this pull request Apr 8, 2026
6 tasks
fairyhunter13 added a commit to fairyhunter13/opencode that referenced this pull request Apr 10, 2026
Port anomalyco/sst upstream dev's proven sync architecture to permanently
fix the flaky "blank subagent view" bug where ctrl+x down into a running
subagent showed an empty content area until completion.

sync.tsx:
- Remove `force` parameter from session.sync()
- Always mark fullSyncedSessions after fetch (drop time.completed gate)
- Remove redundant session.created handler (session.updated upserts)
- Match upstream/dev byte-for-byte in sync-relevant sections

routes/session/index.tsx:
- Task component: createEffect -> onMount with empty-messages guard
- Route-level sync: drop force=true argument

Preserved defensively (ahead of upstream):
- worker.ts + server/routes/event.ts still use GlobalBus.on("event", ...)
  as defense-in-depth on top of InstanceState shared PubSub
- worker.ts WASM stdout/stderr capture to suppress Aborted() TUI corruption
- worker.ts per-directory filter in GlobalBus.on handler

provider.ts:
- Raise FLOOR_CEILINGS.critical from 4 to 10 for non-Anthropic providers
- Expand isAnthropicParent to also match parentModelID containing "claude"
  (covers claude-via-proxy cases)

transform.ts:
- Preserve assistant messages with reasoning blocks verbatim to avoid
  Anthropic 400 "thinking blocks cannot be modified" (ref upstream PR anomalyco#21370)
- Enable 4-BP cache strategy for GitHub Copilot Claude models

llm.ts:
- Use 5m TTL for short-lived subagent BP2 cache breakpoint; 1h for long-lived

processor.ts:
- Remove duplicate SessionSummary.summarize() call that triggered full-history
  DB hydration on every LLM step in multi-step tool loops (ref upstream anomalyco#21762)

Tests:
- task-escalation.test.ts: update FLOOR_CEILINGS critical assertions to
  match new ceiling (critical > opus, toBe(10)); expand coverage
- cache-max-efficiency-e2e.test.ts: new E2E test validating Anthropic prompt
  cache hit rate across multi-turn conversations

Skill:
- Add opencode-prompt-cache-maximization skill (CLAUDE.md)

Submodule:
- Bump cmd/opencode-search-engine to include idle CPU perf improvements

Root cause: our prior fullSyncedSessions + time.completed guard raced
with Task.createEffect's fire-and-forget sync(id, force=false) -- first
fetch at T=0 returned 0 messages, session was never marked synced,
and no re-sync trigger existed, leaving the view blank until the
subagent finished.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

normalizeMessages() removes empty text parts between reasoning blocks, invalidating Anthropic thinking block signatures

1 participant