From d557544560b0492be67b320f06033e9362c2cf09 Mon Sep 17 00:00:00 2001 From: simpolism Date: Sun, 10 May 2026 01:37:56 -0400 Subject: [PATCH] fix(discord): keep free-response channels inline MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Free-response channels are intended as lightweight chat surfaces — the bot responds to every message without requiring an @mention. But the auto-thread gate only checked DISCORD_NO_THREAD_CHANNELS, not DISCORD_FREE_RESPONSE_CHANNELS, so every message in a free-response channel still spawned a brand-new thread. That turns a chat channel into a thread-spawning machine: 1 thread per message. The user-facing docs at website/docs/user-guide/messaging/discord.md already describe the intended behavior ("Free-response channels also skip auto-threading — the bot replies inline rather than spinning off a new thread per message"), so this is a code-vs-docs gap, not a design change. Fix: OR is_free_channel into skip_thread alongside the existing no_thread_channels check. One-line production change. Regression test added at tests/gateway/test_discord_free_response.py: test_discord_free_response_channel_skips_auto_thread asserts that a message in a free-response channel never calls _auto_create_thread. Reverting the one-line fix causes the test to fail with 'Expected mock to not have been awaited. Awaited 1 times.' — i.e. the test demonstrates the bug concretely. --- gateway/platforms/discord.py | 2 +- tests/gateway/test_discord_free_response.py | 31 +++++++++++++++++++++ 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/gateway/platforms/discord.py b/gateway/platforms/discord.py index 1817ece173d..e770d5558da 100644 --- a/gateway/platforms/discord.py +++ b/gateway/platforms/discord.py @@ -4223,7 +4223,7 @@ class DiscordAdapter(BasePlatformAdapter): if not is_thread and not isinstance(message.channel, discord.DMChannel): no_thread_channels_raw = os.getenv("DISCORD_NO_THREAD_CHANNELS", "") no_thread_channels = {ch.strip() for ch in no_thread_channels_raw.split(",") if ch.strip()} - skip_thread = bool(channel_ids & no_thread_channels) + skip_thread = bool(channel_ids & no_thread_channels) or is_free_channel auto_thread = os.getenv("DISCORD_AUTO_THREAD", "true").lower() in {"true", "1", "yes"} is_reply_message = getattr(message, "type", None) == discord.MessageType.reply if auto_thread and not skip_thread and not is_voice_linked_channel and not is_reply_message: diff --git a/tests/gateway/test_discord_free_response.py b/tests/gateway/test_discord_free_response.py index 91b23bd8602..7fa388dc4ae 100644 --- a/tests/gateway/test_discord_free_response.py +++ b/tests/gateway/test_discord_free_response.py @@ -446,6 +446,37 @@ async def test_discord_voice_linked_channel_skips_mention_requirement_and_auto_t assert event.source.chat_type == "group" +@pytest.mark.asyncio +async def test_discord_free_response_channel_skips_auto_thread(adapter, monkeypatch): + """Free-response channels should reply inline, never spawn a new thread. + + Without this, every message in a free-response channel would auto-create + a fresh thread (since the channel bypasses the @mention gate, every + message looks like a fresh trigger). That turns a "lightweight chat" + channel into a thread-spawning machine — see the docs at + website/docs/user-guide/messaging/discord.md which already describe + this as the intended behavior. + """ + monkeypatch.setenv("DISCORD_REQUIRE_MENTION", "true") + monkeypatch.setenv("DISCORD_FREE_RESPONSE_CHANNELS", "789") + monkeypatch.delenv("DISCORD_AUTO_THREAD", raising=False) # default true + + adapter._auto_create_thread = AsyncMock() + + message = make_message( + channel=FakeTextChannel(channel_id=789), + content="casual chat in free-response channel", + ) + + await adapter._handle_message(message) + + adapter._auto_create_thread.assert_not_awaited() + adapter.handle_message.assert_awaited_once() + event = adapter.handle_message.await_args.args[0] + assert event.text == "casual chat in free-response channel" + assert event.source.chat_type == "group" + + @pytest.mark.asyncio