# NousResearch/hermes-agent · Discord thread_require_mention option for multi-bot threads

## Open now

- 0 issues, 1 PR open on GitHub.
- PR #25703: fix: add Discord strict mention alias

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.

## TL;DR

- 11 member(s): 0 open issue(s), 4 closed issue(s), 1 open PR(s), 1 merged PR(s), 5 PR(s) closed without merging.
- A fix was merged to main in PR #25445. Verify it addresses your case.
- **Recommended:** read PR #25445 (and any competing PRs below) before filing. Confirm the merged change covers your symptom.

## Maintainer resolution verdict

- Internal-tracker verdict over this cluster's issue members: `partially_resolved` · deciding PR(s): `pr-25445`. This is distinct from the fix status above. Weigh it yourself.

## How this cluster was grouped

- Link evidence (`STRUCTURAL_PARTIAL`): some members are connected by structural evidence (shared changed files, or a closing/superseded reference); the rest are joined by an automated relevance judgement over their text.
- Members outside that structural core may belong to a different underlying problem.
- Pairwise link density `0.15`: the share of member pairs that carry a link, counting a structural link in full and a relevance-only link at 0.36. `1.00` means every pair is linked.

## Closed / duplicate issues

- #16725: Discord known-thread participation bypass cannot be disabled
- #20742: Add Discord strict mention mode for thread follow-ups
- #23084: discord.thread_require_mention YAML key is ignored (only env var works)
- #25312: \[Feature\]: Opt-in DISCORD_THREAD_REQUIRE_MENTION for multi-bot threads

## Prior & competing PRs

- **#23089: fix(discord): add thread_require_mention config option to gate in-thr…**. closed without merge · fix tier `suggested`
  - maintainer direction: category `implemented_elsewhere`; implication `superseded`; refs `d863773c81b4d1c958b2f28b76bcd8b0809d7eac`
  - _suggested-tier fix detail withheld; re-request with `?tier=all` to include it._
