Skip to content

Echo tracker doesn't cover tool-sent messages in self-chat mode #26770

Description

@danielrobertdinu

Summary

In self-chat mode (selfChatMode: true), messages sent via agent tools (e.g., the message tool during heartbeat/cron jobs) bypass the echo tracker (createEchoTracker). When WhatsApp echoes these messages back as inbound events, they pass access control and trigger a new agent turn, causing double-reply loops.

Environment

  • OpenClaw 2026.2.23
  • WhatsApp channel, self-chat mode (same phone number for user and bot)
  • Cron jobs with delivery.mode: "announce"

Steps to reproduce

  1. Configure self-chat mode: channels.whatsapp.selfChatMode: true with allowFrom containing the bot's own phone number
  2. Set up a cron job (e.g., hourly email check) with sessionTarget: "isolated" and delivery.mode: "announce"
  3. Wait for the cron job to fire and produce output that gets delivered to WhatsApp
  4. Observe the gateway logs

What happens

  1. Cron job runs in isolated session, agent generates a response
  2. Response is delivered to WhatsApp via sock.sendMessage() (outbound)
  3. WhatsApp echoes the message back as an incoming event with fromMe: true
  4. checkInboundAccessControl allows it because isFromMe && isSamePhone is not blocked (line ~419 in channel-web-DHXuNMKT.js)
  5. echoTracker.has(msg.body) returns false — the text was never registered
  6. Message is processed as new inbound → agent responds → loop

Log evidence (timestamps condensed):

18:17:46 — outbound: "Sent message 3EB003A1... -> sha256:0eb78bb3..."
18:17:48 — inbound:  "Inbound message +40740243812 -> +40740243812 (direct, 188 chars)"
18:17:48 — processed: "[Ovo] 🦉 Notat. E doar notificare standard..."

Root cause

createEchoTracker (source: src/web/auto-reply/monitor/echo.ts) maintains a Set of recently sent texts via rememberText(). This is called from the deliver callback inside dispatchReplyWithBufferedBlockDispatcher (the auto-reply flow).

However, messages sent through other code paths — specifically the message tool handler and the cron announce delivery — call sock.sendMessage() directly without registering the text in the echo tracker.

Relevant code locations

  • createEchoTracker: channel-web-DHXuNMKT.js ~line 945
  • rememberSentText (only caller): channel-web-DHXuNMKT.js ~line 1673, inside deliver callback
  • Echo check on inbound: channel-web-DHXuNMKT.js ~line 1748
  • Access control gap: channel-web-DHXuNMKT.js ~line 419 (isFromMe && !isSamePhone — allows same-phone fromMe)

Expected behavior

All outbound WhatsApp messages — whether sent via auto-reply, tool calls, or cron delivery — should be registered in the echo tracker before sending. When the echo returns, it should be detected and dropped.

Possible fixes

  1. Centralize outbound tracking: Register all texts in the echo tracker at the sendMessageWhatsApp level (in outbound-*.js), so every outbound path is covered
  2. Filter isFromMe in self-chat DMs: Add an option to skip fromMe messages even when isSamePhone is true, since the bot should never need to process its own outbound echoes
  3. Prefix-based filtering: In self-chat mode, skip inbound messages that start with the configured responsePrefix (e.g., [Ovo])

Current workaround

Added an anti-self-reply instruction in SOUL.md (system prompt) that tells the LLM to detect and skip its own echoed messages based on the [Ovo] prefix. This works at the LLM level but is a soft guard — it depends on model compliance and wastes tokens on the echo processing turn.

Metadata

Metadata

Assignees

No one assigned

    Labels

    staleMarked as stale due to inactivity

    Type

    No type

    Fields

    Priority

    None yet

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions