mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-05-18 04:41:56 +00:00
feat(discord): add thread_require_mention for multi-bot threads
By default, once Hermes participates in a Discord thread (auto-created on @mention or replied in once) it auto-responds to every subsequent message in that thread without requiring further @mentions. That's the right default for one-on-one conversations and isolated channel threads. But it's a confirmed footgun in multi-bot threads. When a user invokes one bot per turn — addressing Codex first, then Hermes — every other bot in the thread also fires on every message, burning credits and spamming the channel. Author has hit this personally in active multi-bot research-team threads. Add a new `discord.thread_require_mention` config key (env: `DISCORD_THREAD_REQUIRE_MENTION`), default `false` to preserve existing behavior. When `true`, the in-thread mention shortcut is disabled and threads are gated the same way channels are. Explicit @mentions still pass through as expected. Mirrors the existing helper shape (config.extra > env > default) and the existing yaml→env bridge pattern used by `require_mention`. Changes: - gateway/platforms/discord.py: new `_discord_thread_require_mention()` helper; in_bot_thread shortcut now AND's with `not _discord_thread_require_mention()` - gateway/config.py: bridge `discord.thread_require_mention` from config.yaml to `DISCORD_THREAD_REQUIRE_MENTION` env var (mirrors the existing `require_mention` bridge two lines above) - hermes_cli/config.py: add `thread_require_mention: False` default to DEFAULT_CONFIG['discord'] - tests/gateway/test_discord_free_response.py: 4 new tests covering default behaviour (in-thread shortcut still works), enabled behaviour (mention required in threads), enabled+mentioned (mention still passes through), and yaml-via-config.extra path. Also clears DISCORD_* env vars in the `adapter` fixture so process-env state from the contributor's shell doesn't leak into per-test behaviour. - tests/gateway/test_config.py: 2 new tests covering the yaml→env bridge (both the apply-from-yaml and env-precedence-over-yaml paths) - website/docs/user-guide/messaging/discord.md: document the new env var + config key with multi-bot rationale; cross-link from `auto_thread` section Tested on Ubuntu 24.04.
This commit is contained in:
parent
d557544560
commit
d863773c81
6 changed files with 169 additions and 3 deletions
|
|
@ -302,6 +302,43 @@ class TestLoadGatewayConfig:
|
|||
|
||||
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()
|
||||
|
|
|
|||
|
|
@ -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="fake-token")
|
||||
adapter = DiscordAdapter(config)
|
||||
adapter._client = SimpleNamespace(user=SimpleNamespace(id=999))
|
||||
|
|
@ -494,3 +508,73 @@ async def test_discord_voice_linked_parent_thread_still_requires_mention(adapter
|
|||
await adapter._handle_message(message)
|
||||
|
||||
adapter.handle_message.assert_not_awaited()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
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()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
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()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
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()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
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()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue