{"repo":"NousResearch/hermes-agent","public_id":"16725","display_label":"Discord thread_require_mention option for multi-bot threads","summary":"Members all concern requiring explicit mentions in Discord threads (a strict mention mode, also called thread_require_mention), including feature requests, implementation PRs, and a report that the known-thread participation bypass cannot be disabled. Some items also cover the YAML key not being forwarded to the environment variable and an alias for the strict mention setting.","subsystem":"gateway","counts":{"members":11,"open_issues":0,"open_issues_distinct":0,"open_prs":1,"closed_issues":4,"merged_prs":1,"attempt_prs":6},"landed_fix":{"status":"merged_to_main","basis":"structural","pr":25445,"merged_at":"2026-05-14T05:21:44+00:00"},"resolution":{"state":"partially_resolved","by":"pr-25445"},"freshness":{"state":"verified","last_verified_at":"2026-05-14T05:21:44+00:00","is_stale":false},"grouping":{"backbone":"STRUCTURAL_PARTIAL","link_density":0.15345454545454548},"fix_edges":[{"issue_number":25312,"pr_number":25445,"tier":"verified","basis":"closing_ref","confidence":0.95,"recoverable_from_timeline":true,"pr_merged_at":"2026-05-14T05:21:44+00:00"}],"members":[{"kind":"issue","number":16725,"title":"Discord known-thread participation bypass cannot be disabled","url":null,"state":"closed"},{"kind":"issue","number":20742,"title":"Add Discord strict mention mode for thread follow-ups","url":null,"state":"closed"},{"kind":"issue","number":23084,"title":"discord.thread_require_mention YAML key is ignored (only env var works)","url":null,"state":"closed"},{"kind":"issue","number":25312,"title":"[Feature]: Opt-in DISCORD_THREAD_REQUIRE_MENTION for multi-bot threads","url":null,"state":"closed"},{"kind":"pr","number":16722,"title":"feat(discord): add strict mention mode","url":null,"state":"closed","close_reason":{"refs":[{"kind":"commit","value":"d863773c81b4d1c958b2f28b76bcd8b0809d7eac","number":0},{"kind":"pr","value":"#16725","number":16725}],"category":"implemented_elsewhere","implication":"superseded"},"merged":false},{"kind":"pr","number":23089,"title":"fix(discord): add thread_require_mention config option to gate in-thr…","url":null,"state":"closed","close_reason":{"refs":[{"kind":"commit","value":"d863773c81b4d1c958b2f28b76bcd8b0809d7eac","number":0}],"category":"implemented_elsewhere","implication":"superseded"},"merged":false,"is_fix_pr":true,"fix_tier":"suggested","fix_detail_withheld":true},{"kind":"pr","number":23187,"title":"fix(discord): honor thread_require_mention config","url":null,"state":"closed","close_reason":{"refs":[{"kind":"commit","value":"d863773c81b4d1c958b2f28b76bcd8b0809d7eac","number":0},{"kind":"pr","value":"#23292","number":23292}],"category":"implemented_elsewhere","implication":"superseded"},"merged":false},{"kind":"pr","number":23292,"title":"fix(gateway): forward discord.thread_require_mention YAML key to env var","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#23089","number":23089},{"kind":"pr","value":"#23084","number":23084}],"category":"duplicate_of","implication":"duplicate"},"merged":false},{"kind":"pr","number":25313,"title":"feat(discord): add thread_require_mention for multi-bot threads","url":null,"state":"closed","close_reason":{"refs":[{"kind":"commit","value":"d863773c8","number":0},{"kind":"pr","value":"#25445","number":25445}],"category":"merged_via","implication":"superseded"},"merged":false},{"kind":"pr","number":25445,"title":"feat(discord): add thread_require_mention for multi-bot threads (salvage #25313)","url":null,"state":"merged","close_reason":{"category":"merged"},"merged":true,"is_fix_pr":true,"fix_tier":"verified","files_changed":["gateway/config.py","gateway/platforms/discord.py","hermes_cli/config.py","scripts/release.py","tests/gateway/test_config.py","tests/gateway/test_discord_free_response.py","website/docs/user-guide/messaging/discord.md"],"fix_hunks":[{"file":"gateway/config.py","hunks":["@@ -852,6 +852,8 @@ def load_gateway_config() -> GatewayConfig:\n             if isinstance(discord_cfg, dict):\n                 if \"require_mention\" in discord_cfg and not os.getenv(\"DISCORD_REQUIRE_MENTION\"):\n                     os.environ[\"DISCORD_REQUIRE_MENTION\"] = str(discord_cfg[\"require_mention\"]).lower()\n+                if \"thread_require_mention\" in discord_cfg and not os.getenv(\"DISCORD_THREAD_REQUIRE_MENTION\"):\n+                    os.environ[\"DISCORD_THREAD_REQUIRE_MENTION\"] = str(discord_cfg[\"thread_require_mention\"]).lower()\n                 frc = discord_cfg.get(\"free_response_channels\")\n                 if frc is not None and not os.getenv(\"DISCORD_FREE_RESPONSE_CHANNELS\"):\n                     if isinstance(frc, list):"]},{"file":"gateway/platforms/discord.py","hunks":["@@ -3577,6 +3577,25 @@ def _discord_free_response_channels(self) -> set:\n             return {part.strip() for part in s.split(\",\") if part.strip()}\n         return set()\n \n+    def _discord_thread_require_mention(self) -> bool:\n+        \"\"\"Return whether thread participation requires @mention to follow up.\n+\n+        When ``False`` (default), once the bot has participated in a thread it\n+        keeps responding to every message in that thread without needing to be\n+        mentioned again — useful for one-on-one conversations.\n+\n+        When ``True``, the @mention requirement is enforced inside threads as\n+        well.  Set this when multiple bots share a thread and you want each\n+        one to only fire on explicit @mention, avoiding bot-to-bot loops or\n+        unwanted cross-replies.\n+        \"\"\"\n+        configured = self.config.extra.get(\"thread_require_mention\")\n+        if configured is not None:\n+            if isinstance(configured, str):\n+                return configured.lower() not in (\"false\", \"0\", \"no\", \"off\")\n+            return bool(configured)\n+        return os.getenv(\"DISCORD_THREAD_REQUIRE_MENTION\", \"false\").lower() in (\"true\", \"1\", \"yes\", \"on\")\n+\n     def _thread_parent_channel(self, channel: Any) -> Any:\n         \"\"\"Return the parent text channel when invoked from a thread.\"\"\"\n         return getattr(channel, \"parent\", None) or channel","@@ -4209,8 +4228,15 @@ async def _handle_message(self, message: DiscordMessage) -> None:\n             )\n \n             # Skip the mention check if the message is in a thread where\n-            # the bot has previously participated (auto-created or replied in).\n-            in_bot_thread = is_thread and thread_id in self._threads\n+            # the bot has previously participated (auto-created or replied in)\n+            # — UNLESS thread_require_mention is enabled, in which case threads\n+            # are gated the same as channels.  Useful when multiple bots share\n+            # a thread.\n+            in_bot_thread = (\n+                is_thread\n+                and thread_id in self._threads\n+                and not self._discord_thread_require_mention()\n+            )\n \n             if require_mention and not is_free_channel and not in_bot_thread:\n                 if self._client.user not in message.mentions and not mention_prefix:"]},{"file":"hermes_cli/config.py","hunks":["@@ -1229,6 +1229,7 @@ def _ensure_hermes_home_managed(home: Path):\n         \"free_response_channels\": \"\",  # Comma-separated channel IDs where bot responds without mention\n         \"allowed_channels\": \"\",        # If set, bot ONLY responds in these channel IDs (whitelist)\n         \"auto_thread\": True,           # Auto-create threads on @mention in channels (like Slack)\n+        \"thread_require_mention\": False,  # If True, require @mention in threads too (multi-bot threads)\n         \"reactions\": True,             # Add 👀/✅/❌ reactions to messages during processing\n         \"channel_prompts\": {},         # Per-channel ephemeral system prompts (forum parents apply to child threads)\n         # Opt-in DM role-based auth (#12136). By default, DISCORD_ALLOWED_ROLES"]},{"file":"scripts/release.py","hunks":["@@ -605,6 +605,7 @@\n     \"[redacted:email]\": \"m0n5t3r\",\n     \"[redacted:email]\": \"JiayuuWang\",\n     \"[redacted:email]\": \"jquesnelle\",\n+    \"[redacted:email]\": \"simpolism\",\n     \"[redacted:email]\": \"jjovalle99\",\n     \"[redacted:email]\": \"Julientalbot\",\n     \"[redacted:email]\": \"kagura-agent\","]},{"file":"tests/gateway/test_config.py","hunks":["@@ -302,6 +302,43 @@ def test_thread_sessions_per_user_defaults_to_false(self, tmp_path, monkeypatch)\n \n         assert config.thread_sessions_per_user is False\n \n+    def test_bridges_discord_thread_require_mention_from_config_yaml(self, tmp_path, monkeypatch):\n+        \"\"\"discord.thread_require_mention in config.yaml should reach the runtime env var.\"\"\"\n+        hermes_home = tmp_path / \".hermes\"\n+        hermes_home.mkdir()\n+        config_path = hermes_home / \"config.yaml\"\n+        config_path.write_text(\n+            \"discord:\\n\"\n+            \"  thread_require_mention: true\\n\",\n+            encoding=\"utf-8\",\n+        )\n+\n+        monkeypatch.setenv(\"HERMES_HOME\", str(hermes_home))\n+        monkeypatch.delenv(\"DISCORD_THREAD_REQUIRE_MENTION\", raising=False)\n+\n+        load_gateway_config()\n+\n+        assert os.environ.get(\"DISCORD_THREAD_REQUIRE_MENTION\") == \"true\"\n+\n+    def test_thread_require_mention_yaml_does_not_overwrite_env(self, tmp_path, monkeypatch):\n+        \"\"\"Explicit env var should win over config.yaml (env > yaml precedence).\"\"\"\n+        hermes_home = tmp_path / \".hermes\"\n+        hermes_home.mkdir()\n+        config_path = hermes_home / \"config.yaml\"\n+        config_path.write_text(\n+            \"discord:\\n\"\n+            \"  thread_require_mention: false\\n\",\n+            encoding=\"utf-8\",\n+        )\n+\n+        monkeypatch.setenv(\"HERMES_HOME\", str(hermes_home))\n+        monkeypatch.setenv(\"DISCORD_THREAD_REQUIRE_MENTION\", \"true\")  # user override\n+\n+        load_gateway_config()\n+\n+        # Env value preserved, not clobbered by yaml.\n+        assert os.environ.get(\"DISCORD_THREAD_REQUIRE_MENTION\") == \"true\"\n+\n     def test_bridges_quoted_false_platform_enabled_from_config_yaml(self, tmp_path, monkeypatch):\n         hermes_home = tmp_path / \".hermes\"\n         hermes_home.mkdir()"]},{"file":"tests/gateway/test_discord_free_response.py","hunks":["@@ -88,6 +88,20 @@ def adapter(monkeypatch):\n     monkeypatch.setattr(discord_platform.discord, \"Thread\", FakeThread, raising=False)\n     monkeypatch.setattr(discord_platform.discord, \"ForumChannel\", FakeForumChannel, raising=False)\n \n+    # Clear DISCORD_* env vars the test file exercises so tests don't leak\n+    # process-env state from the contributor's shell into per-test behaviour.\n+    # Individual tests still monkeypatch.setenv() for their own scenarios.\n+    for _var in (\n+        \"DISCORD_REQUIRE_MENTION\",\n+        \"DISCORD_THREAD_REQUIRE_MENTION\",\n+        \"DISCORD_FREE_RESPONSE_CHANNELS\",\n+        \"DISCORD_AUTO_THREAD\",\n+        \"DISCORD_NO_THREAD_CHANNELS\",\n+        \"DISCORD_ALLOWED_CHANNELS\",\n+        \"DISCORD_IGNORED_CHANNELS\",\n+    ):\n+        monkeypatch.delenv(_var, raising=False)\n+\n     config = PlatformConfig(enabled=True, token=\"[redacted:secret-assignment]\")\n     adapter = DiscordAdapter(config)\n     adapter._client = SimpleNamespace(user=SimpleNamespace(id=999))","@@ -463,3 +477,73 @@ async def test_discord_voice_linked_parent_thread_still_requires_mention(adapter\n     await adapter._handle_message(message)\n \n     adapter.handle_message.assert_not_awaited()\n+\n+\n[redacted:email]\n+async def test_discord_thread_default_keeps_responding_after_participation(adapter, monkeypatch):\n+    \"\"\"Default behavior: once the bot is in a thread, it auto-responds without @mention.\"\"\"\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+\n+    thread = FakeThread(channel_id=456, name=\"follow-up\")\n+    adapter._threads.mark(\"456\")  # bot has previously participated\n+\n+    message = make_message(channel=thread, content=\"follow-up without mention\")\n+    await adapter._handle_message(message)\n+\n+    adapter.handle_message.assert_awaited_once()\n+\n+\n[redacted:email]\n+async def test_discord_thread_require_mention_gates_followups(adapter, monkeypatch):\n+    \"\"\"When thread_require_mention=true, even bot-participated threads need @mention.\"\"\"\n+    monkeypatch.setenv(\"DISCORD_REQUIRE_MENTION\", \"true\")\n+    monkeypatch.setenv(\"DISCORD_THREAD_REQUIRE_MENTION\", \"true\")\n+    monkeypatch.delenv(\"DISCORD_FREE_RESPONSE_CHANNELS\", raising=False)\n+\n+    thread = FakeThread(channel_id=456, name=\"multi-bot thread\")\n+    adapter._threads.mark(\"456\")  # bot has previously participated\n+\n+    message = make_message(channel=thread, content=\"ambient chatter — not for me\")\n+    await adapter._handle_message(message)\n+\n+    adapter.handle_message.assert_not_awaited()\n+\n+\n[redacted:email]\n+async def test_discord_thread_require_mention_still_responds_when_mentioned(adapter, monkeypatch):\n+    \"\"\"thread_require_mention=true still lets explicit @mentions through in threads.\"\"\"\n+    monkeypatch.setenv(\"DISCORD_REQUIRE_MENTION\", \"true\")\n+    monkeypatch.setenv(\"DISCORD_THREAD_REQUIRE_MENTION\", \"true\")\n+    monkeypatch.delenv(\"DISCORD_FREE_RESPONSE_CHANNELS\", raising=False)\n+\n+    thread = FakeThread(channel_id=456, name=\"multi-bot thread\")\n+    adapter._threads.mark(\"456\")\n+    bot_user = adapter._client.user\n+\n+    message = make_message(\n+        channel=thread,\n+        content=f\"<@{bot_user.id}> hey, this one's for you\",\n+        mentions=[bot_user],\n+    )\n+    await adapter._handle_message(message)\n+\n+    adapter.handle_message.assert_awaited_once()\n+\n+\n[redacted:email]\n+async def test_discord_thread_require_mention_via_config_extra(adapter, monkeypatch):\n+    \"\"\"thread_require_mention can also be set via config.extra (yaml).\"\"\"\n+    monkeypatch.setenv(\"DISCORD_REQUIRE_MENTION\", \"true\")\n+    monkeypatch.delenv(\"DISCORD_THREAD_REQUIRE_MENTION\", raising=False)\n+    monkeypatch.delenv(\"DISCORD_FREE_RESPONSE_CHANNELS\", raising=False)\n+    adapter.config.extra[\"thread_require_mention\"] = True\n+\n+    thread = FakeThread(channel_id=456, name=\"multi-bot thread\")\n+    adapter._threads.mark(\"456\")\n+\n+    message = make_message(channel=thread, content=\"ambient — should be ignored\")\n+    await adapter._handle_message(message)\n+\n+    adapter.handle_message.assert_not_awaited()"]},{"file":"website/docs/user-guide/messaging/discord.md","hunks":["@@ -277,6 +277,7 @@ Discord behavior is controlled through two files: **`~/.hermes/.env`** for crede\n | `DISCORD_HOME_CHANNEL_NAME` | No | `\"Home\"` | Display name for the home channel in logs and status output. |\n | `DISCORD_COMMAND_SYNC_POLICY` | No | `\"safe\"` | Controls native slash-command startup sync. `\"safe\"` diffs existing global commands and only updates what changed, recreating commands when Discord metadata changes cannot be applied via patch. `\"bulk\"` preserves the old `tree.sync()` behavior. `\"off\"` skips startup sync entirely. |\n | `DISCORD_REQUIRE_MENTION` | No | `true` | When `true`, the bot only responds in server channels when `@mentioned`. Set to `false` to respond to all messages in every channel. |\n+| `DISCORD_THREAD_REQUIRE_MENTION` | No | `false` | When `true`, the in-thread mention shortcut is disabled — threads are gated the same as channels, requiring `@mention` even after the bot has already participated. Use this when multiple bots share a thread and you want each to fire only on explicit `@mention`. |\n | `DISCORD_FREE_RESPONSE_CHANNELS` | No | — | Comma-separated channel IDs where the bot responds without requiring an `@mention`, even when `DISCORD_REQUIRE_MENTION` is `true`. |\n | `DISCORD_IGNORE_NO_MENTION` | No | `true` | When `true`, the bot stays silent if a message `@mentions` other users but does **not** mention the bot. Prevents the bot from jumping into conversations directed at other people. Only applies in server channels, not DMs. |\n | `DISCORD_AUTO_THREAD` | No | `true` | When `true`, automatically creates a new thread for every `@mention` in a text channel, so each conversation is isolated (similar to Slack behavior). Messages already inside threads or DMs are unaffected. |","@@ -302,6 +303,7 @@ The `discord` section in `~/.hermes/config.yaml` mirrors the env vars above. Con\n # Discord-specific settings\n discord:\n   require_mention: true           # Require @mention in server channels\n+  thread_require_mention: false   # If true, require @mention in threads too (multi-bot threads)\n   free_response_channels: \"\"      # Comma-separated channel IDs (or YAML list)\n   auto_thread: true               # Auto-create threads on @mention\n   reactions: true                 # Add emoji reactions during processing","@@ -324,6 +326,20 @@ group_sessions_per_user: true     # Isolate sessions per user in shared channels\n \n When enabled, the bot only responds in server channels when directly `@mentioned`. DMs always get a response regardless of this setting.\n \n+#### `discord.thread_require_mention`\n+\n+**Type:** boolean — **Default:** `false`\n+\n+By default, once the bot has participated in a thread (auto-created on `@mention` or replied in once), it keeps responding to every subsequent message in that thread without needing to be `@mentioned` again. That's the right default for one-on-one conversations.\n+\n+In **multi-bot threads** where users address one bot per turn, this default becomes a footgun — every other bot in the thread also fires on every message, burning credits and spamming the channel. Set `thread_require_mention: true` to disable the in-thread shortcut and gate threads the same way channels are gated. Explicit `@mentions` still work as before.\n+\n+```yaml\n+discord:\n+  require_mention: true\n+  thread_require_mention: true    # multi-bot setup\n+```\n+\n #### `discord.free_response_channels`\n \n **Type:** string or list — **Default:** `\"\"`","@@ -350,7 +366,7 @@ Free-response channels also **skip auto-threading** — the bot replies inline r\n \n **Type:** boolean — **Default:** `true`\n \n-When enabled, every `@mention` in a regular text channel automatically creates a new thread for the conversation. This keeps the main channel clean and gives each conversation its own isolated session history. Once a thread is created, subsequent messages in that thread don't require `@mention` — the bot knows it's already participating.\n+When enabled, every `@mention` in a regular text channel automatically creates a new thread for the conversation. This keeps the main channel clean and gives each conversation its own isolated session history. Once a thread is created, subsequent messages in that thread don't require `@mention` — the bot knows it's already participating. Set [`thread_require_mention`](#discordthread_require_mention) to `true` to disable this in-thread shortcut for multi-bot setups.\n \n Messages sent in existing threads or DMs are unaffected by this setting. Channels listed in `discord.free_response_channels` or `discord.no_thread_channels` also bypass auto-threading and get inline replies instead.\n \n"]}]},{"kind":"pr","number":25703,"title":"fix: add Discord strict mention alias","url":null,"state":"open","close_reason":{"refs":[{"kind":"commit","value":"cc8e5ec2afbfd10a3cff4e710210dd9ecae64a33","number":0}],"category":"uncategorized","implication":"needs_llm"},"merged":false}],"trust":{"default_tier":"verified","includes_suggested":false}}