fix: strip thinking/redacted_thinking blocks from non-latest assistant messages for Anthropic#39919
Conversation
…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.
Greptile SummaryThis 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 The core logic in
Confidence Score: 3/5
Last reviewed commit: 738d6b6 |
| const isAnthropicProvider = | ||
| params.modelApi === "anthropic-messages" || | ||
| params.modelApi === "bedrock-converse-stream" || | ||
| (params.provider ?? "").toLowerCase() === "anthropic" || | ||
| (params.provider ?? "").toLowerCase() === "amazon-bedrock"; |
There was a problem hiding this 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";
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.|
any updates here? looks like another fix here -> #39940 |
xiaoyaner0201
left a comment
There was a problem hiding this comment.
🔍 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 |
Analysis
After reading all 7 diffs:
#43783 is the strongest candidate. It:
- Creates a standalone
stripThinkingFromNonLatestAssistant()without changingdropThinkingBlockssemantics (preserves Copilot behavior) - Integrates via existing
policy.preserveSignaturesflag (proper transcript policy mechanism) - Only PR that also fixes the compaction path (
compact.ts) — others only fixsanitizeSessionHistory - Handles both
thinkingandredacted_thinkingblock types - 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.
Fixes #39887
Problem
When compaction or session serialization runs on a conversation that uses Anthropic extended thinking,
thinkingandredacted_thinkingcontent blocks in assistant messages can be modified. The Anthropic API then rejects subsequent requests with:This permanently breaks the session since every subsequent API call fails with the same error.
Root Cause
Two issues:
dropThinkingBlocksonly handledtype: "thinking", missingtype: "redacted_thinking"entirely. Anthropic returns both block types and both must be handled.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:
dropThinkingBlocks: Now strips boththinkingandredacted_thinkingblock types.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.sanitizeSessionHistory: For Anthropic providers (anthropic-messages,bedrock-converse-stream,anthropic,amazon-bedrock), appliesstripThinkingFromNonLatestAssistantbefore other sanitization steps.Testing
dropThinkingBlocksto coverredacted_thinkingstripThinkingFromNonLatestAssistant:redacted_thinkingblocksFiles Changed
src/agents/pi-embedded-runner/thinking.ts— Addedredacted_thinkingsupport + newstripThinkingFromNonLatestAssistantfunctionsrc/agents/pi-embedded-runner/thinking.test.ts— Extended testssrc/agents/pi-embedded-runner/google.ts— Apply stripping insanitizeSessionHistoryfor Anthropic