openclaw - 💡(How to fix) Fix Incomplete thinking blocks (missing signature) persisted to session JSONL → next request fails with `Invalid signature in thinking block`

Official PRs (…)
ON THIS PAGE

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…

When an Anthropic stream is interrupted mid-content_block_delta (e.g. session takeover, network drop, process kill, sessions_yield abort), the partially-streamed thinking block is persisted to the session JSONL without a valid signature. The next request that replays this history is rejected by the Anthropic API with:

LLM request rejected: messages.N.content.M: Invalid `signature` in `thinking` block

The session becomes unusable until the JSONL is manually edited to remove the offending thinking block(s).

Error Message

This recovers the session but loses the reasoning history, and it's a manual step — the user has to know what error to look for and where to find the session file.

Root Cause

The session writer appends messages to the JSONL in a streaming fashion (see src/.../transcript.tsfs.appendFile(transcriptPath, JSON.stringify(entry) + "\n", "utf-8")). When the stream is cut before content_block_stop delivers the opaque signature, the persisted thinking block has an empty/missing signature.

OpenClaw already has the right sanitizer:

// src/agents/pi-embedded-runner/thinking.ts
export function stripInvalidThinkingSignatures(messages: AgentMessage[]): AgentMessage[]

…and it does the right thing (drops thinking blocks where hasReplayableThinkingSignature(block) is false).

The bug is that this sanitizer is conditionally gated, not unconditionally applied to persisted history before replay:

// dist selection-Cr-9-UpD.js around line 8809
const validatedThinkingSignatures = policy.preserveSignatures
  ? stripInvalidThinkingSignatures(sanitizedImages)
  : sanitizedImages;

On some recovery paths (notably after EmbeddedAttemptSessionTakeoverError and certain sessions_yield interrupts), policy.preserveSignatures is not true, so the invalid thinking block survives sanitization and reaches the Anthropic API, which then 400s the whole request.

Fix Action

Fix / Workaround

Workaround (current, manual)

Code Example

LLM request rejected: messages.N.content.M: Invalid `signature` in `thinking` block

---

// src/agents/pi-embedded-runner/thinking.ts
export function stripInvalidThinkingSignatures(messages: AgentMessage[]): AgentMessage[]

---

// dist selection-Cr-9-UpD.js around line 8809
const validatedThinkingSignatures = policy.preserveSignatures
  ? stripInvalidThinkingSignatures(sanitizedImages)
  : sanitizedImages;

---

// Always strip — providers are the authority on signature validity,
// and persisted-but-unsigned blocks are never replayable.
const validatedThinkingSignatures = providerRequiresSignedThinking(model)
  ? stripInvalidThinkingSignatures(sanitizedImages)
  : (policy.preserveSignatures
      ? stripInvalidThinkingSignatures(sanitizedImages)
      : sanitizedImages);

---

import json, sys
from pathlib import Path

path = Path(sys.argv[1])
out_lines = []
for line in path.read_text().splitlines():
    msg = json.loads(line)
    content = msg.get("content")
    if isinstance(content, list):
        msg["content"] = [b for b in content if b.get("type") != "thinking"]
    out_lines.append(json.dumps(msg, ensure_ascii=False))
path.write_text("\n".join(out_lines) + "\n")
RAW_BUFFERClick to expand / collapse

Bug: Incomplete thinking blocks (missing signature) get persisted to session JSONL, causing Invalid signature in thinking block on next request

Summary

When an Anthropic stream is interrupted mid-content_block_delta (e.g. session takeover, network drop, process kill, sessions_yield abort), the partially-streamed thinking block is persisted to the session JSONL without a valid signature. The next request that replays this history is rejected by the Anthropic API with:

LLM request rejected: messages.N.content.M: Invalid `signature` in `thinking` block

The session becomes unusable until the JSONL is manually edited to remove the offending thinking block(s).

Environment

  • OpenClaw: installed via npm at /Users/stellarlink/.npm-global/lib/node_modules/openclaw
  • OS: macOS Darwin 24.6.0 (arm64)
  • Provider: Anthropic (claude-opus-4-7, claude-sonnet-4-6)
  • Thinking level: adaptive

Repro path

  1. Run an agent session with thinking enabled (adaptive / any non-off level)
  2. While the model is mid-stream emitting a thinking block (between content_block_start and content_block_stop), trigger one of:
    • Session takeover by another writer (touches EmbeddedAttemptSessionTakeoverError path)
    • Network drop / process kill
    • sessions_yield abort during reasoning
  3. Restart / continue the session
  4. Next LLM request fails with Invalid signature in thinking block

