From a52041b2e0ab13d79c4721dd391794268b5be487 Mon Sep 17 00:00:00 2001 From: James Date: Thu, 4 Jun 2026 12:17:14 -0700 Subject: [PATCH] fix: avoid masking missed Discord parent messages Preserve startup missed-message backfill behavior while avoiding false address classifications from unrelated parent-channel messages. --- plugins/platforms/discord/adapter.py | 19 ++++++++--- .../test_discord_missed_message_backfill.py | 33 ++++++++++++++++++- 2 files changed, 47 insertions(+), 5 deletions(-) diff --git a/plugins/platforms/discord/adapter.py b/plugins/platforms/discord/adapter.py index 609020a54567f..8a66f104773c8 100644 --- a/plugins/platforms/discord/adapter.py +++ b/plugins/platforms/discord/adapter.py @@ -2159,7 +2159,7 @@ class DiscordAdapter(BasePlatformAdapter): if bot_id is None: return False - async def _scan_history(channel: Any) -> bool: + async def _scan_history(channel: Any, *, allow_unreferenced_bot_response: bool) -> bool: history = getattr(channel, "history", None) if not callable(history): return False @@ -2172,17 +2172,28 @@ class DiscordAdapter(BasePlatformAdapter): continue reference = getattr(candidate, "reference", None) ref_id = str(getattr(reference, "message_id", "") or "") - if not ref_id or ref_id == str(getattr(message, "id", "")): + if ref_id == str(getattr(message, "id", "")): + return True + if allow_unreferenced_bot_response and not ref_id: return True except Exception: return False return False - if await _scan_history(getattr(message, "channel", None)): + message_channel = getattr(message, "channel", None) + # In a thread, bot responses are usually ordinary unreferenced messages + # after the user's post. In a parent channel, however, an unreferenced + # bot post after a user message is not evidence that it answered this + # specific message; otherwise one successful reply can mask multiple + # missed parent-channel posts after a rate-limit/outage burst. + if await _scan_history( + message_channel, + allow_unreferenced_bot_response=bool(getattr(message_channel, "parent_id", None)), + ): return True thread = getattr(message, "thread", None) - if thread is not None and await _scan_history(thread): + if thread is not None and await _scan_history(thread, allow_unreferenced_bot_response=True): return True return False diff --git a/tests/gateway/test_discord_missed_message_backfill.py b/tests/gateway/test_discord_missed_message_backfill.py index 408ddc84a8c84..acb7c283b921f 100644 --- a/tests/gateway/test_discord_missed_message_backfill.py +++ b/tests/gateway/test_discord_missed_message_backfill.py @@ -60,8 +60,9 @@ class FakeReaction: class FakeChannel: - def __init__(self, channel_id=123, history_messages=None): + def __init__(self, channel_id=123, history_messages=None, parent_id=None): self.id = channel_id + self.parent_id = parent_id self.name = "wiki-inbox" self.guild = SimpleNamespace(name="emo") self.topic = None @@ -124,6 +125,36 @@ async def test_should_not_backfill_message_with_non_down_bot_response(adapter): assert await adapter._should_backfill_discord_message(message) is False +@pytest.mark.asyncio +async def test_parent_channel_unreferenced_bot_message_does_not_suppress_backfill(adapter): + unrelated_bot_post = SimpleNamespace( + id=2, + content="Done — captured a different item.", + author=SimpleNamespace(id=999, bot=True), + reference=None, + created_at=datetime.now(timezone.utc), + ) + channel = FakeChannel(history_messages=[unrelated_bot_post]) + message = make_message(message_id=1, channel=channel) + + assert await adapter._should_backfill_discord_message(message) is True + + +@pytest.mark.asyncio +async def test_thread_unreferenced_bot_message_suppresses_backfill(adapter): + bot_post = SimpleNamespace( + id=2, + content="Done — captured it.", + author=SimpleNamespace(id=999, bot=True), + reference=None, + created_at=datetime.now(timezone.utc), + ) + thread = FakeChannel(channel_id=456, parent_id=123, history_messages=[bot_post]) + message = make_message(message_id=1, channel=thread) + + assert await adapter._should_backfill_discord_message(message) is False + + @pytest.mark.asyncio async def test_backfills_when_only_down_notice_exists(adapter): down_notice = SimpleNamespace(