feat(discord): add free_response_auto_thread opt-in - #18455
Conversation
Free-response channels skip auto-threading by default so the bot replies inline (lightweight chat mode). This prevented users who wanted BOTH mention-free replies AND per-conversation threads from getting either. Add a new opt-in `discord.free_response_auto_thread` (env: `DISCORD_FREE_RESPONSE_AUTO_THREAD`, default false) that, when true, re-enables auto-threading in free-response channels. Voice-linked channels continue to skip auto-thread regardless, and the flag is gated behind the global `DISCORD_AUTO_THREAD=true`. Default behavior is unchanged; all 291 existing discord tests pass.
|
Competes with #17564 which implements the same feature (threaded free-response channels) via a different config surface (thread_free_response_channels list vs boolean flag). |
|
Competes with #17564 |
|
+1 — this exactly covers my use case: a private Discord server where the bot responds without mentions but I'd still like per-conversation thread isolation to avoid context pollution and long-context token burn. I arrived at the same design (#19131) and am closing it in favor of this more complete implementation (tests + docs + config defaults). |
Resolves conflict in gateway/platforms/discord.py. Upstream commit ad4542b ('fix(gateway): allow free_response_channels to override DISCORD_IGNORE_NO_MENTION') removed the unconditional 'or is_free_channel' clause from skip_thread, which would have made free-response channels auto-thread by default. However, upstream also kept test_discord_free_channel_skips_auto_thread which still asserts free channels skip auto-thread by default — so upstream main's tests were inconsistent with its own code. This PR's design (skip_thread for free channels by default + opt-in flag DISCORD_FREE_RESPONSE_AUTO_THREAD to enable threading) is preserved, which also satisfies the upstream test that was failing on main alone.
|
Closing as no longer needed after re-checking current main. Current main already auto-threads free-response channels by default: the Discord handler now computes The behavior change came from Since this PR would reintroduce default inline behavior unless |
|
Reopening this after looking at the follow-up history around #25311, #25444, and #26058. My earlier close comment was based on one snapshot of Timeline / context:
Given that current baseline, this PR is useful again as a compatibility-preserving middle ground. It does not need to undo #25444's inline default. Instead, it can keep the documented/current behavior for normal
So the framing should be: this PR is not primarily about reverting #25444. It is about resolving the tension between #25444/#25310 (inline free-response channels as documented default) and #26058 (mention-free + threaded workflow) by making threaded free-response behavior explicit and opt-in. It probably needs a rebase/refresh against current
Short version: my earlier close rationale was incomplete after #25444/#26058. Reopening because this PR can provide the missing explicit opt-in for threaded free-response channels while preserving the current inline default. |
Resolve Discord free-response auto-thread conflicts by preserving current main's inline default from NousResearch#25444 while keeping this PR's explicit DISCORD_FREE_RESPONSE_AUTO_THREAD opt-in for threaded free-response channels. Keep upstream thread_require_mention/history_backfill defaults and tests, add no_thread_channels precedence coverage, and preserve docs/config env bridging for the opt-in flag.
teknium1
left a comment
There was a problem hiding this comment.
Thanks for preserving the inline default while adding a concrete opt-in workflow. The requested capability is still absent on current main: plugins/platforms/discord/adapter.py:6223 unconditionally treats free-response channels as skip_thread, matching the current docs at website/docs/user-guide/messaging/discord.md:377.
Problems
- The production hunk targets
gateway/platforms/discord.py, but cc8e5ec moved the live adapter toplugins/platforms/discord/adapter.py; current thread routing is at lines 6220-6227. - The config hunk targets the legacy Discord bridge in
gateway/config.py. Discord YAML bridging now belongs in the plugin hook atplugins/platforms/discord/adapter.py:8283-8317. - After adding this setting, clear
DISCORD_FREE_RESPONSE_AUTO_THREADin the fixture attests/gateway/test_discord_free_response.py:108-120; the existing inline-default regression at line 658 otherwise depends on the caller environment.
Suggested changes
- Salvage the handler and YAML bridge into the plugin locations above, and add a config.yaml bridge test in addition to the routing tests.
Automated hermes-sweeper review.
| @@ -4476,7 +4476,18 @@ async def _handle_message(self, message: DiscordMessage) -> None: | |||
| if not is_thread and not isinstance(message.channel, discord.DMChannel): | |||
There was a problem hiding this comment.
Blocking: cc8e5ec removed this legacy adapter path. Current main handles this route in plugins/platforms/discord/adapter.py:6220-6227; port this logic there so the feature reaches the live Discord adapter.
| @@ -921,6 +921,8 @@ def load_gateway_config() -> GatewayConfig: | |||
| os.environ["DISCORD_FREE_RESPONSE_CHANNELS"] = str(frc) | |||
There was a problem hiding this comment.
Blocking: Discord-specific YAML bridging moved into the plugin in cc8e5ec. Add this setting to _apply_yaml_config at plugins/platforms/discord/adapter.py:8283-8317; this legacy block is no longer the Discord config path.
What does this PR do?
Adds an opt-in
discord.free_response_auto_threadflag (env:DISCORD_FREE_RESPONSE_AUTO_THREAD, defaultfalse) that re-enables auto-threading in free-response channels. Default behavior is unchanged — users who never set the flag get the current inline-reply behavior, bit-for-bit.Previously, free-response channels (channels listed in
DISCORD_FREE_RESPONSE_CHANNELS) unconditionally skipped auto-threading — see #11629, which made this an explicit invariant: "free_response_channels ALWAYS skip auto-thread". This is the right default for lightweight chat channels, but it removes a legitimate configuration that a number of users were quietly relying on: @mention-free replies with per-conversation threads.Related Issue
Fixes #15262.
That issue documents the exact regression this PR addresses: users had configured
free_response_channels(often as*) specifically to get mention-free behavior, and were (accidentally, in the pre-#11629 world) also getting auto-threads. Oncefree_response_channelsstarted being honored correctly, the threads disappeared and their channels became a single undifferentiated firehose of inline replies. The issue proposes exactly this fix — a separate toggle so users can opt back into threading without re-introducing the@mentionrequirement.Type of Change
Use Case / Why
Community bot operators commonly want a middle-ground channel mode:
require_mention: true+auto_thread: true(default) — every@mentionspawns a thread. Organized, but requires pinging the bot for every message.free_response_channels(current behavior after fix(discord): auto-thread cluster — free-response + reply + fallback + mention-name-strip #11629) — no @mention required, no threads. Lightweight chat, but no conversation isolation — everyone's replies interleave in one channel.*wildcard bug).This is a small, surgical addition — one flag, default off, preserves the existing invariant for every user who doesn't explicitly opt in.
Solution
Narrow change in
gateway/platforms/discord.pyaround theskip_threadcomputation. The previous line:becomes:
Three invariants preserved:
DISCORD_FREE_RESPONSE_AUTO_THREADis unset (orfalse),free_skips_threadreduces tois_free_channel, matching the current behavior exactly.and not is_voice_linked_channelclause keeps voice channels on their existing interaction model regardless of the new flag.auto_threadtoggle. Because this only affects whether we clearskip_thread, the subsequentif auto_thread and not skip_thread ...check still short-circuits ifDISCORD_AUTO_THREAD=false. Settingfree_response_auto_thread=truewhileauto_thread=falseis a no-op, as documented.The config plumbing mirrors every other
discord.*flag —gateway/config.pymirrorsconfig.yaml→ env var,hermes_cli/config.pyadds the default. Docs updated inwebsite/docs/user-guide/messaging/discord.mdandwebsite/docs/reference/environment-variables.md.Changes Made
gateway/platforms/discord.py— computeskip_threadbased on the new opt-in (default false preserves current behavior; voice-linked channels always skip).gateway/config.py— mirrordiscord.free_response_auto_threadfromconfig.yamltoDISCORD_FREE_RESPONSE_AUTO_THREADwhen the env var is not already set (same pattern asauto_thread,reactions, etc.).hermes_cli/config.py— addfree_response_auto_thread: FalsetoDEFAULT_CONFIG["discord"]sohermes config check/ migrations surface it.tests/gateway/test_discord_free_response.py— 4 new tests covering: opt-in enables auto-thread, default disabled, voice-linked channels ignore the flag, and globalDISCORD_AUTO_THREAD=falsestill wins.website/docs/user-guide/messaging/discord.md— document the flag in both the env-var table and theconfig.yamlexample, with an explanatory paragraph underfree_response_channels.website/docs/reference/environment-variables.md— one-row entry in the Discord section.How to Test
~/.hermes/config.yaml:free_response_auto_thread(or set tofalse) → bot replies inline in the channel, no thread (default behavior preserved).free_response_auto_thread: truebutauto_thread: false→ bot replies inline (global toggle still wins, as documented).Automated test run
All 20 pre-existing free-response tests pass, plus the 4 new ones.
Checklist
Code
feat(discord):)test_discord_free_response.py)Documentation & Housekeeping
website/docs/user-guide/messaging/discord.md,website/docs/reference/environment-variables.md)DEFAULT_CONFIGinhermes_cli/config.pyso the new key is discoverable viahermes config checkCONTRIBUTING.mdorAGENTS.md— N/A (no architecture change)Prior Art Search
I searched existing issues and PRs before starting; the following are related but do not overlap:
free_response_channels_create_thread/ equivalent toggle.auto_threadfire only on explicit@mentionin all channels. Different knob, different intent. If #8597 lands first, this PR still applies cleanly (different lines, different condition).auto_thread: smartdesign. Superseded by #11629. This PR deliberately avoids adding new modes toauto_thread— it's a plain boolean side-flag.no_thread_channels).No open or merged PR currently introduces
DISCORD_FREE_RESPONSE_AUTO_THREADor an equivalent per-free-channel toggle.Design Notes
auto_threadmode ("smart","mentions-only", etc.)? Keepsauto_threada clean boolean, matches the surrounding flag style (reactions,require_mention, etc.), and composes cleanly with future work like feat(gateway): auto_thread only creates threads on @mention #8597 (which refines what triggersauto_threadglobally) without either side needing to know about the other.DISCORD_AUTO_THREAD? The new flag is a refinement of existing auto-thread behavior, not an override. If someone disables auto-thread globally, they shouldn't get threads anywhere — including free-response channels. This matches user intent and avoids a surprising interaction.