OpenClaw’s Discord Duplicate-Split Bug Shows Why Agent Delivery Needs Logical-Message Dedupe
Duplicate messages are funny exactly once. After that, they are a delivery-governance bug. OpenClaw issue #84130 reports a Discord channel/thread failure where an assistant intentionally sent a long visible reply via message(action=send), Discord split it into two messages, and then OpenClaw’s normal final delivery path sent the same logical reply again as another two-message split pair. The user saw four messages. The platform thought it was doing the safe thing twice.
The interesting part is not that Discord split a long message. Chunking is table stakes for real channels. The bug is that OpenClaw’s duplicate suppression appears to operate too close to physical transport chunks and not reliably enough at the logical-message level. The user experienced one answer. The assistant intentionally delivered one answer. The runtime had enough machinery to avoid duplicate delivery in other paths. Yet one long reply became two split batches.
The user sees a reply, not chunks
The reported environment is OpenClaw 2026.5.12 in a Discord channel/thread session. The sender pattern matters: the assistant explicitly called message(action=send) for visible output, and the final assistant output was expected to be silent, effectively NO_REPLY. The first delivered split pair had message IDs 1505846828413747270 and 1505846829349077123. The duplicate delivered split pair had message IDs 1505846840614977587 and 1505846843081228400.
This was not repeated text inside a single message. It was the same logical long reply split into two chunks, then delivered again as another two chunks. That distinction is the whole story. A dedupe system that only compares individual physical message bodies can miss the fact that the final path is about to re-deliver the same logical answer already sent by the messaging tool. If the tool path records chunks and the final path compares an unsplit string, suppression can fail. If target normalization differs between Discord channel and thread metadata, suppression can fail. If split chunks are not recorded as one logical send plus physical delivery IDs, suppression can fail.
The issue references earlier duplicate-delivery work: #44467 covered message.send triggering duplicate delivery-mirror replies, and #65493 covered delivery-mirror duplicate messages sent via message(action=send). Existing shipped guards cited by the reporter include transcript-only openclaw/delivery-mirror and openclaw/gateway-injected assistant messages being ignored in handleMessageEnd(); successful message.send completions committed to normalized dedupe state; and message_end block replies skipped when text duplicates a successful messaging-tool send. Those mechanisms are good. This bug is the channel-specific edge case that escaped them.
Idempotency belongs at the conversation layer
Agent delivery systems are deceptively hard because the user sees one conversational artifact while the platform handles many internal artifacts: tool calls, source replies, progress updates, final assistant text, transcript mirrors, delivery mirrors, channel chunks, retries, cleanup events, and sometimes preview edits. The moment those artifacts can re-enter visible delivery independently, duplicate bugs appear. Here, the agent already sent the answer, and the runtime also decided it had a final answer to deliver. Discord chunking turned one logical duplicate into two visible duplicates.
The fix shape should be conversation-layer idempotency. Dedupe should key on normalized target plus logical reply identity, not merely on raw transport packets. If a 3,500-character answer is chunked into two Discord messages, the platform should record one logical send with two physical message IDs. Then, when the final delivery path sees the same normalized text headed to the same Discord channel/thread target, it can suppress it even if its representation is an unsplit string.
That sounds straightforward until you account for real channels. Discord threads and channels have different metadata paths. Slack has its own thread semantics and update behavior. Telegram can split, edit, and route by topic. Email has completely different delivery and retry expectations. WebChat is controlled by the platform but still has transcript mirrors. A dedupe rule that works in WebChat is not delivery correctness for Discord.
This is where audit logs become product features. A good agent runtime should be able to answer four questions without screenshot archaeology: which subsystem decided to deliver this text, what target did it believe it was using, what prior sends were recorded, and why did suppression pass or fail? If the answer is “we compare strings somewhere near message_end,” operators will keep rediscovering duplicates by watching channels get spammed.
The issue’s suggested regression test is the right one: Discord channel/thread session, long message.send that triggers splitting, final delivery with the same logical text, assert only one split batch is sent. That captures the real failure shape. It does not test “dedupe works” in the abstract; it tests the channel mutation that broke the invariant.
For builders, the checklist is short. Normalize targets before dedupe. Record chunk batches as one logical send plus physical delivery IDs. Suppress final delivery against successful logical sends. Keep transcript mirrors and delivery mirrors out of visible reply loops. Test every channel that mutates output shape. Idempotency at the transport retry layer is not enough. Agent platforms need idempotency at the conversation layer, because that is where users feel the bug.
The editorial take: a helpful assistant should not become a duplicate-message cannon with excellent intentions. If the platform cannot tell that it already said the thing, the model is not the problem. The delivery ledger is.
Sources: GitHub issue #84130, issue #44467, issue #65493, PR #84126