Skip to content

fix: strip thinking-only assistant messages to prevent Bedrock empty content rejection#498

Closed
wujiaming88 wants to merge 1 commit into
Martian-Engineering:mainfrom
wujiaming88:fix/strip-thinking-only-assistant-messages
Closed

fix: strip thinking-only assistant messages to prevent Bedrock empty content rejection#498
wujiaming88 wants to merge 1 commit into
Martian-Engineering:mainfrom
wujiaming88:fix/strip-thinking-only-assistant-messages

Conversation

@wujiaming88

Copy link
Copy Markdown
Contributor

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 with text: ""). The Bedrock provider's convertMessages function 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.

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

  1. Use an extended-thinking model (e.g. Claude Opus with thinking enabled)
  2. Have lossless-claw enabled
  3. Over a conversation, some assistant turns will have only thinking blocks
  4. When context is reassembled, these messages survive the empty-content filter
  5. OpenClaw core's dropThinkingBlocks() strips the thinking blocks
  6. Bedrock rejects the empty content → ValidationException

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.

Changes

  • src/assembler.ts: Add isThinkingOnlyContent() helper and extend the cleanedEntries filter
  • test/assembler-blocks.test.ts: Add 9 unit tests for isThinkingOnlyContent()

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).

…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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant