Skip to content

Thinking block signature corruption: shouldPreserveThinkingBlocks() + delete on shared object references #90483

Description

@JesicaMacBot

Bug Report: Thinking block signature corruption — shouldPreserveThinkingBlocks() + destructive delete on shared object references

Repository: https://github.com/openclaw/openclaw
Version: 2026.5.27
Files:

  • dist/provider-replay-helpers-B816pw0N.jsshouldPreserveThinkingBlocks()
  • dist/chat-display-projection-C0lsXGny.jsdelete entry.thinkingSignature
  • dist/openclaw-tools-BUQsixTe.jsdelete entry.thinkingSignature

Severity: High — causes 400: Invalid signature in thinking block on every turn after a compaction, /new session, or any replay through truncation/display projection code paths


Summary

Two separate issues in OpenClaw combine to produce reliable, unrecoverable thinking-block signature invalidation:

  1. shouldPreserveThinkingBlocks() returns true for Claude 4.x models (Sonnet 4, Opus 4, Haiku 4), instructing OpenClaw to keep thinking blocks in conversation context across turns. This is the prerequisite: thinking blocks must remain in context for the signature corruption to reach the Anthropic API.

  2. delete entry.thinkingSignature in both chat-display-projection-*.js and openclaw-tools-*.js operates on object references rather than on deep-copied entries. When the same message object is referenced by both the live conversation history and the display/truncation projection pipeline, the delete mutates the shared object, stripping thinkingSignature from the live history entry. On the next turn, the thinking block is sent to Anthropic without its signature, which Anthropic rejects with 400: Invalid signature in thinking block.

The failure is triggered by any code path that runs the projection or truncation pipeline while holding a reference to the same block objects used in the replay context — including normal turn-over-turn operation at longer context lengths.


Issue 1: shouldPreserveThinkingBlocks() returns true for Claude 4.x

File: dist/provider-replay-helpers-B816pw0N.js (~line 53)

function shouldPreserveThinkingBlocks(modelId) {
    const id = normalizeLowercaseStringOrEmpty(modelId);
    if (!id.includes("claude")) return false;
    if (id.includes("opus-4") || id.includes("sonnet-4") || id.includes("haiku-4")) return true;
    if (/claude-[5-9]/.test(id) || /claude-\d{2,}/.test(id)) return true;
    return false;
}

When this returns true, OpenClaw sets dropThinkingBlocks: false in the replay policy, meaning thinking blocks are preserved in the messages sent to Anthropic on subsequent turns. This is the correct intent per Anthropic docs (Claude 4.x natively supports thinking blocks in context), but it exposes all the downstream mutation bugs below — any code path that mutates a thinking block's thinkingSignature after this point will corrupt the replay.

Used in:

// attempt.tool-run-context-*.js
...isAnthropic && modelId.includes("claude")
  ? { dropThinkingBlocks: !shouldPreserveThinkingBlocks(modelId) }
  : {}

Issue 2: delete entry.thinkingSignature on shared object references

File: dist/chat-display-projection-C0lsXGny.js (~line 81)

if ("thinkingSignature" in entry) {
    delete entry.thinkingSignature;   // ← mutates the live object
    changed = true;
}

File: dist/openclaw-tools-BUQsixTe.js (~line 8364)

if (type === "thinking") {
    // ...truncate entry.thinking...
    if ("thinkingSignature" in entry) {
        delete entry.thinkingSignature;   // ← mutates the live object
        truncated = true;
    }
}

Both functions are designed for display projection or context truncation — they should produce a view of the data, not mutate the source. But if the caller passes a reference to the same objects held in the replay context (rather than a deep copy), delete entry.thinkingSignature removes the signature from the live history entry.

On the next API call, OpenClaw sends the thinking block to Anthropic with thinkingSignature absent (or the block gets misclassified as an unsigned block and converted to plain text), producing:

400 Bad Request: Invalid signature in thinking block

This failure is deterministic once triggered. The session cannot recover without discarding the corrupted turn(s).


Reproduction

  1. Configure a Claude 4.x model (Sonnet 4, Opus 4, or Haiku 4) with extended thinking enabled.
  2. Conduct a multi-turn conversation until the context is long enough that display projection or truncation runs on a turn boundary.
  3. The next turn will produce 400: Invalid signature in thinking block.

