Summary
OpenClaw can send duplicate or near-duplicate user-visible messages when a normal assistant reply and a heartbeat-generated reply happen around the same topic/time.
This is not just a config issue. The root cause appears to be a structural outbound architecture gap:
heartbeat replies and normal replies use parallel send paths
- both ultimately deliver through the same outbound delivery layer
- but there is no shared cross-source suppression/deduplication layer before delivery
As a result, both replies can be considered independently valid and both get sent.
Root cause
1. Heartbeat has its own independent send path
Heartbeat replies are generated and sent in:
src/infra/heartbeat-runner.ts
That path:
- builds heartbeat payloads
- performs heartbeat-only dedupe
- then directly calls
deliverOutboundPayloads(...)
The dedupe there only compares heartbeat state such as:
lastHeartbeatText
lastHeartbeatSentAt
So it can suppress:
- heartbeat → heartbeat duplicates
But it does not suppress:
- normal reply → heartbeat near-duplicates
2. Normal assistant replies use a separate independent path
Normal user-facing replies go through:
src/auto-reply/reply/dispatch-from-config.ts
src/auto-reply/reply/route-reply.ts
That path also independently decides a payload is valid and sends it.
There is no shared “recently sent equivalent content” suppression step that is aware of heartbeat-originated deliveries.
3. Shared delivery exists, but shared dedupe does not
Both paths eventually reach the outbound delivery layer:
deliverOutboundPayloads(...)
However, this layer is acting as a transport/delivery boundary, not as a cross-source dedupe/suppression boundary.
So today the system effectively behaves like this:
- normal reply path decides “send”
- heartbeat path decides “send”
- both independently reach delivery
- no unified pre-delivery gate says:
- “a highly similar reply was just sent from another source”
- “main conversation already replied; heartbeat should only send delta / suppress”
- “direct chat should suppress heartbeat chatter immediately after a normal reply”
Observed effect
In direct chats especially, this can produce:
- duplicate user-visible messages
- near-duplicate status updates
- heartbeat follow-up messages that restate what the assistant already just said
- the impression that the assistant is “replying twice”
This is especially noticeable when:
- heartbeat polling is active
- the user is also actively chatting in the same session
- both flows respond to the same recent context
Why this is a code-logic / architecture issue, not just config
This is not primarily caused by a bad heartbeat prompt or a delivery toggle.
The issue is that the system currently lacks a shared policy such as:
- cross-source outbound dedupe between heartbeat and normal replies
- “main reply already sent recently, heartbeat only sends meaningful delta”
- direct-chat heartbeat cooldown after recent assistant reply
- a unified pre-delivery suppression layer across reply sources
Related issues already touch this problem space
There are already multiple issues describing nearby symptoms and adjacent failure modes in this area, including heartbeat duplication, multi-session heartbeat responses, main-session pollution, duplicated cron/heartbeat deliveries, and duplicate visible assistant messages.
Examples include:
These issues all point at overlapping symptoms, but the common structural cause appears to be:
- multiple outbound reply producers
- one shared delivery layer
- no unified cross-source suppression gate before user-visible delivery
So this issue is intended to capture the system-level coordination problem behind that symptom family, not just one isolated surface manifestation.
Relevant code areas
src/infra/heartbeat-runner.ts
src/auto-reply/reply/dispatch-from-config.ts
src/auto-reply/reply/route-reply.ts
- outbound delivery path via
deliverOutboundPayloads(...)
Practical conclusion
This appears to be a system-level coordination bug:
- two parallel outbound reply pipelines
- one shared delivery layer
- no unified cross-source suppression before user-visible delivery
That structural gap is what allows duplicate / near-duplicate replies to reach the user.
Summary
OpenClaw can send duplicate or near-duplicate user-visible messages when a normal assistant reply and a heartbeat-generated reply happen around the same topic/time.
This is not just a config issue. The root cause appears to be a structural outbound architecture gap:
heartbeatreplies andnormalreplies use parallel send pathsAs a result, both replies can be considered independently valid and both get sent.
Root cause
1. Heartbeat has its own independent send path
Heartbeat replies are generated and sent in:
src/infra/heartbeat-runner.tsThat path:
deliverOutboundPayloads(...)The dedupe there only compares heartbeat state such as:
lastHeartbeatTextlastHeartbeatSentAtSo it can suppress:
But it does not suppress:
2. Normal assistant replies use a separate independent path
Normal user-facing replies go through:
src/auto-reply/reply/dispatch-from-config.tssrc/auto-reply/reply/route-reply.tsThat path also independently decides a payload is valid and sends it.
There is no shared “recently sent equivalent content” suppression step that is aware of heartbeat-originated deliveries.
3. Shared delivery exists, but shared dedupe does not
Both paths eventually reach the outbound delivery layer:
deliverOutboundPayloads(...)However, this layer is acting as a transport/delivery boundary, not as a cross-source dedupe/suppression boundary.
So today the system effectively behaves like this:
Observed effect
In direct chats especially, this can produce:
This is especially noticeable when:
Why this is a code-logic / architecture issue, not just config
This is not primarily caused by a bad heartbeat prompt or a delivery toggle.
The issue is that the system currently lacks a shared policy such as:
Related issues already touch this problem space
There are already multiple issues describing nearby symptoms and adjacent failure modes in this area, including heartbeat duplication, multi-session heartbeat responses, main-session pollution, duplicated cron/heartbeat deliveries, and duplicate visible assistant messages.
Examples include:
[Bug] heartbeat target: "last" causes multiple sessions to respond simultaneouslyHeartbeat fires per-session instead of per-agent, causing duplicate HEARTBEAT_OK responses[Bug]: Possible regression: heartbeat sessions can re-trigger on local exec completed events and spam duplicate heartbeat log entries[Bug]: Telegram DMs can still land in agent:main:main, polluting heartbeat/main session after #40519[Bug]: Cron job with sessionTarget: "main" triggers both systemEvent and reminder despite delivery.mode: "none"Cron/heartbeat deliveries duplicated 2-6x per trigger[Bug]: Webchat shows duplicate assistant messages because chat.history returns delivery-mirror transcript entries as normal assistant messages[Bug]: Webchat duplicates assistant replies via delivery-mirror transcript entryCommentary text can leak into final assistant replies, and duplicate visible replies can occur after tool sends[Bug]: Cron delivery duplicates announce mode messages to Discord[Bug]: Gateway lacks circuit breaker — model repetition loop sent 32 duplicate messages to Telegram in 3 minutesThese issues all point at overlapping symptoms, but the common structural cause appears to be:
So this issue is intended to capture the system-level coordination problem behind that symptom family, not just one isolated surface manifestation.
Relevant code areas
src/infra/heartbeat-runner.tssrc/auto-reply/reply/dispatch-from-config.tssrc/auto-reply/reply/route-reply.tsdeliverOutboundPayloads(...)Practical conclusion
This appears to be a system-level coordination bug:
That structural gap is what allows duplicate / near-duplicate replies to reach the user.