openclaw - ✅(Solved) Fix Anthropic 400: empty text ContentBlock not filtered on user/toolResult/assistant-error/images-only/preserveSignatures paths [1 pull requests, 2 comments, 3 participants]

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

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…
GitHub stats
openclaw/openclaw#73640Fetched 2026-04-29 06:17:05
View on GitHub
Comments
2
Participants
3
Timeline
5
Reactions
0
Author
Timeline (top)
commented ×2closed ×1cross-referenced ×1referenced ×1

Anthropic API rejects requests with Validation error: The text field in the ContentBlock object at messages.N.content.0 is blank. Add text to the text field, and try again. when the session transcript contains an empty-text content block that was not caught by sanitization.

Error Message

Validation error: The text field in the ContentBlock object at messages.64.content.0 is blank. Add text to the text field, and try again.

Root Cause

sanitizeSessionMessagesImages in src/agents/pi-embedded-helpers/images.ts is the only place that filters empty-text blocks out of the transcript, but it only does so on a narrow path: role === "assistant", stopReason !== "error", sanitizeMode === "full", and (when preserveSignatures is on) no thinking block present.

Empty { type: "text", text: "" } blocks are not filtered for:

  1. role === "user" messages (src/agents/pi-embedded-helpers/images.ts:94-106) — only sanitizeContentBlocksImages runs, which does not touch empty text blocks.
  2. role === "toolResult" messages (src/agents/pi-embedded-helpers/images.ts:82-92) — same, image-only sanitization.
  3. role === "assistant" with stopReason === "error" (src/agents/pi-embedded-helpers/images.ts:110-122) — bypasses the empty-text filter entirely.
  4. role === "assistant" under sanitizeMode === "images-only" (src/agents/pi-embedded-helpers/images.ts:129-137) — bypasses the filter.
  5. role === "assistant" with preserveSignatures and any thinking block present (src/agents/pi-embedded-helpers/images.ts:140-142) — keeps original content including empty text blocks.

isEmptyAssistantMessageContent in the same file already treats text.trim() === "" as empty, so there is precedent that this is a known shape to drop.

Any one of the five paths above can produce a transcript that Anthropic rejects with the validation error above.

Fix Action

Fixed

PR fix notes

PR #73658: fix(agents): drop blank-text ContentBlocks on every Anthropic transcript path (#73640)

Description (problem / solution / changelog)

What

Fixes #73640. Anthropic's API rejects requests whose messages.N.content.K carries a { type: "text", text: "" } block:

Validation error: The text field in the ContentBlock object at messages.64.content.0 is blank. Add text to the text field, and try again.

Once that 400 fires for a session, every subsequent turn replays the same transcript and hits the same error — the session is wedged with no per-turn recovery path.

Root cause

sanitizeSessionMessagesImages in src/agents/pi-embedded-helpers/images.ts was the only place filtering empty-text blocks before the provider call, but it only did so on a single narrow path: role === "assistant" AND stopReason !== "error" AND sanitizeMode === "full" AND (when preserveSignatures is on) no thinking/redacted_thinking block present.

Empty { type: "text", text: "" } blocks survived for five other branches:

  1. role === "user" content arrays — only image sanitization ran, never touched text blocks
  2. role === "toolResult" content arrays — same
  3. role === "assistant" with stopReason === "error" — bypassed the filter entirely
  4. role === "assistant" under sanitizeMode === "images-only" — bypassed the filter
  5. role === "assistant" with preserveSignatures AND thinking block — kept original content including blank text

Any one of those five paths could produce a transcript Anthropic rejects.

Fix

Extract the inlined empty-text filter into a small dropEmptyTextBlocks(content) helper at the top of the file, and apply it uniformly to all five sites:

function dropEmptyTextBlocks<T>(content: readonly T[]): T[] {
  return content.filter((block) => {
    if (!block || typeof block !== "object") return true;
    const rec = block as { type?: unknown; text?: unknown };
    if (rec.type !== "text") return true;
    return typeof rec.text === "string" && rec.text.trim().length > 0;
  });
}

The helper only rewrites text blocks; thinking, redacted_thinking, tool_use, tool_result, and image blocks pass through unchanged. The previous isThinkingOrRedactedBlock carve-out (which kept blank-text companions when a thinking block was present under preserveSignatures) is removed because dropping empties never disturbs thinking-block ordering — and it still triggered the 400.

