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-v1 → claude-sonnet-4-6 → google/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)
- Empty-response path records empty assistant block.
pi-embedded (around maxEmptyResponseRetryAttempts / emptyResponseRetryInstruction logic) stores the empty Gemini output in session history before failing over.
- 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.
- 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
- Configure any session with primary = Bedrock Anthropic, last fallback =
google/gemini-3-flash-preview.
- 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).
- 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.
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
amazon-bedrock/global.anthropic.claude-opus-4-7claude-opus-4-6-v1→claude-sonnet-4-6→google/gemini-3-flash-previewSymptom
User sees assistant replies like:
…repeatedly across multiple turns in the same session.
Log evidence
Session transcript snapshot
Assistant message stored with
content: [{"type":"text","text":""}]andstopReason: "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)
pi-embedded(aroundmaxEmptyResponseRetryAttempts/emptyResponseRetryInstructionlogic) stores the empty Gemini output in session history before failing over.ContentBlocks.Expected behavior
"[no content]"placeholder) empty assistant text blocks in conversation history before making a fallback provider call.Reproducer
google/gemini-3-flash-preview.ContentBlockblank 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.jsemptyResponseRetryAttemptsand downstream fallback dispatch), before calling the next candidate:And in the delivery path: if the final fallback returns a provider validation error (
4xxshape), 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
google/gemini-3-flash-previewfromagents.defaults.model.fallbacks(now Opus 4.7 → Opus 4.6 → Sonnet 4.6 only).Cheers.