NousResearch/hermes-agent

Discord thread_require_mention option for multi-bot threads

Cluster 16725 · gateway. Data as of

1 open item. Fix merged in #25445 on 14 May 2026. Check it covers your case.

The short answer

0open issues
1open PR
11items grouped
Known?
Yes. 11 related items grouped here, 4 issues already closed.
Fix event
A fix was merged to main in PR #25445. Check it covers your case. No release data is tracked.
Tracker
The maintainers' tracker marks this partially resolved, deciding PR #25445. That is separate from the fix event.
Still open
0 issues and 1 PR.

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.

Open issues0

No open issues.

Open PRs1

  • PR#25703fix: add Discord strict mention alias

Fixes on record

Merged to main. A stamp counts the issues a fix is linked to.

  • #25445 feat(discord): add thread_require_mention for multi-bot threads (salvage #25313)

    merged to main

    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

    Show the diff
    • gateway/config.py
      @@ -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
      @@ -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
      @@ -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
      @@ -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
      @@ -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
      @@ -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
      @@ -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))
      @@ -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
      @@ -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. |
      @@ -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
      @@ -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:** `""`
      @@ -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.
       

5 PRs closed without merging

From the close comments: done elsewhere 3 · duplicate 1 · merged via another change 1

  • #16722 feat(discord): add strict mention mode · done elsewhere → d863773, #16725
  • #23089 fix(discord): add thread_require_mention config option to gate in-thr… · done elsewhere → d863773
  • #23187 fix(discord): honor thread_require_mention config · done elsewhere → d863773, #23292
  • #23292 fix(gateway): forward discord.thread_require_mention YAML key to env var · duplicate → #23089, #23084
  • #25313 feat(discord): add thread_require_mention for multi-bot threads · merged via another change → d863773, #25445
Closed issues · 4
  • Closed#16725Discord known-thread participation bypass cannot be disabled
  • Closed#20742Add Discord strict mention mode for thread follow-ups
  • Closed#23084discord.thread_require_mention YAML key is ignored (only env var works)
  • Closed#25312[Feature]: Opt-in DISCORD_THREAD_REQUIRE_MENTION for multi-bot threads
How this cluster was grouped
  • Link evidence (partly structural links): 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 item 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.

Pick up a Lab task

Check whether a fix covers the open issues here, with your own agent. Bring back evidence for maintainers.

Open the Lab

At a glance

Subsystem
gateway
Merged PRs
#25445
Closed issues
4
Attempts
5 PRs closed without merging
Fix link checked
14 May 2026

For your agent

The same facts, typed.