Test changes

  • Updated the existing preserves interleaved thinking block order when signatures are preserved regression test (the canary for the preserveSignatures branch). Title now ends … and drops blank-text companions. The test still pins thinking / redacted_thinking ordering; the blank-text companion is now expected to be dropped.
  • New test drops blank-text content blocks on user, toolResult, and error-stopped assistant turns (#73640) pinning the four previously-unfiltered branches in one go.

Verified locally

npx oxlint src/agents/pi-embedded-helpers/images.ts
# Found 0 warnings and 0 errors.

npx vitest run \
  src/agents/pi-embedded-helpers.sanitize-session-messages-images.removes-empty-assistant-text-blocks-but-preserves.test.ts \
  src/agents/pi-embedded-runner.sanitize-session-history.policy.test.ts \
  src/agents/pi-embedded-runner.sanitize-session-history.test.ts
# Tests  116 passed (116)

Pre-implement audit

  1. Existing-helper check. The empty-text filter shape was already inlined in the working assistant-full-mode branch (images.ts:143-152); the new helper is the natural extraction. isEmptyAssistantMessageContent (same file, line 35) uses an identical predicate and is left in place because it serves a different caller (full-message classifier vs per-block filter). ✅
  2. Shared-helper caller check. dropEmptyTextBlocks is module-private. The 5 call sites are all inside sanitizeSessionMessagesImages. No external contract change. ✅
  3. Broader-fix rival scan. Zero rival PRs on #73640. ✅

lobster-biscuit: 73640-anthropic-empty-text-content-block

Sign-Off:

  • I have read and agree to the OpenClaw Contributor License Agreement.

Changed files

  • CHANGELOG.md (modified, +1/-0)
  • src/agents/pi-embedded-helpers.sanitize-session-messages-images.removes-empty-assistant-text-blocks-but-preserves.test.ts (modified, +130/-4)
  • src/agents/pi-embedded-helpers/images.ts (modified, +69/-24)

Code Example

Validation error: The text field in the ContentBlock object at messages.64.content.0 is blank. Add text to the text field, and try again.
RAW_BUFFERClick to expand / collapse

Summary

Anthropic API rejects requests with Validation error: The text field in the ContentBlock object at messages.N.content.0 is blank. Add text to the text field, and try again. when the session transcript contains an empty-text content block that was not caught by sanitization.

Observed

Validation error: The text field in the ContentBlock object at messages.64.content.0 is blank. Add text to the text field, and try again.

Once this happens the session is stuck — every subsequent turn replays the same transcript and hits the same 400.

Root cause

sanitizeSessionMessagesImages in src/agents/pi-embedded-helpers/images.ts is the only place that filters empty-text blocks out of the transcript, but it only does so on a narrow path: role === "assistant", stopReason !== "error", sanitizeMode === "full", and (when preserveSignatures is on) no thinking block present.

Empty { type: "text", text: "" } blocks are not filtered for:

  1. role === "user" messages (src/agents/pi-embedded-helpers/images.ts:94-106) — only sanitizeContentBlocksImages runs, which does not touch empty text blocks.
  2. role === "toolResult" messages (src/agents/pi-embedded-helpers/images.ts:82-92) — same, image-only sanitization.
  3. role === "assistant" with stopReason === "error" (src/agents/pi-embedded-helpers/images.ts:110-122) — bypasses the empty-text filter entirely.
  4. role === "assistant" under sanitizeMode === "images-only" (src/agents/pi-embedded-helpers/images.ts:129-137) — bypasses the filter.
  5. role === "assistant" with preserveSignatures and any thinking block present (src/agents/pi-embedded-helpers/images.ts:140-142) — keeps original content including empty text blocks.

isEmptyAssistantMessageContent in the same file already treats text.trim() === "" as empty, so there is precedent that this is a known shape to drop.

Any one of the five paths above can produce a transcript that Anthropic rejects with the validation error above.

Suggested fix

Extract the empty-text-block filter (currently inlined at images.ts:143-152) into a small helper like dropEmptyTextBlocks(content) and apply it uniformly to:

  • user content arrays
  • toolResult content arrays
  • assistant content arrays on all branches (error / images-only / preserveSignatures-with-thinking)

Preserve existing behavior where a resulting empty assistant message is dropped. For user / toolResult, if filtering leaves an empty array, substitute a single whitespace placeholder rather than emitting content: [], since Anthropic also rejects empty content arrays.

Happy to send a PR with the refactor + tests covering each of the five bypass paths.

Environment

  • Repo HEAD: a99490fba4af1dc52bde0a8c2c2916e1757d9662
  • Provider: Anthropic (direct)

extent analysis

TL;DR

Extract the empty-text-block filter into a helper function and apply it uniformly to all content arrays to prevent Anthropic API validation errors.

Guidance

  • Identify the five paths in src/agents/pi-embedded-helpers/images.ts where empty-text blocks are not filtered: role === "user", role === "toolResult", role === "assistant" with stopReason === "error", role === "assistant" under sanitizeMode === "images-only", and role === "assistant" with preserveSignatures and any thinking block present.
  • Create a dropEmptyTextBlocks helper function to filter out empty-text blocks from content arrays.
  • Apply the dropEmptyTextBlocks function to all content arrays, including user, toolResult, and assistant content arrays, to ensure uniform filtering.
  • Handle the case where filtering leaves an empty array for user and toolResult content by substituting a single whitespace placeholder.

Example

function dropEmptyTextBlocks(content) {
  return content.filter(block => block.type !== 'text' || block.text.trim() !== '');
}

Notes

The suggested fix requires refactoring the existing code to extract the empty-text-block filter into a reusable helper function and applying it uniformly to all content arrays. This should prevent Anthropic API validation errors due to empty-text blocks.

Recommendation

Apply the workaround by extracting the empty-text-block filter into a helper function and applying it uniformly to all content arrays, as this will prevent Anthropic API validation errors and ensure consistent filtering of empty-text blocks.

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