openclaw - 💡(How to fix) Fix [Bug]: message_tool_only causes agent self-reply loops because successful message(send) reawakens the LLM like any other tool result

Official PRs (…)
ON THIS PAGE

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…

In sourceReplyDeliveryMode === "message_tool_only", a successful message(action=send) is persisted as a toolResult and immediately continues the agent loop, so the next LLM iteration sees the agent's own just-sent message in context. Many models read this as a cue to keep speaking, producing chained sends, self-replies, and on weaker models, hallucinated continuations.

The shape is structural — the runner has two different "wake the LLM" rules:

Persisted messageWakes the LLM?
user (inbound from channel)No — only when the dispatcher fires a new turn
toolResultYes — agent loop iterates automatically

message(action=send) is semantically a user-facing reply (a speech-act), but transport-wise it's a tool that returns a toolResult, so it gets the wrong rule. From the LLM's POV the conversation continued and it still has the floor.

Root Cause

In sourceReplyDeliveryMode === "message_tool_only", a successful message(action=send) is persisted as a toolResult and immediately continues the agent loop, so the next LLM iteration sees the agent's own just-sent message in context. Many models read this as a cue to keep speaking, producing chained sends, self-replies, and on weaker models, hallucinated continuations.

The shape is structural — the runner has two different "wake the LLM" rules:

Persisted messageWakes the LLM?
user (inbound from channel)No — only when the dispatcher fires a new turn
toolResultYes — agent loop iterates automatically

message(action=send) is semantically a user-facing reply (a speech-act), but transport-wise it's a tool that returns a toolResult, so it gets the wrong rule. From the LLM's POV the conversation continued and it still has the floor.

Fix Action

Fix / Workaround

Persisted messageWakes the LLM?
user (inbound from channel)No — only when the dispatcher fires a new turn
toolResultYes — agent loop iterates automatically

Why prompt-side mitigation isn't enough

Why hook-based mitigation isn't enough

RAW_BUFFERClick to expand / collapse

Summary

In sourceReplyDeliveryMode === "message_tool_only", a successful message(action=send) is persisted as a toolResult and immediately continues the agent loop, so the next LLM iteration sees the agent's own just-sent message in context. Many models read this as a cue to keep speaking, producing chained sends, self-replies, and on weaker models, hallucinated continuations.

The shape is structural — the runner has two different "wake the LLM" rules:

Persisted messageWakes the LLM?
user (inbound from channel)No — only when the dispatcher fires a new turn
toolResultYes — agent loop iterates automatically

message(action=send) is semantically a user-facing reply (a speech-act), but transport-wise it's a tool that returns a toolResult, so it gets the wrong rule. From the LLM's POV the conversation continued and it still has the floor.

Reproduction

  • Telegram (or any channel) with sourceReplyDeliveryMode === "message_tool_only".
  • Send a message to the bot.
  • Often: the agent emits multiple message(action=send) calls within one turn with no intervening information-gathering tool. The second/third sends frequently restate or extend the first ("actually, also...", "let me add..."), drifting into hallucinated continuations.

Real session on 2026.5.19 with minimax/MiniMax-M2.7 in a Telegram group: 103 successful sends accumulated; 63 confirmed consecutive send → send pairs (same chat, same turn, no other tool between).

Why prompt-side mitigation isn't enough

src/agents/system-prompt.ts already tries:

"If you use message (action=send) to deliver visible output, do not repeat that visible content in your final answer."

Works on strong models. On smaller/faster models — the ones people deploy for cheap chat — the structural cue of "your output came back to you as a tool result" overpowers the prompt.

Why hook-based mitigation isn't enough

I wrote a local plugin (home/extensions/) that uses tool_result_persist to flag "last action was a successful send" and before_tool_call to return { block: true } on a chained send.

  • tool_result_persist fires reliably — confirmed on disk, 99/108 message toolResults carry the rewritten "stop, end the turn" content.
  • before_tool_call does not reliably block the second send. Couldn't pin down whether it's a context-mismatch or inter-call state issue without instrumenting the runner.
  • Even when a block does land, the synthetic blocked toolResult is fed back to the LLM, which retries or works around it.

Crucially, no hook return type lets a plugin say "persist this result, end the run, wait for the next dispatcher trigger." All of before_tool_call, tool_result_persist, after_tool_call, before_message_write, and before_agent_finalize either run mid-loop with no end-run signal, or have no access to the run's abort controller.

Proposed shape (not prescriptive)

Rule: In message_tool_only mode, after executing the current iteration's tool-use batch, if the last tool in that batch was a successful message(action=send), end the run. Persist the toolResult as normal — it's history, the model should see it next turn — but do not loop back to the LLM. The next turn starts on the next dispatcher trigger (new inbound message, heartbeat, etc.), exactly like after an automatic-mode reply.

Why "last in the batch" instead of "any send anywhere":

  • [message(send)] → end. (the core bug case, fixed)
  • [message(send), message(send)] → last is send → end. (chained sends in one batch, fixed)
  • [exec, message(send)] → last is send → end. (do work, then send → done)
  • [message(send), exec] → last is exec → continue. (send interim update, then go check something — legitimate; the exec result feeds the next LLM call)
  • [exec, message(send), exec] → last is exec → continue. (send interim, then more work — legitimate)
  • Failed send (details.ok === false) → not terminal, loop continues so model can recover.

The rule preserves "do work → final send" and "send interim → keep working", and only kills the loop on the shape that actually causes the self-reply pathology. Scoping rides on the existing message_tool_only mode — no new config flag; users who don't want this go back to automatic.

How that's exposed is your call — sketches:

  • Tool-side: message(send) sets a "terminal" marker on its AgentToolResult; the runner honours it when the marker is on the last result of the batch.
  • Hook-side: add endRun?: boolean to PluginHookToolResultPersistResult.
  • Runner-side built-in: hard-coded policy keyed off sourceReplyDeliveryMode === "message_tool_only".

Impact

  • Affects all message_tool_only channels (Telegram, Slack, Discord per src/agents/system-prompt.ts:902) in groups and DMs.
  • Worst on smaller models — the deployments most users pick for cost reasons.
  • Hard to detect from outside: the user sees several messages of vaguely-related-but-drifting content; the agent thinks it's being helpful.

Environment

  • OpenClaw 2026.5.19
  • Provider/model: minimax/MiniMax-M2.7 (Anthropic Messages transport); also observed with several other models per reporter
  • Channel: Telegram groups (channels.telegram.groups.*)
  • Self-hosted gateway, podman, single-bot setup

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

openclaw - 💡(How to fix) Fix [Bug]: message_tool_only causes agent self-reply loops because successful message(send) reawakens the LLM like any other tool result