Skip to content

fix: strip thinking/redacted_thinking blocks from non-latest assistant messages for Anthropic#39919

Open
taw0002 wants to merge 1 commit intoopenclaw:mainfrom
taw0002:fix/compaction-thinking-block-corruption
Open

fix: strip thinking/redacted_thinking blocks from non-latest assistant messages for Anthropic#39919
taw0002 wants to merge 1 commit intoopenclaw:mainfrom
taw0002:fix/compaction-thinking-block-corruption

Conversation

@taw0002
Copy link
Copy Markdown
Contributor

@taw0002 taw0002 commented Mar 8, 2026

Fixes #39887

Problem

When compaction or session serialization runs on a conversation that uses Anthropic extended thinking, thinking and redacted_thinking content blocks in assistant messages can be modified. The Anthropic API then rejects subsequent requests with:

thinking or redacted_thinking blocks in the latest assistant message cannot be modified

This permanently breaks the session since every subsequent API call fails with the same error.

Root Cause

Two issues:

  1. dropThinkingBlocks only handled type: "thinking", missing type: "redacted_thinking" entirely. Anthropic returns both block types and both must be handled.

  2. No mechanism to protect thinking blocks from compaction corruption. The pi-agent-core SDK's compaction/serialization cycle can modify thinking block contents (field ordering, normalization, etc.), making them non-identical to the original API response. Anthropic requires these blocks to be byte-identical in the latest assistant message.

Fix

Three changes:

  1. dropThinkingBlocks: Now strips both thinking and redacted_thinking block types.

  2. stripThinkingFromNonLatestAssistant (new function): Strips thinking/redacted_thinking blocks from all assistant messages except the latest one. Anthropic explicitly allows omitting these blocks from non-latest messages. This is defensive: rather than trying to preserve exact byte representation through the compaction pipeline (which crosses SDK boundaries), we remove blocks from messages where they're not required.

  3. sanitizeSessionHistory: For Anthropic providers (anthropic-messages, bedrock-converse-stream, anthropic, amazon-bedrock), applies stripThinkingFromNonLatestAssistant before other sanitization steps.

Testing

  • Updated existing tests for dropThinkingBlocks to cover redacted_thinking
  • Added comprehensive test suite for stripThinkingFromNonLatestAssistant:
    • Preserves thinking blocks in the latest assistant message
    • Strips from all non-latest assistant messages
    • Handles redacted_thinking blocks
    • Handles interleaved user/toolResult messages
    • Replaces with empty text block when all content was thinking
    • Returns original reference when no changes needed

Files Changed

  • src/agents/pi-embedded-runner/thinking.ts — Added redacted_thinking support + new stripThinkingFromNonLatestAssistant function
  • src/agents/pi-embedded-runner/thinking.test.ts — Extended tests
  • src/agents/pi-embedded-runner/google.ts — Apply stripping in sanitizeSessionHistory for Anthropic

…t messages for Anthropic

Fixes openclaw#39887

When compaction or session serialization modifies thinking/redacted_thinking
content blocks in assistant messages, the Anthropic API rejects the next
request because these blocks must remain byte-identical to the original
response in the latest assistant message.

Changes:

1. **dropThinkingBlocks**: Now strips both `type: "thinking"` and
   `type: "redacted_thinking"` blocks (previously only handled
   `thinking`).

2. **stripThinkingFromNonLatestAssistant** (new): Strips thinking and
   redacted_thinking blocks from all assistant messages EXCEPT the latest
   one. Anthropic allows omitting these blocks from non-latest messages
   but requires the latest message's blocks to be unmodified. This
   prevents compaction-corrupted blocks from reaching the API.

3. **sanitizeSessionHistory**: For Anthropic providers, applies
   stripThinkingFromNonLatestAssistant before other sanitization steps.
   This ensures that even if compaction modifies thinking blocks in older
   messages, they are cleanly removed before the API call.

The fix is defensive: rather than trying to preserve exact byte
representation through compaction (which crosses the pi-agent-core SDK
boundary), we strip blocks that Anthropic doesn't require from older
messages and preserve only the latest assistant's blocks which haven't
been through compaction yet.
@openclaw-barnacle openclaw-barnacle bot added agents Agent runtime and tooling size: M labels Mar 8, 2026
@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps bot commented Mar 8, 2026

Greptile Summary

This PR fixes a session-breaking bug where Anthropic extended thinking blocks were being corrupted during compaction/serialization, causing subsequent API calls to fail. It does so by (1) extending dropThinkingBlocks to also handle redacted_thinking blocks, and (2) introducing stripThinkingFromNonLatestAssistant to proactively remove thinking blocks from non-latest assistant messages (where Anthropic allows omission), applied in sanitizeSessionHistory for Anthropic-backed providers.

The core logic in thinking.ts and its test coverage are well-implemented. One issue was found:

  • Provider alias gap in google.ts: The new isAnthropicProvider check compares provider strings with .toLowerCase() directly, but the rest of the codebase uses normalizeProviderId() which maps aliases like "bedrock" and "aws-bedrock" to "amazon-bedrock". Users whose provider is configured as "bedrock" or "aws-bedrock" will not have thinking blocks stripped from non-latest messages, leaving them exposed to the same bug this PR intends to fix.

Confidence Score: 3/5

  • PR is mostly safe to merge but has a provider normalization gap that leaves some Bedrock users unprotected.
  • The core fix in thinking.ts is correct and well-tested. The integration in google.ts is architecturally sound, but the inline isAnthropicProvider check bypasses the existing normalizeProviderId utility used consistently everywhere else in the codebase. This means provider aliases "bedrock" and "aws-bedrock" are not matched, so affected users would still see the session-breaking error this PR aims to resolve.
  • src/agents/pi-embedded-runner/google.ts — the isAnthropicProvider check at lines 557–561 should use normalizeProviderId for consistency with the rest of the codebase.

