Skip to content

fix(discord): prevent duplicate user message processing on auto-thread and reconnects (#51057) - #51153

Closed
JoaoMarcos44 wants to merge 3 commits into
NousResearch:mainfrom
JoaoMarcos44:fix/51057-discord-duplicate-messages
Closed

JoaoMarcos44 wants to merge 3 commits into
NousResearch:mainfrom
JoaoMarcos44:fix/51057-discord-duplicate-messages

Conversation

@JoaoMarcos44

@JoaoMarcos44 JoaoMarcos44 commented Jun 23, 2026 •

Copy link
Copy Markdown

What does this PR do?

This PR prevents duplicate processing and double agent runs when a single user message is received in Discord. It addresses two distinct root causes:

  1. Auto-threading Context Race (Different Snowflake IDs): When a user sends a message in a text channel and auto_thread is enabled, the bot creates a thread for that message. This triggers a second MESSAGE_CREATE event for the "thread starter message" with message.id == thread.id and type=0 (default). Since the two dispatches represent different contexts (parent channel vs. thread channel), they carry different message (snowflake) IDs. This bypasses the standard message.id deduplication and the message type filter, resulting in two parallel agent runs.
  2. Adapter Recreation/Reconnect (Same Snowflake ID): When the connection drops and a new DiscordAdapter instance is initialized, its local self._dedup cache starts empty, failing to block duplicate messages replayed during the RESUME flow.

To resolve these, we implement:

  • Content-Based Deduplication (Adapter-Level): A sliding window of 5 seconds to drop duplicate messages with the same author and content text (u:{author_id}:{content}) arriving in different contexts.
  • Cross-Adapter Deduplication (Gateway-Level): A platform-level message ID deduplicator on the GatewayRunner which persists across adapter re-connections and instances.

Related Issue

Fixes #51057

Type of Change

  • 🐛 Bug fix (non-breaking change that fixes an issue)
  • ✅ Tests (adding or improving test coverage)

Changes Made

  • plugins/platforms/discord/adapter.py: Added content-based deduplication using self._content_dedup = MessageDeduplicator(max_size=500, ttl_seconds=5). Dropped duplicate deliveries (author+content) within 5 seconds.
  • gateway/run.py: Added cross-adapter deduplication using self._platform_dedup on the GatewayRunner class to prevent duplicate processing of the same message ID across adapter restarts.
  • tests/gateway/test_discord_cross_adapter_dedup.py: Added comprehensive unit tests for cross-adapter and content deduplication behavior.

How to Test

  1. Run the newly added test suite to verify correct deduplication logic under various scenarios:
    pytest tests/gateway/test_discord_cross_adapter_dedup.py
  2. Run existing Discord adapter tests to ensure no regressions:
    pytest tests/gateway/test_discord_free_response.py

Checklist

Code

  • I've read the Contributing Guide
  • My commit messages follow Conventional Commits (fix(scope):, feat(scope):, etc.)
  • I searched for existing PRs to make sure this isn't a duplicate
  • My PR contains only changes related to this fix/feature (no unrelated commits)
  • I've run pytest tests/ -q and all tests pass
  • I've added tests for my changes (required for bug fixes, strongly encouraged for features)
  • I've tested on my platform: Windows 11

Documentation & Housekeeping

  • I've updated relevant documentation (README, docs/, docstrings) — or N/A
  • I've updated cli-config.yaml.example if I added/changed config keys — or N/A
  • I've updated CONTRIBUTING.md or AGENTS.md if I changed architecture or workflows — or N/A
  • I've considered cross-platform impact (Windows, macOS) per the compatibility guide — or N/A
  • I've updated tool descriptions/schemas if I changed tool behavior — or N/A

Screenshots / Logs

Logs of successful test run:

tests\gateway\test_discord_cross_adapter_dedup.py ..........             [100%]
============================= 10 passed in 0.86s ==============================
tests\gateway\test_discord_free_response.py ............................ [ 65%]
...............                                                          [100%]
============================= 43 passed in 1.26s ==============================

@alt-glitch alt-glitch added type/bug Something isn't working P2 Medium — degraded but workaround exists comp/gateway Gateway runner, session dispatch, delivery platform/discord Discord bot adapter sweeper:risk-message-delivery Sweeper risk: may drop, duplicate, misroute, or suppress messages duplicate This issue or pull request already exists labels Jun 23, 2026
@alt-glitch

Copy link
Copy Markdown

This was generated by AI during triage.

Duplicate of #51129 — both fix the same #51057 regression where Discord auto_thread emits a second on_message under the thread context (different snowflake id) that bypasses message.id dedup. #51129 (open, earlier) takes the narrower thread-starter-dedup approach; this PR adds broader content-based + cross-adapter deduplication. Flagging for maintainer to pick the canonical approach.

@JoaoMarcos44
JoaoMarcos44 deleted the fix/51057-discord-duplicate-messages branch June 23, 2026 02:45
@JoaoMarcos44
JoaoMarcos44 restored the fix/51057-discord-duplicate-messages branch June 23, 2026 02:46
@JoaoMarcos44 JoaoMarcos44 reopened this Jun 23, 2026
@alt-glitch alt-glitch removed the duplicate This issue or pull request already exists label Jun 23, 2026

@tonydwb tonydwb left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adds cross-adapter message deduplication to prevent duplicate processing when Discord RESUME replays events after reconnects. Also adds content-based dedup with 5-second TTL to catch same user message delivered in multiple Discord contexts (channel vs thread). Good fix for issue #51057. Well-scoped to 2 files.


Reviewed by Hermes Agent

JoaoMarcos44 and others added 2 commits June 23, 2026 00:43
…_init__

Tests using object.__new__(GatewayRunner) skip __init__ and therefore never
set _platform_dedup.  The committed dedup check used self._platform_dedup.get()
directly, raising AttributeError in every test that dispatched a message with a
message_id (all e2e tests and many gateway unit tests).

- Made the access defensive via getattr() in _handle_message so any stub runner
  that lacks the attribute auto-initialises it on first use.
- Added _platform_dedup = {} to conftest.make_runner to document the expected
  attribute alongside the other runner fields.
- Added test_handle_message_defensive_platform_dedup to verify the fallback.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…dapter dedup false-positives

The gateway-level _platform_dedup (added for Discord reconnect protection)
correctly blocks messages whose message_id was already seen on the same
platform.  Tests that call _handle_message multiple times with the same
hardcoded message_id="m1" were silently getting the second call dropped
(returning None), causing assertion failures:

  test_slash_access_dispatch.py::test_non_admin_with_empty_user_commands_gets_floor_only
  test_telegram_topic_mode.py::test_topic_root_command_explicitly_migrates_and_enables_topic_mode

In production every Discord/Telegram message has a unique snowflake/id.
The test event factories should reflect this — each factory call now
generates a fresh UUID-based id, matching real-world uniqueness and
ensuring the dedup never fires on logically distinct test messages.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@kshitijk4poor

Copy link
Copy Markdown

Thanks for tackling #51057 as well — closing this in favor of #51129, which fixed the same bug and was submitted first.

Both PRs correctly identified the auto-thread starter-message root cause. We went with #51129's approach because it's deterministic: it pre-seeds str(thread.id) into the existing _dedup cache, which neutralizes the exact ID the duplicate thread-starter event carries (per Discord's API, thread.id == starter message.id) with no false-positive risk.

The content-based dedup here (u:{author}:{content} with a 5s TTL) would also drop a legitimate repeated message — e.g. a user sending "yes" twice within 5s gets no response — which the deterministic approach avoids. The cross-adapter _platform_dedup on GatewayRunner addresses a different concern (RESUME-replay across reconnects) than #51057 and touches core; if that's a real issue worth fixing, it'd be welcome as its own scoped PR.

Merged fix: #51552 (#51552). Appreciate the thorough write-up and tests!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp/gateway Gateway runner, session dispatch, delivery P2 Medium — degraded but workaround exists platform/discord Discord bot adapter sweeper:risk-message-delivery Sweeper risk: may drop, duplicate, misroute, or suppress messages type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Discord: single user message dispatched twice → 2 agent runs / 2 responses (v0.17.0)

4 participants