Skip to content

Empty-response fallback leaks provider validation errors to users (self-propagating loop) #73020

Description

@marcushale

Bug: Empty-response fallback path leaks provider validation errors to users

Summary

When a primary model returns an empty response (0 output tokens) and the runtime falls over to a fallback provider that validates content blocks strictly (e.g., Anthropic on Bedrock), the fallback call fails with a validation error. That error string is then surfaced as the assistant's reply to the user and stored in the session transcript as the assistant turn, creating a self-propagating loop where every subsequent turn reproduces the same error.

Environment

  • OpenClaw 2026.4.25 (aa36ee6)
  • macOS 15.x, node v25.5.0
  • Channel: Discord (DM)
  • Primary model on session: amazon-bedrock/global.anthropic.claude-opus-4-7
  • Fallback chain: claude-opus-4-6-v1claude-sonnet-4-6google/gemini-3-flash-preview

Symptom

User sees assistant replies like:

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

…repeatedly across multiple turns in the same session.

Log evidence

15:21:57 [agent/embedded] empty response detected: runId=9022ecc8 sessionId=e249bbc1 
         provider=google/gemini-3-flash-preview — retrying 1/1 with visible-answer continuation
15:22:26 [agent/embedded] empty response retries exhausted: attempts=1/1 
         — surfacing incomplete-turn error
15:22:26 [model-fallback/decision] decision=candidate_failed 
         requested=google/gemini-3-flash-preview candidate=google/gemini-3-flash-preview 
         reason=format next=amazon-bedrock/global.anthropic.claude-opus-4-6-v1
15:22:54 [agent/embedded] embedded run agent end: isError=true 
         model=global.anthropic.claude-opus-4-6-v1 provider=amazon-bedrock 
         error=Validation error: The text field in the ContentBlock object at messages.3.content.0 is blank.

Session transcript snapshot

Assistant message stored with content: [{"type":"text","text":""}] and stopReason: "stop" from the Gemini run. Subsequent Bedrock fallback receives that empty block in history and rejects.

{"role":"assistant","content":[{"type":"text","text":""}],"provider":"google","model":"gemini-3-flash-preview","usage":{"input":39627,"output":0,"totalTokens":39627},"stopReason":"stop"}

Root cause (two compounding bugs)

  1. Empty-response path records empty assistant block. pi-embedded (around maxEmptyResponseRetryAttempts / emptyResponseRetryInstruction logic) stores the empty Gemini output in session history before failing over.
  2. No sanitization before fallback call. When the runtime falls over to a different provider, the empty assistant block is included in the conversation history sent to the fallback. Bedrock Converse strictly rejects empty text ContentBlocks.
  3. Provider validation errors are surfaced as message bodies. The rejection error from Bedrock gets delivered to the Discord channel as the assistant reply, and is stored as the next assistant turn — so every subsequent turn re-reproduces the same failure.

Expected behavior

  • Strip (or replace with a single-space/"[no content]" placeholder) empty assistant text blocks in conversation history before making a fallback provider call.
  • Never surface raw provider validation errors as user-facing message bodies. Either retry with sanitized history, escalate to an internal error (logged, not delivered), or deliver a generic apology string.
  • Do not persist a failed fallback's error message as the assistant turn in the session transcript — the original empty assistant turn should be replaced/invalidated once a successful model responds, or dropped entirely if all fallbacks fail.

Reproducer

  1. Configure any session with primary = Bedrock Anthropic, last fallback = google/gemini-3-flash-preview.
  2. Induce Gemini Flash to return 0 output tokens (e.g., supply an input that triggers its refusal-without-text mode, or use an input that includes only system content and no user text).
  3. Observe: retry-with-continuation also returns empty → runtime falls over to Bedrock → Bedrock returns the ContentBlock blank error → error appears as assistant reply in the channel and in the session transcript.

Suggested fix

In the model-fallback orchestration (around pi-embedded-xwfWu_QR.js emptyResponseRetryAttempts and downstream fallback dispatch), before calling the next candidate:

// Sanitize history: drop empty assistant text blocks so Bedrock validators don't reject.
function sanitizeHistoryForFallback(messages) {
  return messages.map(m => {
    if (m.role !== "assistant" || !Array.isArray(m.content)) return m;
    const cleaned = m.content.filter(b => b.type !== "text" || (b.text && b.text.trim()));
    return cleaned.length > 0 ? { ...m, content: cleaned } : null;
  }).filter(Boolean);
}

And in the delivery path: if the final fallback returns a provider validation error (4xx shape), replace the user-facing body with a generic "[internal error — retry]" message and log the raw error to the gateway error log only.

Workaround applied locally

  • Removed google/gemini-3-flash-preview from agents.defaults.model.fallbacks (now Opus 4.7 → Opus 4.6 → Sonnet 4.6 only).
  • Deleted corrupted DM session transcript to stop the self-propagating error loop.

Cheers.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    Priority

    None yet

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions