Bug
Discord and Slack adapters process replayed events after reconnects, causing duplicate bot responses.
Root Cause
When Discord's gateway drops and RESUMEs, it replays events from the disconnect window. Similarly, Slack Socket Mode reconnects can redeliver events if the ack was lost before disconnect. Neither adapter tracks which messages have already been processed.
Our gateway logs show ~7 RESUME events per day on Discord alone:
2026-04-02 03:33:07 INFO discord.gateway: Shard ID None has successfully RESUMED session...
2026-04-02 03:35:11 INFO discord.gateway: Shard ID None has successfully RESUMED session...
2026-04-02 05:49:57 INFO discord.gateway: Shard ID None has successfully RESUMED session...
(... 4 more on the same day)
Existing Pattern
Six other platform adapters already implement this correctly:
| Adapter |
Dedup Field |
File |
| Mattermost |
_seen_posts |
gateway/platforms/mattermost.py:99-102 |
| Matrix |
_processed_events |
gateway/platforms/matrix.py:117-136 |
| WeCom |
_seen_messages |
gateway/platforms/wecom.py:95-96 |
| Feishu |
_seen_messages |
gateway/platforms/feishu.py:952-955 |
| DingTalk |
_seen_messages |
gateway/platforms/dingtalk.py:89-90 |
| Email |
_seen_uids |
gateway/platforms/email.py:238-240 |
Discord and Slack are the two most popular platforms and both lack this guard.
Suggested Fix
Add a _seen_messages dict (message ID → timestamp) with 5-min TTL and 2000-entry cap to both adapters. The check goes at the very top of the message handler, before any other logic.
Discord (gateway/platforms/discord.py) — in __init__:
self._seen_messages: Dict[str, float] = {}
self._SEEN_TTL = 300 # 5 minutes
self._SEEN_MAX = 2000 # prune threshold
At the top of on_message, before the message.author == self._client.user check:
# Dedup: Discord RESUME replays events after reconnects.
msg_id = str(message.id)
now = time.time()
if msg_id in adapter_self._seen_messages:
return
adapter_self._seen_messages[msg_id] = now
if len(adapter_self._seen_messages) > adapter_self._SEEN_MAX:
cutoff = now - adapter_self._SEEN_TTL
adapter_self._seen_messages = {
k: v for k, v in adapter_self._seen_messages.items()
if v > cutoff
}
Slack (gateway/platforms/slack.py) — same pattern in __init__, and at the top of _handle_slack_message using event.get("ts") as the dedup key.
Why not _active_sessions / _running_agents?
These existing guards in base.py and run.py only prevent concurrent processing of the same session. Once the first response completes and the agent is cleaned up, a replayed event passes right through and triggers a second response.
Environment
- Hermes Agent @
cc54818d (latest main as of 2026-04-03)
- Discord.py with frequent RESUME cycles (~7/day)
- Slack Socket Mode with intermittent reconnects
Bug
Discord and Slack adapters process replayed events after reconnects, causing duplicate bot responses.
Root Cause
When Discord's gateway drops and RESUMEs, it replays events from the disconnect window. Similarly, Slack Socket Mode reconnects can redeliver events if the ack was lost before disconnect. Neither adapter tracks which messages have already been processed.
Our gateway logs show ~7 RESUME events per day on Discord alone:
Existing Pattern
Six other platform adapters already implement this correctly:
_seen_postsgateway/platforms/mattermost.py:99-102_processed_eventsgateway/platforms/matrix.py:117-136_seen_messagesgateway/platforms/wecom.py:95-96_seen_messagesgateway/platforms/feishu.py:952-955_seen_messagesgateway/platforms/dingtalk.py:89-90_seen_uidsgateway/platforms/email.py:238-240Discord and Slack are the two most popular platforms and both lack this guard.
Suggested Fix
Add a
_seen_messagesdict (message ID → timestamp) with 5-min TTL and 2000-entry cap to both adapters. The check goes at the very top of the message handler, before any other logic.Discord (
gateway/platforms/discord.py) — in__init__:At the top of
on_message, before themessage.author == self._client.usercheck:Slack (
gateway/platforms/slack.py) — same pattern in__init__, and at the top of_handle_slack_messageusingevent.get("ts")as the dedup key.Why not
_active_sessions/_running_agents?These existing guards in
base.pyandrun.pyonly prevent concurrent processing of the same session. Once the first response completes and the agent is cleaned up, a replayed event passes right through and triggers a second response.Environment
cc54818d(latest main as of 2026-04-03)