- **#25445: feat(discord): add thread_require_mention for multi-bot threads (salvage #25313)**. merged to main · fix tier `verified`
  - maintainer direction: category `merged`
  - files: `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:
  - `gateway/config.py`

    ```diff
    @@ -852,6 +852,8 @@ def load_gateway_config() -> GatewayConfig:
                 if isinstance(discord_cfg, dict):
                     if "require_mention" in discord_cfg and not os.getenv("DISCORD_REQUIRE_MENTION"):
                         os.environ["DISCORD_REQUIRE_MENTION"] = str(discord_cfg["require_mention"]).lower()
    +                if "thread_require_mention" in discord_cfg and not os.getenv("DISCORD_THREAD_REQUIRE_MENTION"):
    +                    os.environ["DISCORD_THREAD_REQUIRE_MENTION"] = str(discord_cfg["thread_require_mention"]).lower()
                     frc = discord_cfg.get("free_response_channels")
                     if frc is not None and not os.getenv("DISCORD_FREE_RESPONSE_CHANNELS"):
                         if isinstance(frc, list):
    ```
  - `gateway/platforms/discord.py`

    ```diff
    @@ -3577,6 +3577,25 @@ def _discord_free_response_channels(self) -> set:
                 return {part.strip() for part in s.split(",") if part.strip()}
             return set()
     
    +    def _discord_thread_require_mention(self) -> bool:
    +        """Return whether thread participation requires @mention to follow up.
    +
    +        When ``False`` (default), once the bot has participated in a thread it
    +        keeps responding to every message in that thread without needing to be
    +        mentioned again — useful for one-on-one conversations.
    +
    +        When ``True``, the @mention requirement is enforced inside threads as
    +        well.  Set this when multiple bots share a thread and you want each
    +        one to only fire on explicit @mention, avoiding bot-to-bot loops or
    +        unwanted cross-replies.
    +        """
    +        configured = self.config.extra.get("thread_require_mention")
    +        if configured is not None:
    +            if isinstance(configured, str):
    +                return configured.lower() not in ("false", "0", "no", "off")
    +            return bool(configured)
    +        return os.getenv("DISCORD_THREAD_REQUIRE_MENTION", "false").lower() in ("true", "1", "yes", "on")
    +
         def _thread_parent_channel(self, channel: Any) -> Any:
             """Return the parent text channel when invoked from a thread."""
             return getattr(channel, "parent", None) or channel
    ```

    ```diff
    @@ -4209,8 +4228,15 @@ async def _handle_message(self, message: DiscordMessage) -> None:
                 )
     
                 # Skip the mention check if the message is in a thread where
    -            # the bot has previously participated (auto-created or replied in).
    -            in_bot_thread = is_thread and thread_id in self._threads
    +            # the bot has previously participated (auto-created or replied in)
    +            # — UNLESS thread_require_mention is enabled, in which case threads
    +            # are gated the same as channels.  Useful when multiple bots share
    +            # a thread.
    +            in_bot_thread = (
    +                is_thread
    +                and thread_id in self._threads
    +                and not self._discord_thread_require_mention()
    +            )
     
                 if require_mention and not is_free_channel and not in_bot_thread:
                     if self._client.user not in message.mentions and not mention_prefix:
    ```
  - `hermes_cli/config.py`

    ```diff
    @@ -1229,6 +1229,7 @@ def _ensure_hermes_home_managed(home: Path):
             "free_response_channels": "",  # Comma-separated channel IDs where bot responds without mention
             "allowed_channels": "",        # If set, bot ONLY responds in these channel IDs (whitelist)
             "auto_thread": True,           # Auto-create threads on @mention in channels (like Slack)
    +        "thread_require_mention": False,  # If True, require @mention in threads too (multi-bot threads)
             "reactions": True,             # Add 👀/✅/❌ reactions to messages during processing
             "channel_prompts": {},         # Per-channel ephemeral system prompts (forum parents apply to child threads)
             # Opt-in DM role-based auth (#12136). By default, DISCORD_ALLOWED_ROLES
    ```
  - `scripts/release.py`

    ```diff
    @@ -605,6 +605,7 @@
         "[redacted:email]": "m0n5t3r",
         "[redacted:email]": "JiayuuWang",
         "[redacted:email]": "jquesnelle",
    +    "[redacted:email]": "simpolism",
         "[redacted:email]": "jjovalle99",
         "[redacted:email]": "Julientalbot",
         "[redacted:email]": "kagura-agent",
    ```
  - `tests/gateway/test_config.py`

    ```diff
    @@ -302,6 +302,43 @@ def test_thread_sessions_per_user_defaults_to_false(self, tmp_path, monkeypatch)
     
             assert config.thread_sessions_per_user is False
     
    +    def test_bridges_discord_thread_require_mention_from_config_yaml(self, tmp_path, monkeypatch):
    +        """discord.thread_require_mention in config.yaml should reach the runtime env var."""
    +        hermes_home = tmp_path / ".hermes"
    +        hermes_home.mkdir()
    +        config_path = hermes_home / "config.yaml"
    +        config_path.write_text(
    +            "discord:\n"
    +            "  thread_require_mention: true\n",
    +            encoding="utf-8",
    +        )
    +
    +        monkeypatch.setenv("HERMES_HOME", str(hermes_home))
    +        monkeypatch.delenv("DISCORD_THREAD_REQUIRE_MENTION", raising=False)
    +
    +        load_gateway_config()
    +
    +        assert os.environ.get("DISCORD_THREAD_REQUIRE_MENTION") == "true"
    +
    +    def test_thread_require_mention_yaml_does_not_overwrite_env(self, tmp_path, monkeypatch):
    +        """Explicit env var should win over config.yaml (env > yaml precedence)."""
    +        hermes_home = tmp_path / ".hermes"
    +        hermes_home.mkdir()
    +        config_path = hermes_home / "config.yaml"
    +        config_path.write_text(
    +            "discord:\n"
    +            "  thread_require_mention: false\n",
    +            encoding="utf-8",
    +        )
    +
    +        monkeypatch.setenv("HERMES_HOME", str(hermes_home))
    +        monkeypatch.setenv("DISCORD_THREAD_REQUIRE_MENTION", "true")  # user override
    +
    +        load_gateway_config()
    +
    +        # Env value preserved, not clobbered by yaml.
    +        assert os.environ.get("DISCORD_THREAD_REQUIRE_MENTION") == "true"
    +
         def test_bridges_quoted_false_platform_enabled_from_config_yaml(self, tmp_path, monkeypatch):
             hermes_home = tmp_path / ".hermes"
             hermes_home.mkdir()
    ```
  - `tests/gateway/test_discord_free_response.py`

    ```diff
    @@ -88,6 +88,20 @@ def adapter(monkeypatch):
         monkeypatch.setattr(discord_platform.discord, "Thread", FakeThread, raising=False)
         monkeypatch.setattr(discord_platform.discord, "ForumChannel", FakeForumChannel, raising=False)
     
    +    # Clear DISCORD_* env vars the test file exercises so tests don't leak
    +    # process-env state from the contributor's shell into per-test behaviour.
    +    # Individual tests still monkeypatch.setenv() for their own scenarios.
    +    for _var in (
    +        "DISCORD_REQUIRE_MENTION",
    +        "DISCORD_THREAD_REQUIRE_MENTION",
    +        "DISCORD_FREE_RESPONSE_CHANNELS",
    +        "DISCORD_AUTO_THREAD",
    +        "DISCORD_NO_THREAD_CHANNELS",
    +        "DISCORD_ALLOWED_CHANNELS",
    +        "DISCORD_IGNORED_CHANNELS",
    +    ):
    +        monkeypatch.delenv(_var, raising=False)
    +
         config = PlatformConfig(enabled=True, token="[redacted:secret-assignment]")
         adapter = DiscordAdapter(config)
         adapter._client = SimpleNamespace(user=SimpleNamespace(id=999))
    ```

    ```diff
    @@ -463,3 +477,73 @@ async def test_discord_voice_linked_parent_thread_still_requires_mention(adapter
         await adapter._handle_message(message)
     
         adapter.handle_message.assert_not_awaited()
    +
    +
    [redacted:email]
    +async def test_discord_thread_default_keeps_responding_after_participation(adapter, monkeypatch):
    +    """Default behavior: once the bot is in a thread, it auto-responds without @mention."""
    +    monkeypatch.setenv("DISCORD_REQUIRE_MENTION", "true")
    +    monkeypatch.delenv("DISCORD_FREE_RESPONSE_CHANNELS", raising=False)
    +    monkeypatch.delenv("DISCORD_THREAD_REQUIRE_MENTION", raising=False)
    +
    +    thread = FakeThread(channel_id=456, name="follow-up")
    +    adapter._threads.mark("456")  # bot has previously participated
    +
    +    message = make_message(channel=thread, content="follow-up without mention")
    +    await adapter._handle_message(message)
    +
    +    adapter.handle_message.assert_awaited_once()
    +
    +
    [redacted:email]
    +async def test_discord_thread_require_mention_gates_followups(adapter, monkeypatch):
    +    """When thread_require_mention=true, even bot-participated threads need @mention."""
    +    monkeypatch.setenv("DISCORD_REQUIRE_MENTION", "true")
    +    monkeypatch.setenv("DISCORD_THREAD_REQUIRE_MENTION", "true")
    +    monkeypatch.delenv("DISCORD_FREE_RESPONSE_CHANNELS", raising=False)
    +
    +    thread = FakeThread(channel_id=456, name="multi-bot thread")
    +    adapter._threads.mark("456")  # bot has previously participated
    +
    +    message = make_message(channel=thread, content="ambient chatter — not for me")
    +    await adapter._handle_message(message)
    +
    +    adapter.handle_message.assert_not_awaited()
    +
    +
    [redacted:email]
    +async def test_discord_thread_require_mention_still_responds_when_mentioned(adapter, monkeypatch):
    +    """thread_require_mention=true still lets explicit @mentions through in threads."""
    +    monkeypatch.setenv("DISCORD_REQUIRE_MENTION", "true")
    +    monkeypatch.setenv("DISCORD_THREAD_REQUIRE_MENTION", "true")
    +    monkeypatch.delenv("DISCORD_FREE_RESPONSE_CHANNELS", raising=False)
    +
    +    thread = FakeThread(channel_id=456, name="multi-bot thread")
    +    adapter._threads.mark("456")
    +    bot_user = adapter._client.user
    +
    +    message = make_message(
    +        channel=thread,
    +        content=f"<@{bot_user.id}> hey, this one's for you",
    +        mentions=[bot_user],
    +    )
    +    await adapter._handle_message(message)
    +
    +    adapter.handle_message.assert_awaited_once()
    +
    +
    [redacted:email]
    +async def test_discord_thread_require_mention_via_config_extra(adapter, monkeypatch):
    +    """thread_require_mention can also be set via config.extra (yaml)."""
    +    monkeypatch.setenv("DISCORD_REQUIRE_MENTION", "true")
    +    monkeypatch.delenv("DISCORD_THREAD_REQUIRE_MENTION", raising=False)
    +    monkeypatch.delenv("DISCORD_FREE_RESPONSE_CHANNELS", raising=False)
    +    adapter.config.extra["thread_require_mention"] = True
    +
    +    thread = FakeThread(channel_id=456, name="multi-bot thread")
    +    adapter._threads.mark("456")
    +
    +    message = make_message(channel=thread, content="ambient — should be ignored")
    +    await adapter._handle_message(message)
    +
    +    adapter.handle_message.assert_not_awaited()
    ```
  - `website/docs/user-guide/messaging/discord.md`

    ```diff
    @@ -277,6 +277,7 @@ Discord behavior is controlled through two files: **`~/.hermes/.env`** for crede
     | `DISCORD_HOME_CHANNEL_NAME` | No | `"Home"` | Display name for the home channel in logs and status output. |
     | `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. |
     | `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. |
    +| `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`. |
     | `DISCORD_FREE_RESPONSE_CHANNELS` | No | — | Comma-separated channel IDs where the bot responds without requiring an `@mention`, even when `DISCORD_REQUIRE_MENTION` is `true`. |
     | `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. |
     | `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. |
    ```

    ```diff
    @@ -302,6 +303,7 @@ The `discord` section in `~/.hermes/config.yaml` mirrors the env vars above. Con
     # Discord-specific settings
     discord:
       require_mention: true           # Require @mention in server channels
    +  thread_require_mention: false   # If true, require @mention in threads too (multi-bot threads)
       free_response_channels: ""      # Comma-separated channel IDs (or YAML list)
       auto_thread: true               # Auto-create threads on @mention
       reactions: true                 # Add emoji reactions during processing
    ```

    ```diff
    @@ -324,6 +326,20 @@ group_sessions_per_user: true     # Isolate sessions per user in shared channels
     
     When enabled, the bot only responds in server channels when directly `@mentioned`. DMs always get a response regardless of this setting.
     
    +#### `discord.thread_require_mention`
    +
    +**Type:** boolean — **Default:** `false`
    +
    +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.
    +
    +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.
    +
    +```yaml
    +discord:
    +  require_mention: true
    +  thread_require_mention: true    # multi-bot setup
    +```
    +
     #### `discord.free_response_channels`
     
     **Type:** string or list — **Default:** `""`
    ```

    ```diff
    @@ -350,7 +366,7 @@ Free-response channels also **skip auto-threading** — the bot replies inline r
     
     **Type:** boolean — **Default:** `true`
     
    -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.
    +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.
     
     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.
     
    ```
- **#16722: feat(discord): add strict mention mode**. closed without merge
  - maintainer direction: category `implemented_elsewhere`; implication `superseded`; refs `d863773c81b4d1c958b2f28b76bcd8b0809d7eac`, `#16725`
- **#23187: fix(discord): honor thread_require_mention config**. closed without merge
  - maintainer direction: category `implemented_elsewhere`; implication `superseded`; refs `d863773c81b4d1c958b2f28b76bcd8b0809d7eac`, `#23292`
- **#23292: fix(gateway): forward discord.thread_require_mention YAML key to env var**. closed without merge
  - maintainer direction: category `duplicate_of`; implication `duplicate`; refs `#23089`, `#23084`
- **#25313: feat(discord): add thread_require_mention for multi-bot threads**. closed without merge
  - maintainer direction: category `merged_via`; implication `superseded`; refs `d863773c8`, `#25445`
- **#25703: fix: add Discord strict mention alias**. open
  - maintainer direction: category `uncategorized`; implication `needs_llm`; refs `cc8e5ec2afbfd10a3cff4e710210dd9ecae64a33`
  - files: `gateway/config.py`, `gateway/platforms/discord.py`, `tests/e2e/conftest.py`, `tests/gateway/test_discord_free_response.py`

## Fix status

- Merged to main in PR #25445 (cited above). No release data is tracked. Verify downstream.
- Fix last verified (merged) `2026-05-14`.
- Subsystem: `gateway`