Root cause

The session writer appends messages to the JSONL in a streaming fashion (see src/.../transcript.tsfs.appendFile(transcriptPath, JSON.stringify(entry) + "\n", "utf-8")). When the stream is cut before content_block_stop delivers the opaque signature, the persisted thinking block has an empty/missing signature.

OpenClaw already has the right sanitizer:

// src/agents/pi-embedded-runner/thinking.ts
export function stripInvalidThinkingSignatures(messages: AgentMessage[]): AgentMessage[]

…and it does the right thing (drops thinking blocks where hasReplayableThinkingSignature(block) is false).

The bug is that this sanitizer is conditionally gated, not unconditionally applied to persisted history before replay:

// dist selection-Cr-9-UpD.js around line 8809
const validatedThinkingSignatures = policy.preserveSignatures
  ? stripInvalidThinkingSignatures(sanitizedImages)
  : sanitizedImages;

On some recovery paths (notably after EmbeddedAttemptSessionTakeoverError and certain sessions_yield interrupts), policy.preserveSignatures is not true, so the invalid thinking block survives sanitization and reaches the Anthropic API, which then 400s the whole request.

Suggested fix

Two complementary changes:

1. Defensive: never let an unsigned thinking block reach the wire

In prepareReplayMessages (or whichever function wraps sanitizedImagesvalidatedThinkingSignatures), call stripInvalidThinkingSignatures unconditionally for any provider that requires opaque thinking signatures (Anthropic, Bedrock):

// Always strip — providers are the authority on signature validity,
// and persisted-but-unsigned blocks are never replayable.
const validatedThinkingSignatures = providerRequiresSignedThinking(model)
  ? stripInvalidThinkingSignatures(sanitizedImages)
  : (policy.preserveSignatures
      ? stripInvalidThinkingSignatures(sanitizedImages)
      : sanitizedImages);

This makes the existing sanitizer the safety net it was designed to be.

2. Preventive: don't persist incomplete thinking blocks

In the stream consumer that turns Anthropic SSE events into the persisted assistant message:

  • Buffer the in-flight thinking block locally
  • Only flush it to appendMessage(...) when content_block_stop arrives with a non-empty signature
  • If the stream ends/aborts before content_block_stop for that block, either:
    • Drop the block entirely, or
    • Persist it as a text block with the omitted-reasoning placeholder (OMITTED_ASSISTANT_REASONING_TEXT)

This stops the bad data from being written in the first place, so a subsequent openclaw invocation that bypasses the sanitizer (e.g. CLI tooling that reads the JSONL directly) never sees a poisoned block.

Workaround (current, manual)

Strip thinking blocks from the affected session JSONL:

import json, sys
from pathlib import Path

path = Path(sys.argv[1])
out_lines = []
for line in path.read_text().splitlines():
    msg = json.loads(line)
    content = msg.get("content")
    if isinstance(content, list):
        msg["content"] = [b for b in content if b.get("type") != "thinking"]
    out_lines.append(json.dumps(msg, ensure_ascii=False))
path.write_text("\n".join(out_lines) + "\n")

This recovers the session but loses the reasoning history, and it's a manual step — the user has to know what error to look for and where to find the session file.

Impact

  • Severity: High when triggered — session is fully blocked, no auto-recovery
  • Frequency: Reproducible whenever a stream interrupt happens during a thinking block. With adaptive thinking and long-running agents (cron jobs, multi-step tool work, sessions_yield workflows), this is not rare.
  • Blast radius: The session JSONL is corrupted persistently until manually fixed; every subsequent request fails identically.

Related code references

  • dist/plugin-sdk/src/agents/pi-embedded-runner/thinking.d.ts — sanitizer surface
  • dist/selection-Cr-9-UpD.js:2695stripInvalidThinkingSignatures implementation
  • dist/selection-Cr-9-UpD.js:2825assessLastAssistantMessage (already classifies incomplete-thinking)
  • dist/selection-Cr-9-UpD.js:8809 — conditional gate that lets unsigned blocks through
  • dist/transcript-BEv_N5f2.js:347fs.appendFile(...) that writes the bad block

Reporter context

Hit this in production on 2026-05-20 ~11:44 GMT+8 during a routine assistant turn; session became unusable until the JSONL was manually scrubbed.

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING

openclaw - 💡(How to fix) Fix Incomplete thinking blocks (missing signature) persisted to session JSONL → next request fails with `Invalid signature in thinking block`