fix: boot-time patch for Anthropic thinking block signature corruption#44650
fix: boot-time patch for Anthropic thinking block signature corruption#44650rikisann wants to merge 1 commit intoopenclaw:mainfrom
Conversation
Two bugs in @mariozechner/pi-ai cause Anthropic API rejections in multi-turn conversations with extended thinking enabled: 1. transform-messages.js keeps ALL thinking blocks across the entire conversation history. Older blocks accumulate, waste context tokens, and can be corrupted by compaction. 2. anthropic.js runs sanitizeSurrogates() on thinking text, which can strip characters and invalidate the cryptographic signature. This patch applies two fixes at boot time: - Strip thinking blocks from all non-latest assistant messages - Preserve thinking text byte-for-byte for the latest assistant message Fixes openclaw#25347 Fixes openclaw#25194
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 66aa9a8b27
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| }, | ||
| { | ||
| label: "strip old thinking blocks (early return)", | ||
| find: /const transformedContent = assistantMsg\.content\.flatMap\(\(block\) => \{\s*if \(block\.type === "thinking"\) \{/, |
There was a problem hiding this comment.
Make transform-messages patch idempotent
applyAnthropicThinkingPatch() runs on every CLI start, but this regex still matches the already-patched transform-messages.js content, so each run injects another const isLatestAssistant = ... and another if (!isLatestAssistant) line. On the second run this produces duplicate const declarations in the patched dependency, which can fail module parsing the next time the Anthropic transform path is loaded.
Useful? React with 👍 / 👎.
Greptile SummaryThis PR adds a boot-time patching function to Key changes:
Issues found:
Confidence Score: 1/5
Prompt To Fix All With AIThis is a comment left during a code review.
Path: openclaw.mjs
Line: 102-111
Comment:
**Second sub-patch regex matches already-patched code — breaks on every boot after the first**
The `find` regex will still match the patched file, breaking the idempotency guarantee and corrupting `transform-messages.js` on every subsequent start.
After the first successful run, the code in `transform-messages.js` becomes:
```js
const isLatestAssistant = msgIndex === lastAssistantIndex;
const transformedContent = assistantMsg.content.flatMap((block) => {
if (block.type === "thinking") {
if (!isLatestAssistant) { return []; }
```
On the **second boot**, the regex `/const transformedContent = assistantMsg\.content\.flatMap\(\(block\) => \{\s*if \(block\.type === "thinking"\) \{/` still matches because:
1. `const transformedContent = assistantMsg.content.flatMap((block) => {` is still present in the file.
2. `\s*` happily matches the newline + leading spaces that follow.
3. `if (block.type === "thinking") {` is still there.
The replacement is applied a second time, producing a duplicate `const isLatestAssistant` variable declaration — a hard `SyntaxError` — and a duplicate `if (!isLatestAssistant) { return []; }` guard. From that point on, `@mariozechner/pi-ai` fails to load entirely, which is a worse outcome than the original bug.
The fix is to make the `find` pattern unique to the **un-patched** form of the code — e.g., include a fragment that is present in the original but absent after patching. The simplest approach is to anchor the regex on the line that immediately **follows** `if (block.type === "thinking") {` in the original file (whatever appears there before `isLatestAssistant` is inserted), or to add a negative lookahead for `isLatestAssistant`:
```js
find: /const transformedContent = assistantMsg\.content\.flatMap\(\(block\) => \{\s*if \(block\.type === "thinking"\) \{(?!\s*if \(!isLatestAssistant\))/,
```
This ensures the regex does not match once the early-return guard has already been inserted.
How can I resolve this? If you propose a fix, please make it concise.
---
This is a comment left during a code review.
Path: openclaw.mjs
Line: 5-6
Comment:
**Duplicate import from `node:module`**
`"node:module"` is imported twice — once for the default export `module` (line 5, pre-existing) and once for the named export `createRequire` (line 6, new). These can be consolidated into a single import statement:
```suggestion
import module, { createRequire } from "node:module";
```
How can I resolve this? If you propose a fix, please make it concise.Last reviewed commit: 66aa9a8 |
| { | ||
| label: "strip old thinking blocks (early return)", | ||
| find: /const transformedContent = assistantMsg\.content\.flatMap\(\(block\) => \{\s*if \(block\.type === "thinking"\) \{/, | ||
| replace: [ | ||
| "const isLatestAssistant = msgIndex === lastAssistantIndex;", | ||
| " const transformedContent = assistantMsg.content.flatMap((block) => {", | ||
| ' if (block.type === "thinking") {', | ||
| " if (!isLatestAssistant) { return []; }", | ||
| ].join("\n"), | ||
| }, |
There was a problem hiding this comment.
Second sub-patch regex matches already-patched code — breaks on every boot after the first
The find regex will still match the patched file, breaking the idempotency guarantee and corrupting transform-messages.js on every subsequent start.
After the first successful run, the code in transform-messages.js becomes:
const isLatestAssistant = msgIndex === lastAssistantIndex;
const transformedContent = assistantMsg.content.flatMap((block) => {
if (block.type === "thinking") {
if (!isLatestAssistant) { return []; }On the second boot, the regex /const transformedContent = assistantMsg\.content\.flatMap\(\(block\) => \{\s*if \(block\.type === "thinking"\) \{/ still matches because:
const transformedContent = assistantMsg.content.flatMap((block) => {is still present in the file.\s*happily matches the newline + leading spaces that follow.if (block.type === "thinking") {is still there.
The replacement is applied a second time, producing a duplicate const isLatestAssistant variable declaration — a hard SyntaxError — and a duplicate if (!isLatestAssistant) { return []; } guard. From that point on, @mariozechner/pi-ai fails to load entirely, which is a worse outcome than the original bug.
The fix is to make the find pattern unique to the un-patched form of the code — e.g., include a fragment that is present in the original but absent after patching. The simplest approach is to anchor the regex on the line that immediately follows if (block.type === "thinking") { in the original file (whatever appears there before isLatestAssistant is inserted), or to add a negative lookahead for isLatestAssistant:
find: /const transformedContent = assistantMsg\.content\.flatMap\(\(block\) => \{\s*if \(block\.type === "thinking"\) \{(?!\s*if \(!isLatestAssistant\))/,This ensures the regex does not match once the early-return guard has already been inserted.
Prompt To Fix With AI
This is a comment left during a code review.
Path: openclaw.mjs
Line: 102-111
Comment:
**Second sub-patch regex matches already-patched code — breaks on every boot after the first**
The `find` regex will still match the patched file, breaking the idempotency guarantee and corrupting `transform-messages.js` on every subsequent start.
After the first successful run, the code in `transform-messages.js` becomes:
```js
const isLatestAssistant = msgIndex === lastAssistantIndex;
const transformedContent = assistantMsg.content.flatMap((block) => {
if (block.type === "thinking") {
if (!isLatestAssistant) { return []; }
```
On the **second boot**, the regex `/const transformedContent = assistantMsg\.content\.flatMap\(\(block\) => \{\s*if \(block\.type === "thinking"\) \{/` still matches because:
1. `const transformedContent = assistantMsg.content.flatMap((block) => {` is still present in the file.
2. `\s*` happily matches the newline + leading spaces that follow.
3. `if (block.type === "thinking") {` is still there.
The replacement is applied a second time, producing a duplicate `const isLatestAssistant` variable declaration — a hard `SyntaxError` — and a duplicate `if (!isLatestAssistant) { return []; }` guard. From that point on, `@mariozechner/pi-ai` fails to load entirely, which is a worse outcome than the original bug.
The fix is to make the `find` pattern unique to the **un-patched** form of the code — e.g., include a fragment that is present in the original but absent after patching. The simplest approach is to anchor the regex on the line that immediately **follows** `if (block.type === "thinking") {` in the original file (whatever appears there before `isLatestAssistant` is inserted), or to add a negative lookahead for `isLatestAssistant`:
```js
find: /const transformedContent = assistantMsg\.content\.flatMap\(\(block\) => \{\s*if \(block\.type === "thinking"\) \{(?!\s*if \(!isLatestAssistant\))/,
```
This ensures the regex does not match once the early-return guard has already been inserted.
How can I resolve this? If you propose a fix, please make it concise.| import module from "node:module"; | ||
| import { createRequire } from "node:module"; |
There was a problem hiding this comment.
Duplicate import from node:module
"node:module" is imported twice — once for the default export module (line 5, pre-existing) and once for the named export createRequire (line 6, new). These can be consolidated into a single import statement:
| import module from "node:module"; | |
| import { createRequire } from "node:module"; | |
| import module, { createRequire } from "node:module"; |
Prompt To Fix With AI
This is a comment left during a code review.
Path: openclaw.mjs
Line: 5-6
Comment:
**Duplicate import from `node:module`**
`"node:module"` is imported twice — once for the default export `module` (line 5, pre-existing) and once for the named export `createRequire` (line 6, new). These can be consolidated into a single import statement:
```suggestion
import module, { createRequire } from "node:module";
```
How can I resolve this? If you propose a fix, please make it concise.
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.
Problem
Extended thinking sessions consistently fail after several turns with:
Two independent bugs in
@mariozechner/pi-aicombine to cause this:Bug 1: Thinking block accumulation (
transform-messages.js)transformMessages()preserves all thinking blocks across the entire conversation history. Anthropic only validates the latest assistant message's thinking blocks — older ones serve no purpose but:Bug 2: Signature invalidation via sanitization (
anthropic.js)convertMessages()runssanitizeSurrogates()on thinking text before sending it back to the API. This function strips lone surrogates and other characters, modifying the text that Anthropic's cryptographic signature was computed over. The signature check fails, and the API rejects the request.Fix
Two boot-time patches applied before main module load:
Patch 1 (
transform-messages.js): Strip thinking blocks from all assistant messages except the latest one. Anthropic only validates the latest — older blocks are dead weight.Patch 2 (
anthropic.js): SkipsanitizeSurrogates()on thinking text, preserving it byte-for-byte so the signature remains valid.Both patches are applied atomically (temp file + rename) with graceful handling for read-only filesystems and already-patched installations.
Relationship to #27965
PR #27965 by @chocothunder5013 addresses Bug 2 (sanitization) with a similar boot-time approach. This PR is a superset — it fixes both bugs:
Testing
Tested on a production OpenClaw deployment with
thinking=highacross dozens of multi-turn sessions over 12+ hours. Before the fix: sessions crashed within 5-10 turns. After: no thinking block errors observed.Files Changed
openclaw.mjs— boot-time patch function applied before main module loadFixes #25347
Fixes #25194
Supersedes #27965