{"repo":"NousResearch/hermes-agent","public_id":"33666","display_label":"Discord thread history backfill decoupling fixes","summary":"These issues and pull requests address a bug where Discord thread history backfill was being skipped or incorrectly gated by mention or bot-thread conditions. The work involves decoupling backfill logic from mention requirements and related gateway context handling.","subsystem":null,"counts":{"members":4,"open_issues":0,"open_issues_distinct":0,"open_prs":0,"closed_issues":1,"merged_prs":1,"attempt_prs":2},"landed_fix":{"status":"merged_to_main","basis":"structural","pr":33827,"merged_at":"2026-05-28T11:52:03+00:00"},"resolution":{"state":"resolved","by":"pr-33827"},"freshness":{"state":"verified","last_verified_at":"2026-05-28T11:52:03+00:00","is_stale":false},"grouping":{"backbone":"STRUCTURAL_SPANNING","link_density":0.5},"fix_edges":[{"issue_number":33666,"pr_number":33827,"tier":"verified","basis":"closing_ref","confidence":0.95,"recoverable_from_timeline":true,"pr_merged_at":"2026-05-28T11:52:03+00:00"}],"members":[{"kind":"issue","number":33666,"title":"Discord thread backfill skipped when in_bot_thread is true (thread_require_mention=false)","url":null,"state":"closed"},{"kind":"pr","number":33684,"title":"fix(gateway): backfill Discord thread context","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#33827","number":33827},{"kind":"pr","value":"#33705","number":33705},{"kind":"pr","value":"#31467","number":31467}],"category":"merged_via","implication":"superseded"},"merged":false},{"kind":"pr","number":33705,"title":"fix(discord): decouple thread history backfill from mention gating","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#33684","number":33684},{"kind":"pr","value":"#33827","number":33827}],"category":"merged_via","implication":"superseded"},"merged":false},{"kind":"pr","number":33827,"title":"fix(discord): decouple thread history backfill from mention gating","url":null,"state":"merged","close_reason":{"category":"merged"},"merged":true,"is_fix_pr":true,"fix_tier":"verified","files_changed":["plugins/platforms/discord/adapter.py","scripts/release.py","tests/gateway/test_discord_free_response.py","tests/gateway/test_discord_slash_commands.py"],"fix_hunks":[{"file":"plugins/platforms/discord/adapter.py","hunks":["@@ -4811,14 +4811,19 @@ async def _handle_message(self, message: DiscordMessage) -> None:\n         # to keep the partition rule clean.\n         _channel_context = None\n         _is_dm = isinstance(message.channel, discord.DMChannel)\n-        if not _is_dm:\n-            _needed_mention = (\n-                require_mention\n-                and not is_free_channel\n-                and not in_bot_thread\n-            )\n-            _backfill_enabled = self._discord_history_backfill()\n-            if _needed_mention and _backfill_enabled:\n+        if not _is_dm and self._discord_history_backfill():\n+            # Run backfill when there's a real gap to fill:\n+            #   - mention-gated channels with no free-response override\n+            #     (messages between bot turns aren't in the transcript)\n+            #   - any thread (in_bot_thread bypasses the mention check, but\n+            #     processing-window gaps and post-restart context still need\n+            #     recovery)\n+            # DMs skip entirely because every DM message triggers the bot,\n+            # so the session transcript already has everything.\n+            # Auto-threaded messages also skip — we just created the thread,\n+            # there's nothing prior to backfill.\n+            _has_mention_gap = require_mention and not is_free_channel and not in_bot_thread\n+            if (_has_mention_gap or is_thread) and auto_threaded_channel is None:\n                 _backfill_text = await self._fetch_channel_context(\n                     message.channel, before=message,\n                 )"]},{"file":"scripts/release.py","hunks":["@@ -52,6 +52,7 @@\n     \"270604154+superearn-fisher@users.noreply.github.com\": \"superearn-fisher\",\n     \"3540493+kpadilha@users.noreply.github.com\": \"kpadilha\",\n     \"40378218+chaconne67@users.noreply.github.com\": \"chaconne67\",\n+    \"Pluviobyte@users.noreply.github.com\": \"Pluviobyte\",\n     \"[redacted:email]\": \"sanghyuk-seo-nexcube\",\n     \"[redacted:email]\": \"Brixyy\",\n     \"[redacted:email]\": \"wangpuv\","]},{"file":"tests/gateway/test_discord_free_response.py","hunks":["@@ -851,6 +851,27 @@ async def test_discord_per_user_channel_backfills_too(adapter, monkeypatch):\n     assert event.channel_context == \"[Recent channel messages]\\n[Alice] context\"\n \n \n[redacted:email]\n+async def test_discord_participated_thread_backfills_without_mention(adapter, monkeypatch):\n+    \"\"\"Known threads still need recent thread context when mention gating is bypassed.\"\"\"\n+    monkeypatch.setenv(\"DISCORD_REQUIRE_MENTION\", \"true\")\n+    monkeypatch.delenv(\"DISCORD_FREE_RESPONSE_CHANNELS\", raising=False)\n+    monkeypatch.delenv(\"DISCORD_THREAD_REQUIRE_MENTION\", raising=False)\n+    adapter.config.extra[\"history_backfill\"] = True\n+    adapter._fetch_channel_context = AsyncMock(return_value=\"[Recent channel messages]\\n[Alice] thread context\")\n+\n+    thread = FakeThread(channel_id=456, name=\"follow-up\")\n+    adapter._threads.mark(\"456\")\n+\n+    message = make_message(channel=thread, content=\"follow-up without mention\")\n+    await adapter._handle_message(message)\n+\n+    adapter._fetch_channel_context.assert_awaited_once()\n+    event = adapter.handle_message.await_args.args[0]\n+    assert event.text == \"follow-up without mention\"\n+    assert event.channel_context == \"[Recent channel messages]\\n[Alice] thread context\"\n+\n+\n @pytest.mark.asyncio\n async def test_discord_dm_does_not_backfill(adapter, monkeypatch):\n     \"\"\"DMs skip backfill — every DM triggers the bot, so there's no mention gap.\"\"\"","@@ -884,3 +905,25 @@ async def test_discord_dm_does_not_backfill(adapter, monkeypatch):\n         assert event.channel_context is None\n \n \n[redacted:email]\n+async def test_discord_auto_thread_skips_backfill(adapter, monkeypatch):\n+    \"\"\"Auto-created threads skip backfill — the thread is brand new with no prior context.\"\"\"\n+    monkeypatch.setenv(\"DISCORD_REQUIRE_MENTION\", \"true\")\n+    monkeypatch.setenv(\"DISCORD_AUTO_THREAD\", \"true\")\n+    monkeypatch.delenv(\"DISCORD_NO_THREAD_CHANNELS\", raising=False)\n+    monkeypatch.delenv(\"DISCORD_FREE_RESPONSE_CHANNELS\", raising=False)\n+    adapter.config.extra[\"history_backfill\"] = True\n+\n+    fake_thread = FakeThread(channel_id=777, name=\"auto-thread\")\n+    adapter._auto_create_thread = AsyncMock(return_value=fake_thread)\n+    adapter._fetch_channel_context = AsyncMock(return_value=\"[Recent channel messages]\\n[Alice] noise\")\n+\n+    bot_user = adapter._client.user\n+    parent = FakeTextChannel(channel_id=200, name=\"general\")\n+    message = make_message(channel=parent, content=\"hello\", mentions=[bot_user])\n+    await adapter._handle_message(message)\n+\n+    adapter._auto_create_thread.assert_awaited_once()\n+    adapter._fetch_channel_context.assert_not_awaited()\n+\n+"]},{"file":"tests/gateway/test_discord_slash_commands.py","hunks":["@@ -624,6 +624,13 @@ def __init__(self, channel_id=100, name=\"general\", guild_name=\"TestGuild\"):\n         self.guild = SimpleNamespace(name=guild_name, id=1)\n         self.topic = None\n \n+    def history(self, *args, **kwargs):\n+        async def _empty():\n+            return\n+            yield  # pragma: no cover — make this an async generator\n+\n+        return _empty()\n+\n \n class _FakeThreadChannel(_discord_mod.Thread):\n     \"\"\"isinstance(ch, discord.Thread) → True.\"\"\"","@@ -636,6 +643,13 @@ def __init__(self, channel_id=200, name=\"existing-thread\", guild_name=\"TestGuild\n         self.topic = None\n         self.parent = SimpleNamespace(id=parent_id, name=\"general\", guild=SimpleNamespace(name=guild_name, id=1))\n \n+    def history(self, *args, **kwargs):\n+        async def _empty():\n+            return\n+            yield  # pragma: no cover — make this an async generator\n+\n+        return _empty()\n+\n \n def _fake_message(channel, *, content=\"Hello\", author_id=42, display_name=\"Jezza\"):\n     return SimpleNamespace(\n"]}]}],"trust":{"default_tier":"verified","includes_suggested":false}}