fix(agents): strip thinking blocks with invalid signatures before API replay#70054
fix(agents): strip thinking blocks with invalid signatures before API replay#70054castaples wants to merge 3 commits into
Conversation
… replay When Bedrock returns thinking blocks with empty thinkingSignature, they get persisted to the session JSONL transcript. On subsequent turns, the Anthropic API rejects these with 'Invalid signature in thinking block', causing the session to become unrecoverable without /reset. Add stripInvalidThinkingSignatures() that filters out thinking blocks with missing, empty, or non-string signatures during transcript sanitization, before messages are sent to the API. Wire it into sanitizeSessionHistory in replay-history.ts so it runs whenever preserveSignatures is true (Anthropic/Bedrock providers). Also adds isInvalidThinkingSignatureError() so callers can detect the specific Anthropic rejection and apply dropThinkingBlocks + retry as a follow-up safety net (left for a future PR — replay-side prevention should fix openclaw#45010 on its own for the empty-signature case). This is a rebase of openclaw#45072 onto current main, with two changes: - The original PR routed retry logic through run.ts/attempt.ts/types.ts. That plumbing has diverged significantly. Splitting prevention from retry keeps this PR scoped to the replay-history change so it can land independently; retry can ride on top of the new wrapAnthropicStreamWithRecovery wrapper (introduced after openclaw#45072 was opened) in a follow-up. - google.ts has been renamed to replay-history.ts since openclaw#45072 was opened; the wire-in target moved with it. Fixes openclaw#45010 Co-authored-by: Fourier <[email protected]> Made-with: Cursor
Greptile SummaryThis PR adds Confidence Score: 5/5Safe to merge — targeted, well-tested fix with correct ordering and reference-equality optimisation. All findings are P2 (style/edge-case notes). The core logic is correct: empty/missing signatures are stripped, valid signatures and No files require special attention. Prompt To Fix All With AIThis is a comment left during a code review.
Path: src/agents/pi-embedded-runner/thinking.ts
Line: 89
Comment:
**`redacted_thinking` blocks with invalid fields not covered**
`stripInvalidThinkingSignatures` only processes `type === "thinking"` blocks and passes `redacted_thinking` blocks through unconditionally. `redacted_thinking` blocks carry a `signature` field (not `thinkingSignature`) and `isSignedThinkingBlock` treats them as always-signed (line 43: `record.type === "redacted_thinking"` is sufficient). If Bedrock ever returns a `redacted_thinking` block with an empty or missing `signature`, this function would silently forward the corrupt block to the API. This is a narrow edge-case today but worth noting if the validation scope expands.
How can I resolve this? If you propose a fix, please make it concise.Reviews (1): Last reviewed commit: "fix(agents): strip thinking blocks with ..." | Re-trigger Greptile |
…gnatures Address Greptile review feedback on openclaw#70054. The original implementation only checked `type === "thinking"` blocks and would silently forward any `type === "redacted_thinking"` block with an empty/missing `signature` field to the API. Bedrock has not been observed corrupting `redacted_thinking.signature` in practice (the original openclaw#45010 report is specifically about `thinkingSignature` on `thinking` blocks), but the cost of covering both shapes is trivial and matches `isSignedThinkingBlock`'s view that both block types carry a signature worth validating. Adds three tests for the redacted_thinking shape: empty signature, missing signature, and valid signature (preserved). Made-with: Cursor
|
Addressed in 540a031 — |
…alidThinkingSignatures Local `codex review --base upstream/main` caught a P1 regression: the previous implementation only checked `thinkingSignature` and `signature`, but `isSignedThinkingBlock` (and the rest of the runner) also recognises the snake_case `thought_signature` field that `sanitizeSessionMessagesImages` preserves when `preserveSignatures: true`. Anthropic / Bedrock replay histories that carry valid signatures only in `thought_signature` would have been classified as unsigned and stripped, silently erasing the latest signed reasoning turn and breaking immutable thinking replay on the next request — exactly the data-loss class of bug this helper exists to prevent. Fix: - Extract a `hasNonEmptyStringSignature` helper that checks all three recognised signature field names with union semantics. A block is signed if ANY of `signature` / `thinkingSignature` / `thought_signature` is a non-empty string. A block is stripped only when ALL of them are missing, empty, or non-string. - Use the existing `isThinkingBlock` predicate so the type-gating logic stays in one place and stays in sync if the set of thinking block types ever expands. Tests cover: - thinking blocks signed only via `thought_signature` (preserved) - redacted_thinking blocks signed only via `thought_signature` (preserved) - thinking blocks where all signature fields are empty (stripped) - thinking blocks where one signature field is empty but another is valid (preserved — belt-and-braces) Made-with: Cursor
|
Local Per CONTRIBUTING.md ("If you have access to Codex, run
Codex was right. Fix:
Local validation re-run on the new code: Updating my "Codex review" disclosure box in the PR body accordingly. |
|
Related work from PRtags group Title: Open PR candidate: invalid thinking signature replay fix rebased and scope-reduced
|
Fixes #45010. Supersedes #70054. Co-authored-by: Chris Staples <[email protected]> Co-authored-by: Fourier <[email protected]>
|
Codex maintainer follow-up: thanks @castaples, this was the right bug class and the PR gave us the useful shape. I landed the fix directly on What changed from this PR before landing:
Live verification before landing:
Closing this PR as superseded by the landed commit. Thank you for the report, rebase, and test direction here. |
Fixes openclaw#45010. Supersedes openclaw#70054. Co-authored-by: Chris Staples <[email protected]> Co-authored-by: Fourier <[email protected]>
Fixes openclaw#45010. Supersedes openclaw#70054. Co-authored-by: Chris Staples <[email protected]> Co-authored-by: Fourier <[email protected]>
Fixes openclaw#45010. Supersedes openclaw#70054. Co-authored-by: Chris Staples <[email protected]> Co-authored-by: Fourier <[email protected]>
Fixes openclaw#45010. Supersedes openclaw#70054. Co-authored-by: Chris Staples <[email protected]> Co-authored-by: Fourier <[email protected]>
Fixes openclaw#45010. Supersedes openclaw#70054. Co-authored-by: Chris Staples <[email protected]> Co-authored-by: Fourier <[email protected]>
Fixes openclaw#45010. Supersedes openclaw#70054. Co-authored-by: Chris Staples <[email protected]> Co-authored-by: Fourier <[email protected]>
Fixes openclaw#45010. Supersedes openclaw#70054. Co-authored-by: Chris Staples <[email protected]> Co-authored-by: Fourier <[email protected]>
Rebase of #45072 onto current
main, scoped down to the prevention piece so it can land independently. Original work by @4ier (credited viaCo-authored-bytrailer).Summary
stripInvalidThinkingSignatures()tosrc/agents/pi-embedded-runner/thinking.ts. Strips assistantthinkingblocks whosethinkingSignatureis missing, empty, or non-string — the cases where Bedrock streaming has been observed to persist a corrupt block to the session JSONL transcript.sanitizeSessionHistory()insrc/agents/pi-embedded-runner/replay-history.ts(this is where fix(agents): strip thinking blocks with invalid signatures before API replay #45072'sgoogle.tschange moved to in the file restructure that happened after that PR was opened). Runs wheneverpolicy.preserveSignaturesis true (Anthropic / Bedrock path), before the existingdropThinkingBlocksstep.isInvalidThinkingSignatureError()so a future PR can wrap the runner stream with a retry-on-API-rejection safety net using the existingwrapAnthropicStreamWithRecoverypattern.thinking.test.tscover the empty / missing / mixed-validity / non-assistant cases plus the error-matcher.Why this fixes #45010
The reproducible failure on Bedrock + Claude Opus 4.6 + extended thinking is:
thinkingSignature: \"\".Invalid signature in thinking block./reset.Stripping at replay-sanitisation time prevents step 3 from ever sending the bad block to the API. Both newly-corrupted sessions and existing damaged ones recover automatically on the next turn.
What this PR deliberately does NOT include from #45072
The original PR added a defense-in-depth retry by threading a new
forceDropThinkingBlocksflag throughrun.ts,run/attempt.ts, andrun/types.ts. Those files have changed substantially since #45072 was opened, andwrapAnthropicStreamWithRecovery(introduced after #45072) provides the same retry shape with less plumbing. Splitting prevention from retry keeps this PR small and avoids re-doing diverged plumbing — retry can ride on top of the wrapper in a follow-up that also coversTHINKING_BLOCK_ERROR_PATTERN-style errors.Bot feedback from #45072
Both review comments (greptile + codex) on the original PR were about
MIN_VALID_SIGNATURE_LENGTH = 10being too permissive. @4ier addressed them in c999982 by removing the length heuristic entirely and treating the API as the authoritative validator. Those resolutions are incorporated here — the implementation only checks for empty / missing / non-string signatures, never length thresholds, and the test for a "valid" signature uses\"b\".repeat(356)to match the documented real-signature lower bound.Local verification
pnpm exec tsgo --noEmit -p tsconfig.json— cleanpnpm exec oxlint <touched files>— 0 warnings, 0 errorspnpm exec oxfmt --check <touched files>— formattedtest/setup-openclaw-runtime.ts:372failed withVitest failed to find the runnerfor all 381 tests in the agents project — looks like a local/Windows env issue unrelated to this change. Relying on CI (which was green for these files in fix(agents): strip thinking blocks with invalid signatures before API replay #45072) to confirm.Test plan
Closes #45010
Supersedes #45072
Made with Cursor
AI-assisted PR disclosure
Per CONTRIBUTING.md:
Co-authored-bytrailer); this PR is a manual rebase of that work onto currentmainwith the scope reduction described above. All edits, the rebase strategy, and the bot-feedback responses were reviewed and approved by the human submitter.pnpm exec tsgo --noEmit -p tsconfig.json,pnpm exec oxlint, andpnpm exec oxfmt --checkare clean for the touched files. Local Vitest run was attempted buttest/setup-openclaw-runtime.ts:372failed withVitest failed to find the runnerfor all 381 agents-project tests on Windows — appears to be a local env issue unrelated to this change. Relying on CI (currently 92 SUCCESS / 8 SKIPPED / 0 FAILURE) for full test validation. Production workaround in our downstream deployment confirms the empty-signature symptom and that boundary-side stripping mitigates it; the upstream fix here is the canonical version.codex review --base upstream/mainlocally (codex-cli 0.122.0) and it caught a P1 regression in my own implementation —stripInvalidThinkingSignatureswas missing the snake_casethought_signaturefield thatsanitizeSessionMessagesImagespreserves, which would have silently erased valid Claude-signed reasoning turns. Fixed in e073423 with new tests; re-running codex on the fix confirms no remaining actionable findings. Greptile also reviewed (5/5 confidence, all findings addressed).stripInvalidThinkingSignatureswalks assistant messages in the replay history and removesthinking/redacted_thinkingblocks whose signature field is missing, empty, or non-string. It runs insanitizeSessionHistoryonly whenpolicy.preserveSignaturesis true (Anthropic / Bedrock providers), and runs beforedropThinkingBlocksso the latest-assistant-message preservation indropThinkingBlocksdoesn't reintroduce corrupt blocks. Returns the original array reference when nothing changed (caller can use reference equality to skip work). The retry helperisInvalidThinkingSignatureErroris additive and not yet wired — left for a follow-up that useswrapAnthropicStreamWithRecoveryto retry on API rejection.