Last reviewed commit: 738d6b6

Comment on lines +557 to +561
const isAnthropicProvider =
params.modelApi === "anthropic-messages" ||
params.modelApi === "bedrock-converse-stream" ||
(params.provider ?? "").toLowerCase() === "anthropic" ||
(params.provider ?? "").toLowerCase() === "amazon-bedrock";
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Provider aliases not normalized — "bedrock" / "aws-bedrock" users won't be protected

The new isAnthropicProvider check compares the raw provider string with .toLowerCase(), but the rest of the codebase uses normalizeProviderId() (from model-selection.ts) which has additional alias mappings:

  • "bedrock""amazon-bedrock"
  • "aws-bedrock""amazon-bedrock"

This means that if a user has configured provider: "bedrock" or provider: "aws-bedrock", isAnthropicProvider evaluates to false and stripThinkingFromNonLatestAssistant is never called — leaving those sessions vulnerable to the same bug this PR is fixing.

The existing isAnthropicApi function already exists in transcript-policy.ts (lines 57–64) and uses normalizeProviderId correctly, but it is not exported. The fix is either to export isAnthropicApi and reuse it here, or to apply normalizeProviderId in the inline check:

const normalizedProvider = normalizeProviderId(params.provider ?? "");
const isAnthropicProvider =
  params.modelApi === "anthropic-messages" ||
  params.modelApi === "bedrock-converse-stream" ||
  normalizedProvider === "anthropic" ||
  normalizedProvider === "amazon-bedrock";
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/agents/pi-embedded-runner/google.ts
Line: 557-561

Comment:
**Provider aliases not normalized — `"bedrock"` / `"aws-bedrock"` users won't be protected**

The new `isAnthropicProvider` check compares the raw provider string with `.toLowerCase()`, but the rest of the codebase uses `normalizeProviderId()` (from `model-selection.ts`) which has additional alias mappings:

- `"bedrock"``"amazon-bedrock"`
- `"aws-bedrock"``"amazon-bedrock"`

This means that if a user has configured `provider: "bedrock"` or `provider: "aws-bedrock"`, `isAnthropicProvider` evaluates to `false` and `stripThinkingFromNonLatestAssistant` is never called — leaving those sessions vulnerable to the same bug this PR is fixing.

The existing `isAnthropicApi` function already exists in `transcript-policy.ts` (lines 57–64) and uses `normalizeProviderId` correctly, but it is not exported. The fix is either to export `isAnthropicApi` and reuse it here, or to apply `normalizeProviderId` in the inline check:

```
const normalizedProvider = normalizeProviderId(params.provider ?? "");
const isAnthropicProvider =
  params.modelApi === "anthropic-messages" ||
  params.modelApi === "bedrock-converse-stream" ||
  normalizedProvider === "anthropic" ||
  normalizedProvider === "amazon-bedrock";
```

How can I resolve this? If you propose a fix, please make it concise.

@coletoncodes
Copy link
Copy Markdown

any updates here?

looks like another fix here -> #39940

Copy link
Copy Markdown
Contributor

@xiaoyaner0201 xiaoyaner0201 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔍 Duplicate PR Group: Anthropic Thinking Block Compaction

This PR is one of 7 open PRs addressing the same issue: Anthropic thinking/redacted_thinking blocks from older assistant messages cause API errors during context compaction.

Related PRs

PR Author Date Approach Preserves latest redacted_thinking Compaction path Tests Clean diff
#24261 @Vaibhavee89 02-23 Guard functions + logging N/A (no actual strip) Detect only Medium
#25381 @Nipurn123 02-24 Modify dropThinkingBlocks skip latest Good ❌ (bundles sendDice)
#27142 @slatem 02-26 Extend dropThinkingBlocks to Anthropic ❌ (strips all) Limited
#39919 @taw0002 03-08 New strip function in sanitizeSessionHistory Good
#39940 @liangruochong44-ui 03-08 Policy flag for Anthropic ❌ (strips all) Medium ❌ (bundles cron/SQLite/UI)
#43783 @coletoncodes 03-12 New stripThinkingFromNonLatestAssistant + compact path Best
#44650 @rikisann 03-13 Runtime monkey-patch of node_modules None ⚠️ Fragile approach

Analysis

After reading all 7 diffs:

#43783 is the strongest candidate. It:

  1. Creates a standalone stripThinkingFromNonLatestAssistant() without changing dropThinkingBlocks semantics (preserves Copilot behavior)
  2. Integrates via existing policy.preserveSignatures flag (proper transcript policy mechanism)
  3. Only PR that also fixes the compaction path (compact.ts) — others only fix sanitizeSessionHistory
  4. Handles both thinking and redacted_thinking block types
  5. Clean diff with comprehensive tests (toolResult interleaving, empty blocks, reference equality)

#39919 is a close second (same core function, clean diff, good tests) but misses the compaction path.

Notable discovery from #44650: sanitizeSurrogates may corrupt thinking block signatures — worth investigating separately even though the monkey-patch approach is not viable.

Not recommended: #27142 and #39940 strip thinking from ALL messages including latest (breaks multi-turn quality). #25381 and #39940 bundle unrelated changes.

Similarities identified via embedding-based clustering (cosine > 0.82). Maintainers: #43783 appears ready for review.

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

Labels

agents Agent runtime and tooling size: M

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Compaction corrupts thinking/redacted_thinking blocks, breaking Anthropic API sessions

3 participants