fix(gateway): prevent double dispatch of Discord messages via thread-starter dedup

When _auto_create_thread() creates a thread from a user message via
message.create_thread(), Discord fires a second MESSAGE_CREATE event
for the 'thread starter message'.  That starter message carries
message.id == thread.id and may arrive with type=default instead of
type=21 (thread_starter_message), so the existing type filter in
on_message does not catch it — triggering a second call into
_handle_message and thus a second agent run and response.

Fix: after _auto_create_thread succeeds and returns a thread, pre-seed
the dedup cache with str(thread.id) via self._dedup.is_duplicate().
The dedup cache is the same TTL-based MessageDeduplicator that already
guards against Discord RESUME event replays.  Calling is_duplicate()
marks the ID as seen; when the duplicate thread-starter MESSAGE_CREATE
arrives, on_message's guard returns True and the event is dropped.

This is a minimal, targeted fix:
- No new state: reuses the existing _dedup instance
- No timing/race: the pre-seed happens synchronously inside the async
  _handle_message, before the thread-starter event can be dispatched
- Scoped: only fires when auto-threading is enabled AND thread creation
  succeeds (thread object is not None)

Also adds tests in tests/gateway/test_discord_double_dispatch.py
covering the pre-seed behaviour, failure modes (thread creation fails,
auto-thread disabled), and dedup cache integrity.

Closes #51057
This commit is contained in:
manusjs 2026-06-23 01:25:51 +00:00 committed by kshitij
parent 89538d47b8
commit 807bdc17f6
2 changed files with 526 additions and 0 deletions

View file

@ -5285,6 +5285,16 @@ class DiscordAdapter(BasePlatformAdapter):
thread_id = str(thread.id)
auto_threaded_channel = thread
self._threads.mark(thread_id)
# Pre-seed dedup: when _auto_create_thread creates a thread
# via message.create_thread(), Discord fires a second
# MESSAGE_CREATE event for the "thread starter message".
# That starter message carries id == thread.id and may
# arrive with type=default (not type=21/thread_starter_message),
# so the type filter above does not catch it. Marking the
# thread id in the dedup cache now ensures that duplicate
# event is dropped before it can trigger a second agent run.
# Fixes #51057.
self._dedup.is_duplicate(str(thread.id))
referenced_attachments = []
reference = getattr(message, "reference", None)