fix(assembler): drop empty content for user/toolResult roles, not only assistant#606
Merged
jalehman merged 1 commit intoMay 11, 2026
Conversation
…y assistant The assemble pass's `cleanedEntries` filter only guarded `assistant` messages against empty/blank content. An empty `user` or `toolResult` content array produced upstream during incremental compaction could survive the cleaned-tail filter and reach Bedrock Converse, which rejects it with the literal wording: The content field in the Message object at messages.N is empty. Add a ContentBlock object to the content field and try again. A direct probe against Bedrock confirms this exact wording is reproducible only when `content === []`; bare strings or non-empty arrays produce a different (`SerializationException`) error. The asymmetry between the assistant guard and the missing user/toolResult guard explains why the 0.9.3 prefill safety net (Martian-Engineering#572) and the 0.9.4 blank-text guard did not fully close the validation surface. Extract a unified `isEmptyMessageContent` helper that drops `undefined`, `null`, empty-string, whitespace-only string, and empty-array content for any role, while preserving the existing assistant-only thinking-only and blank-text guards. Dropping a `toolResult` here is safe — `sanitizeToolUseResultPairing` runs immediately below and re-pairs missing results with a synthetic `[lossless-claw] missing tool result …` placeholder. Add `test/assembler-empty-content.test.ts` covering 28 cases: universal empty handling across all three roles, non-empty preservation, the assistant-only thinking/blank-text guards, and a direct regression for the production failure shape. Co-authored-by: Cursor <cursoragent@cursor.com>
Contributor
|
Thank you! |
This was referenced May 11, 2026
Merged
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.
Summary
Fix Bedrock
messages.N is emptyvalidation rejection by extending the assemble pass's empty-content filter to coveruserandtoolResultroles, not onlyassistant.The pre-existing
cleanedEntriesfilter atsrc/assembler.tsonly guarded theassistantrole. An empty content array produced upstream for auserortoolResultmessage during incremental compaction could survive the cleaned-tail pass and reach Bedrock Converse, which rejects it with the literal wording:A direct probe against
eu.anthropic.claude-haiku-4-5-20251001-v1:0viaConverseCommandconfirms this exact wording is reproducible only whencontent === [](empty array); bare strings produce a differentSerializationExceptionerror, so the production wording is specific to the empty-array shape.The asymmetry between the assistant guard and the missing user/toolResult guard explains why the v0.9.3 prefill safety net (#572) and the v0.9.4 blank-text guard did not fully close the validation surface.
Production reproduction signal
Observed in a long-running OpenClaw session against
eu.anthropic.claude-opus-4-6-v1viabedrock-converse-stream. The failed turn was recorded as:{ "type": "message", "timestamp": "2026-05-01T07:50:52.195Z", "message": { "role": "assistant", "content": [{"type":"text","text":"[assistant turn failed before producing content]"}], "api": "bedrock-converse-stream", "provider": "amazon-bedrock", "model": "eu.anthropic.claude-opus-4-6-v1", "stopReason": "error", "errorMessage": "Validation error: The content field in the Message object at messages.0 is empty. Add a ContentBlock object to the content field and try again." } }Timeline (the LCM
conversationsrow for this session at the time of failure):sum_14ac9f3745cdfa00createdsum_91b1ebdf4c1bb243committedmessages.0 is empty(≤1 s after the summary)The ≤1 s delta between summary commit and failure points to a transient mid-compaction state rather than a persisted shape — replaying the same conversation's stored
lcm.dbstate today through the assemble pipeline produces zero empty-array messages, so the empty array is produced and consumed within the compaction window.What changed
New helper
isEmptyMessageContent(message)insrc/assembler.ts(exported with@internalJSDoc, matching the existingtoolCallBlockFromPart/toolResultBlockFromPart/blockFromPart/contentFromPartsconvention). It returnstruewhen content is:undefinedornull""or whitespace-only string[]for any roleassistantrole specifically (preserves the existing v0.9.3 / v0.9.4 guards)Replace the
cleanedEntriesfilter to use the new helper. The filter now drops empty content for any role.Extended comment on the filter explains the new compaction-window failure mode and notes that dropping a
toolResultis safe becausesanitizeToolUseResultPairingruns immediately afterwards and re-pairs missing results with the existing synthetic[lossless-claw] missing tool result …placeholder.Regression test
test/assembler-empty-content.test.ts(28 cases):user/assistant/toolResultuserandtoolResult)Changeset (
patchbump perAGENTS.mdguidance for fixes / compatibility work).Risk and compatibility
assistantrole (thinking-only and blank-text-only guards still fire).sanitizeToolUseResultPairingalready handles missing tool results via synthetic placeholders, so a dropped emptytoolResultis recoverable.Test plan
npm test— 879 / 886 tests pass on Windows. The 7 failures are pre-existing Windows-specific path/HOME issues (backslashes vs forward slashes,HOMEenv handling) unrelated to this change. Will run green on CI'subuntu-latest.npm test -- test/assembler-empty-content.test.ts test/assembler-blocks.test.ts test/transcript-repair.test.ts— 75 / 75 green.npm run build— bundle builds cleanly (688.4 KB).Related
text field … is blankwording for assistant contentisThinkingOnlyContent/isBlankContenthelpers (lines 97–124) — the newisEmptyMessageContenthelper builds on them and keeps their assistant-specific scope intactHappy to iterate on naming, helper placement, or the changeset wording — and to extend the filter to additional empty shapes if maintainers know of other transient post-compaction states that produce them.
Made with Cursor