In practice, this manifests as session freezes, /new session being required after N turns, or intermittent 400 errors after compaction events. The frequency increases with context length.


Root Cause Analysis

The architecture assumes that display/projection/truncation pipelines work on copies of message data. That assumption breaks whenever the pipeline receives references to the same JavaScript objects held in the live replay context. The delete is not the wrong operation in isolation — it is wrong because it mutates shared state.

The correct fix is either:

  • Deep-clone entries before passing them into truncation/projection pipelines, or
  • Make the projection functions non-mutating (return new objects rather than modifying in place)

The shouldPreserveThinkingBlocks() issue is a prerequisite: if thinking blocks were always dropped (dropThinkingBlocks: true), they would never reach the replay path and the mutation bug would have no effect. However, the correct long-term fix for Issue 2 is to stop mutating shared references — not to permanently disable thinking block preservation.


Proposed Fixes

Fix 1: Make projection/truncation functions non-mutating

In chat-display-projection-*.js and openclaw-tools-*.js, replace in-place mutation with a shallow copy before deletion:

// BEFORE (mutates shared reference):
if ("thinkingSignature" in entry) {
    delete entry.thinkingSignature;
    changed = true;
}

// AFTER (non-mutating):
if ("thinkingSignature" in entry) {
    entry = { ...entry };          // shallow copy — signature lives only on this entry
    delete entry.thinkingSignature;
    changed = true;
}

Or, if the function is intended to return a projected view, restructure to return new objects rather than modifying the inputs.

Fix 2 (workaround, acceptable trade-off): shouldPreserveThinkingBlocks() returns false unconditionally

Until Issue 2 is resolved, returning false unconditionally prevents thinking blocks from ever reaching the replay context, eliminating the corruption path entirely. The trade-off is loss of prompt-cache prefix matching on Claude 4.x.

// PATCHED (workaround):
function shouldPreserveThinkingBlocks(modelId) {
    return false; // always drop thinking blocks until shared-reference mutation is fixed
}

Patches Applied (Workaround)

Applied as a direct source patch to dist/provider-replay-helpers-B816pw0N.js on OpenClaw 2026.5.27, managed via ~/.openclaw/patches/apply-patches.sh to survive updates:

--- a/dist/provider-replay-helpers-B816pw0N.js
+++ b/dist/provider-replay-helpers-B816pw0N.js
@@ -53,7 +53,7 @@
-function shouldPreserveThinkingBlocks(modelId) {
+function shouldPreserveThinkingBlocks(modelId) { return false; // patched: always drop thinking blocks (bug #2 fix)
     const id = normalizeLowercaseStringOrEmpty(modelId);
     if (!id.includes("claude")) return false;
     if (id.includes("opus-4") || id.includes("sonnet-4") || id.includes("haiku-4")) return true;
     if (/claude-[5-9]/.test(id) || /claude-\d{2,}/.test(id)) return true;
     return false;
 }

The delete entry.thinkingSignature mutation in chat-display-projection-*.js and openclaw-tools-*.js still needs a proper fix upstream regardless of the workaround above, since any future re-enablement of thinking block preservation will reintroduce the corruption.


Environment

  • OpenClaw 2026.5.27
  • macOS 26.5.1 (Apple Silicon, Mac Mini M4)
  • Anthropic Claude Sonnet 4, Claude Opus 4 (both affected; shouldPreserveThinkingBlocks() returns true for both)
  • Extended thinking enabled

Related

  • Upstream bug in @earendil-works/pi-ai 0.75.5: sanitizeSurrogates() called on signed thinking block content (earendil-works/pi — filed separately). The two bugs interact: if either triggers, the session fails. The sanitizeSurrogates bug fires when surrogates are present in thinking content; this bug fires when display/truncation pipelines share object references with the replay context.

Metadata

Metadata

Assignees

No one assigned

    Labels

    P1High-priority user-facing bug, regression, or broken workflow.impact:auth-providerAuth, provider routing, model choice, or SecretRef resolution may break.impact:session-stateSession, memory, transcript, context, or agent state can drift or corrupt.

    Type

    No type

    Fields

    Priority

    None yet

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions