Skip to content

claude-cli tool-call-then-empty-text turns trigger empty_response fallback → duplicate user-visible reply in Telegram groups (v2026.6.1, follow-up to #89008) #91302

Description

@benjamin1492

Version

OpenClaw 2026.6.1 (claude-cli provider, Node v22.22.0, Linux, gitSha 9ce85a0). Manifest: harness [email protected]. Channel: Telegram group, default queue/delivery settings.

Summary

When a claude-cli turn completes normally (stop_reason: "end_turn") and the agent already delivered its reply via a tool call (e.g. the Telegram message tool), the final output.text is empty by design — the side effect is done. The cli-backend then throws FailoverError({ reason: "empty_response" }) at src/agents/cli-runner.ts:457. Because empty_response is a fallback-triggering reason, the model-fallback chain re-runs the same turn on the configured fallback model, which doesn't know the reply already shipped and produces a second, user-visible reply — a true duplicate in the channel.

This is the same root throw as #89008 (closed completed 2026-06-02), but a distinct trigger: in #89008 the assistant turn had only a thinking block; here the turn produced a tool_use block whose tool call already produced the user-visible reply. #89008's fix appears to cover thinking-only end_turn — the empty-then-fallback path is still live in 2026.6.1 for tool-call-then-empty-text turns.

The user-facing symptom is stronger than #89008's: not just a wrong-model reply, but a literal duplicate of the primary model's reply, posted by a different model with different voice/style, in a production group chat.

Reproduction signature

Real gateway log from a Telegram group inbound on 2026-06-07T19:18 EDT, 2026.6.1:

2026-06-07T19:18:40.135-04:00 [agent/cli-backend] claude live session turn: provider=claude-cli model=claude-sonnet-4-6 durationMs=16641 rawLines=57 outBytes=0 outHash=e3b0c44298fc
2026-06-07T19:18:40.175-04:00 [model-fallback/decision] model fallback decision: decision=candidate_failed requested=anthropic/claude-sonnet-4-6 candidate=anthropic/claude-sonnet-4-6 reason=empty_response next=deepseek/deepseek-v4-pro detail=CLI backend returned an empty response.
2026-06-07T19:18:46.776-04:00 [model-fallback/decision] model fallback decision: decision=candidate_succeeded requested=anthropic/claude-sonnet-4-6 candidate=deepseek/deepseek-v4-pro reason=unknown next=none

outHash=e3b0c44298fc = SHA256 of empty string — same fingerprint #89008 reported. End-user impact: two replies posted to the Telegram group, one from Sonnet (via the message tool on the primary turn), one from DeepSeek (via the fallback turn). The primary model's reply was a normal Telegram-tool delivery; the fallback model's reply was a near-paraphrase of the same content, breaking the agent illusion in a 3-user group.

After emptying the agent's fallbacks chain in openclaw.json (workaround applied 2026-06-07T19:31), later identical conditions show next=none and the duplicate disappears — confirming the empty-throw still fires, but with nothing to fall back to:

2026-06-07T19:52:07.417-04:00 [agent/cli-backend] claude live session turn: provider=claude-cli model=claude-sonnet-4-6 durationMs=41248 rawLines=183 outBytes=0 outHash=e3b0c44298fc
2026-06-07T19:52:07.483-04:00 [model-fallback/decision] model fallback decision: decision=candidate_failed requested=anthropic/claude-sonnet-4-6 candidate=anthropic/claude-sonnet-4-6 reason=empty_response next=none detail=CLI backend returned an empty response.

i.e. the primary turn still delivered correctly via the message tool (user saw one reply), but the backend continues to log a spurious empty_response. The post-2026.5.31 fix (#89008) does not cover this case.

Reproduction recipe

  1. Agent on the claude-cli backend (any model that calls the message tool reliably in groups, e.g. claude-sonnet-4-6).
  2. Channel: Telegram group, requireMention: false, default group delivery (sourceReplyDeliveryMode: "message_tool_only").
  3. Agent's model.fallbacks non-empty (e.g. ["deepseek/deepseek-v4-pro"]).
  4. Send any inbound that prompts a normal reply.
  5. Observe: model calls the message tool, Telegram delivers the reply, then 0.05–0.10s later a second reply from the fallback model is posted — exact same trace/session, two models, one inbound.

Suspected root

src/agents/cli-runner.ts:453-464:

const executeCliAttempt = async (cliSessionIdToUse?: string) => {
  const output = await executePreparedCliRun(context, cliSessionIdToUse);
  const assistantText = output.text.trim();
  if (!assistantText) {
    throw new FailoverError("CLI backend returned an empty response.", {
      reason: "empty_response",
      provider: params.provider,
      model: context.modelId,
      sessionId: params.sessionId,
      lane: params.lane,
    });
  }

The check is text-only and doesn't consider whether the same turn already produced a tool call (which, for channel-tool deliveries like Telegram's message tool, IS the user-visible side effect). After #89008's fix, thinking-only completions appear to be tolerated, but a completion whose final assistant message contains only tool_use block(s) + no text still hits this throw.

Suggested fix

Treat a clean stop_reason completion with empty text but a non-zero tool-call count as a successful no-text turn, not empty_response. Either:

Workaround in place

agents.defaults.model.fallbacks: [] on the Telegram-facing agent. Strictly worse failure mode if claude-cli has a real outage (silent drop instead of fallback), but at least the production group stops getting double replies. Once upstream is patched, re-add the fallback chain.

Severity

Production message duplication in Telegram group chats. Silent — no error surfaces to the user; both replies look "real."

Cross-reference: closed but related #89008 (thinking-only), closed not-planned #84327 (Opus skipping message tool — separate Opus-side bug we'll file independently).

Metadata

Metadata

Assignees

Labels

P1High-priority user-facing bug, regression, or broken workflow.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.impact:message-lossChannel message delivery can be lost, duplicated, or misrouted.issue-rating: 🦞 diamond lobsterVery strong issue quality with high-confidence source-level or clear reproduction.

Type

No type

Fields

Priority

None yet

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions