Skip to content

[Bug] Heartbeat and normal replies use parallel outbound paths without shared cross-source dedupe, causing duplicate / near-duplicate user-visible messages #60207

@KimGLee

Description

@KimGLee

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    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