From 8ce66a01ee50a3cae9540a388090a3b3bc64ed5e Mon Sep 17 00:00:00 2001 From: insecurejezza <70424851+insecurejezza@users.noreply.github.com> Date: Sat, 14 Mar 2026 23:43:27 +1100 Subject: [PATCH 1/4] fix(discord): retry without reply reference for system messages --- gateway/platforms/discord.py | 28 +++++++++-- tests/gateway/test_discord_send.py | 76 ++++++++++++++++++++++++++++++ 2 files changed, 100 insertions(+), 4 deletions(-) create mode 100644 tests/gateway/test_discord_send.py diff --git a/gateway/platforms/discord.py b/gateway/platforms/discord.py index 332d83f5..eaf457fc 100644 --- a/gateway/platforms/discord.py +++ b/gateway/platforms/discord.py @@ -605,10 +605,30 @@ class DiscordAdapter(BasePlatformAdapter): logger.debug("Could not fetch reply-to message: %s", e) for i, chunk in enumerate(chunks): - msg = await channel.send( - content=chunk, - reference=reference if i == 0 else None, - ) + chunk_reference = reference if i == 0 else None + try: + msg = await channel.send( + content=chunk, + reference=chunk_reference, + ) + except Exception as e: + err_text = str(e) + if ( + chunk_reference is not None + and "error code: 50035" in err_text + and "Cannot reply to a system message" in err_text + ): + logger.warning( + "[%s] Reply target %s is a Discord system message; retrying send without reply reference", + self.name, + reply_to, + ) + msg = await channel.send( + content=chunk, + reference=None, + ) + else: + raise message_ids.append(str(msg.id)) return SendResult( diff --git a/tests/gateway/test_discord_send.py b/tests/gateway/test_discord_send.py new file mode 100644 index 00000000..f8cb5dea --- /dev/null +++ b/tests/gateway/test_discord_send.py @@ -0,0 +1,76 @@ +from types import SimpleNamespace +from unittest.mock import AsyncMock, MagicMock +import sys + +import pytest + +from gateway.config import PlatformConfig + + +def _ensure_discord_mock(): + if "discord" in sys.modules and hasattr(sys.modules["discord"], "__file__"): + return + + discord_mod = MagicMock() + discord_mod.Intents.default.return_value = MagicMock() + discord_mod.Client = MagicMock + discord_mod.File = MagicMock + discord_mod.DMChannel = type("DMChannel", (), {}) + discord_mod.Thread = type("Thread", (), {}) + discord_mod.ForumChannel = type("ForumChannel", (), {}) + discord_mod.ui = SimpleNamespace(View=object, button=lambda *a, **k: (lambda fn: fn), Button=object) + discord_mod.ButtonStyle = SimpleNamespace(success=1, primary=2, danger=3, green=1, blurple=2, red=3) + discord_mod.Color = SimpleNamespace(orange=lambda: 1, green=lambda: 2, blue=lambda: 3, red=lambda: 4) + discord_mod.Interaction = object + discord_mod.Embed = MagicMock + discord_mod.app_commands = SimpleNamespace(describe=lambda **kwargs: (lambda fn: fn)) + + ext_mod = MagicMock() + commands_mod = MagicMock() + commands_mod.Bot = MagicMock + ext_mod.commands = commands_mod + + sys.modules.setdefault("discord", discord_mod) + sys.modules.setdefault("discord.ext", ext_mod) + sys.modules.setdefault("discord.ext.commands", commands_mod) + + +_ensure_discord_mock() + +from gateway.platforms.discord import DiscordAdapter # noqa: E402 + + +@pytest.mark.asyncio +async def test_send_retries_without_reference_when_reply_target_is_system_message(): + adapter = DiscordAdapter(PlatformConfig(enabled=True, token="***")) + + ref_msg = SimpleNamespace(id=99) + sent_msg = SimpleNamespace(id=1234) + send_calls = [] + + async def fake_send(*, content, reference=None): + send_calls.append({"content": content, "reference": reference}) + if len(send_calls) == 1: + raise RuntimeError( + "400 Bad Request (error code: 50035): Invalid Form Body\n" + "In message_reference: Cannot reply to a system message" + ) + return sent_msg + + channel = SimpleNamespace( + fetch_message=AsyncMock(return_value=ref_msg), + send=AsyncMock(side_effect=fake_send), + ) + adapter._client = SimpleNamespace( + get_channel=lambda _chat_id: channel, + fetch_channel=AsyncMock(), + ) + + result = await adapter.send("555", "hello", reply_to="99") + + assert result.success is True + assert result.message_id == "1234" + assert channel.fetch_message.await_count == 1 + assert channel.send.await_count == 2 + assert send_calls[0]["reference"] is ref_msg + assert send_calls[1]["reference"] is None From a05a4afa5369e873e08d299c6c6cab62b99b7bff Mon Sep 17 00:00:00 2001 From: teknium1 Date: Sat, 14 Mar 2026 21:44:50 -0700 Subject: [PATCH 2/4] fix: align salvaged Discord send test mock with current slash-command API --- tests/gateway/test_discord_send.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/tests/gateway/test_discord_send.py b/tests/gateway/test_discord_send.py index f8cb5dea..de253146 100644 --- a/tests/gateway/test_discord_send.py +++ b/tests/gateway/test_discord_send.py @@ -23,7 +23,11 @@ def _ensure_discord_mock(): discord_mod.Color = SimpleNamespace(orange=lambda: 1, green=lambda: 2, blue=lambda: 3, red=lambda: 4) discord_mod.Interaction = object discord_mod.Embed = MagicMock - discord_mod.app_commands = SimpleNamespace(describe=lambda **kwargs: (lambda fn: fn)) + discord_mod.app_commands = SimpleNamespace( + describe=lambda **kwargs: (lambda fn: fn), + choices=lambda **kwargs: (lambda fn: fn), + Choice=lambda **kwargs: SimpleNamespace(**kwargs), + ) ext_mod = MagicMock() commands_mod = MagicMock() From fd687d09678c3b1b11eddf3fd011f8bc57feaf05 Mon Sep 17 00:00:00 2001 From: Joshua Martinez <29747003+joshkmartinez@users.noreply.github.com> Date: Fri, 13 Mar 2026 20:43:22 -0700 Subject: [PATCH 3/4] fix slack docs reference --- hermes_cli/setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hermes_cli/setup.py b/hermes_cli/setup.py index ef5f0969..935cb5da 100644 --- a/hermes_cli/setup.py +++ b/hermes_cli/setup.py @@ -2155,7 +2155,7 @@ def setup_gateway(config: dict): ) print() print_info( - " Full guide: https://hermes-agent.ai/docs/user-guide/messaging/slack" + " Full guide: https://hermes-agent.nousresearch.com/docs/user-guide/messaging/slack/" ) print() bot_token = prompt("Slack Bot Token (xoxb-...)", password=True) From 2119b6879968e10e9d78aff59f31c00dcfaae5af Mon Sep 17 00:00:00 2001 From: teknium1 Date: Sat, 14 Mar 2026 21:49:04 -0700 Subject: [PATCH 4/4] fix: clarify Slack setup guidance - mark private-channel scopes/events as optional - note reinstall requirement after scope/event changes - correct Slack allowlist messaging to match gateway behavior --- hermes_cli/setup.py | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/hermes_cli/setup.py b/hermes_cli/setup.py index 935cb5da..fca2b862 100644 --- a/hermes_cli/setup.py +++ b/hermes_cli/setup.py @@ -2142,16 +2142,18 @@ def setup_gateway(config: dict): print_info(" • Create an App-Level Token with 'connections:write' scope") print_info(" 3. Add Bot Token Scopes: Features → OAuth & Permissions") print_info(" Required scopes: chat:write, app_mentions:read,") - print_info(" channels:history, channels:read, groups:history,") - print_info(" im:history, im:read, im:write, users:read, files:write") + print_info(" channels:history, channels:read, im:history,") + print_info(" im:read, im:write, users:read, files:write") + print_info(" Optional for private channels: groups:history") print_info(" 4. Subscribe to Events: Features → Event Subscriptions → Enable") - print_info(" Required events: message.im, message.channels,") - print_info(" message.groups, app_mention") - print_warning(" ⚠ Without message.channels/message.groups events,") - print_warning(" the bot will ONLY work in DMs, not channels!") + print_info(" Required events: message.im, message.channels, app_mention") + print_info(" Optional for private channels: message.groups") + print_warning(" ⚠ Without message.channels the bot will ONLY work in DMs,") + print_warning(" not public channels.") print_info(" 5. Install to Workspace: Settings → Install App") + print_info(" 6. Reinstall the app after any scope or event changes") print_info( - " 6. After installing, invite the bot to channels: /invite @YourBot" + " 7. After installing, invite the bot to channels: /invite @YourBot" ) print() print_info( @@ -2173,14 +2175,17 @@ def setup_gateway(config: dict): ) print() allowed_users = prompt( - "Allowed user IDs (comma-separated, leave empty for open access)" + "Allowed user IDs (comma-separated, leave empty to deny everyone except paired users)" ) if allowed_users: save_env_value("SLACK_ALLOWED_USERS", allowed_users.replace(" ", "")) print_success("Slack allowlist configured") else: + print_warning( + "⚠️ No Slack allowlist set - unpaired users will be denied by default." + ) print_info( - "⚠️ No allowlist set - anyone in your workspace can use the bot!" + " Set SLACK_ALLOW_ALL_USERS=true or GATEWAY_ALLOW_ALL_USERS=true only if you intentionally want open workspace access." ) # ── WhatsApp ──