fix: strip thinking-only assistant messages to prevent Bedrock empty content rejection#498
Closed
wujiaming88 wants to merge 1 commit into
Closed
Conversation
…content rejection When lossless-claw reassembles messages from the database, assistant messages that contain only thinking/reasoning/redacted_thinking blocks (with no visible text or tool calls) pass through the existing empty- content filter because their content array is non-empty. These messages then reach OpenClaw core, where dropThinkingBlocks() strips all thinking blocks, leaving an empty content array (or a placeholder with text: ""). The Bedrock provider's convertMessages filters out text blocks where text.trim().length === 0, producing a completely empty content array that Bedrock rejects with: ValidationException: The content field in the Message object at messages.N is empty. Add a ContentBlock object to the content field and try again. This is particularly common with extended-thinking models (Claude with thinking enabled) where some assistant turns consist entirely of thinking blocks — for instance when the model reasons internally before delegating to tool calls that were later orphan-stripped. Fix: extend the cleanedEntries filter in the assembler to also detect and drop assistant messages whose content consists entirely of thinking/reasoning/redacted_thinking blocks. These messages carry no semantic value after thinking-block removal, so dropping them is safe and prevents the downstream empty-content error. Includes the isThinkingOnlyContent() helper and 9 unit tests.
jalehman
added a commit
that referenced
this pull request
Apr 28, 2026
…king-only messages (#506) * fix: prevent Bedrock empty-content rejection from dedup gaps and thinking-only messages Two fixes for the Bedrock 'content field is empty' error: 1. **deduplicateAfterTurnBatch tail/suffix matching** (engine.ts) When storedCount > batchLen (e.g. after compaction), the old code returned the full batch unchanged, causing duplicate ingestion. The new code attempts tail-matching (entire batch already stored) and suffix-matching (partial overlap) before falling back to full ingestion. Also adds suffix fallback for the existing prefix-mismatch path. 2. **Strip thinking-only assistant messages** (assembler.ts) Assistant messages containing only thinking/redacted_thinking/reasoning blocks pass the existing empty-content filter (content.length > 0) but become empty after the provider layer strips thinking blocks. Add isThinkingOnlyContent() to detect and filter these before they reach Bedrock. Closes: relates to PR #498 (previously closed) Tested: production hotpatch running since 2026-04-26 on openclaw@2026.4.24 with Bedrock Claude Opus 4 — zero recurrence of the error. * fix: harden dedup tail replay handling --------- Co-authored-by: 五岳团队 <wujiaming88@users.noreply.github.com> Co-authored-by: Josh Lehman <josh@martian.engineering>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
When lossless-claw reassembles messages from the database, assistant messages that contain only thinking/reasoning/redacted_thinking blocks (with no visible text or tool calls) pass through the existing empty-content filter in the assembler because their content array is non-empty (length > 0).
These messages then reach OpenClaw core, where
dropThinkingBlocks()strips all thinking blocks, leaving an empty content array (or a single text block withtext: ""). The Bedrock provider'sconvertMessagesfunction filters out text blocks wheretext.trim().length === 0, producing a completely empty content array that Bedrock rejects with:Why this is worse with lossless-claw
Without lossless-claw, the default context engine works with live conversation messages where thinking blocks are typically paired with visible text or tool calls. Lossless-claw faithfully reconstructs all stored message parts from the database (including raw thinking blocks), and the fresh tail includes these thinking-only messages unchanged — creating more opportunities for this edge case to surface.
This is particularly common with extended-thinking models (Claude with thinking enabled) where some assistant turns consist entirely of thinking blocks — for instance when the model reasons internally before delegating to tool calls that were later orphan-stripped by
filterNonFreshAssistantToolCalls.Reproduction path
dropThinkingBlocks()strips the thinking blocksValidationExceptionFix
Extend the
cleanedEntriesfilter in the assembler to also detect and drop assistant messages whose content consists entirely of thinking/reasoning/redacted_thinking blocks. These messages carry no semantic value after thinking-block removal, so dropping them is safe and prevents the downstream empty-content error.Changes
src/assembler.ts: AddisThinkingOnlyContent()helper and extend thecleanedEntriesfiltertest/assembler-blocks.test.ts: Add 9 unit tests forisThinkingOnlyContent()Complementary fix
This pairs with openclaw/openclaw#71623 which addresses the same issue from the OpenClaw core side by using a non-empty placeholder in
dropThinkingBlocks(). Both fixes are independent and provide defense-in-depth — the lossless-claw fix prevents the problematic messages from reaching the core pipeline at all.Test results
All 784 tests pass (775 existing + 9 new).