Skip to content

claude-cli thinking-only (end_turn, empty text) turns trigger empty_response model-fallback re-run on a different model #89008

@joshgaskin

Description

@joshgaskin

Version

OpenClaw 2026.5.28 (claude-cli provider, Node v22.22.1, Linux). Confirmed still present in 2026.5.31-beta.3.

Summary

When a claude-cli turn completes normally (stop_reason: "end_turn") but the final assistant message contains only a thinking block and no text (a valid NO_REPLY), the CLI backend throws FailoverError({ reason: "empty_response" }). Because empty_response is a fallback-triggering reason, the model-fallback chain then re-runs the turn on the configured fallback model (e.g. an OpenRouter model). The fallback model produces a real reply — but on a different model than configured, with different behavior/instruction-following, and a user-visible ↪️ Model Fallback: … banner.

This is the same root trigger as the (closed) #82394 and #83231, but a distinct downstream consequence that those fixes did not cover:

A completed end_turn with no text is a legitimate "nothing to say", not a transient model failure — it should not trigger the fallback chain.

Reproduction signature

Gateway logs around an affected turn:

[agent/cli-backend] claude live session turn: provider=claude-cli model=claude-sonnet-4-6 durationMs=28899 rawLines=43 outBytes=0 outHash=e3b0c44298fc...   # sha256 of ""
[model-fallback/decision] model fallback decision: decision=candidate_failed requested=claude-cli/claude-sonnet-4-6 candidate=claude-cli/claude-sonnet-4-6 reason=empty_response next=openrouter/<fallback>
[model-fallback/decision] model fallback decision: decision=candidate_succeeded requested=claude-cli/claude-sonnet-4-6 candidate=openrouter/<fallback>

The claude-cli transcript (~/.claude/projects/<cwd>/<session>.jsonl) for the affected turn shows the final assistant message as:

type=assistant  stop_reason=end_turn  content blocks=[thinking]  text length=0

Clusters on long/heavy reasoning turns (20–45s, dozens–hundreds of stream lines) and under concurrency. Not auth (reason=auth is separate), not rate-limit/overloaded (no such signals), not model-specific (observed on both claude-sonnet-4-6 and claude-opus-4-7).

Code pointers

  • cli-runner-*.js executeCliAttempt:
    const assistantText = output.text.trim();
    if (!assistantText) throw new FailoverError("CLI backend returned an empty response.", { reason: "empty_response", ... });
    const assistantTexts = assistantText ? [assistantText] : [];          // already handles empty
    const lastAssistant = assistantText.length > 0 ? buildCliHookAssistantMessage(...) : void 0;  // already handles empty
    The downstream is already written to tolerate empty text — only this throw forces the failover.
  • model-fallback-*.js still classifies empty_response as a fallback-eligible reason in 2026.5.31-beta.3 (alongside rate_limit/overloaded/timeout/unknown).

Suggested fix

When the underlying completion has a clean stop reason (end_turn/stop) and no text and no tool call, resolve the turn as a valid empty no-reply instead of throwing empty_response → fallback. Equivalently: exclude empty_response from the fallback-trigger path when stop_reason === "end_turn". Genuine CLI failures (process error, timeout, parse/format error) already throw via separate paths and should keep their failover.

Metadata

Metadata

Assignees

No one assigned

    Labels

    P2Normal backlog priority with limited blast radius.clawsweeper:fix-shape-clearClawSweeper found a clear likely implementation shape for this issue.clawsweeper:queueable-fixClawSweeper marked this issue as an existing queue_fix_pr work candidate.clawsweeper:source-reproClawSweeper found a high-confidence source-level issue reproduction.impact:auth-providerAuth, provider routing, model choice, or SecretRef resolution may break.issue-rating: 🦞 diamond lobsterVery strong issue quality with high-confidence source-level or clear reproduction.

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions