Skip to content

feat(discord): final response inline, tool progress in thread - #68409

Open
HangGlidersRule wants to merge 2 commits into
NousResearch:mainfrom
HangGlidersRule:feat/discord-progress-in-thread-response-in-channel
Open

feat(discord): final response inline, tool progress in thread#68409
HangGlidersRule wants to merge 2 commits into
NousResearch:mainfrom
HangGlidersRule:feat/discord-progress-in-thread-response-in-channel

Conversation

@HangGlidersRule

Copy link
Copy Markdown
Contributor

Problem

When DISCORD_AUTO_THREAD is enabled, the Discord adapter rewrites source.chat_id to the auto-created thread ID (line 7245: effective_channel = auto_threaded_channel or message.channel). This causes both tool-progress messages and the final text response to go into the thread.

The desired behaviour (matching Slack) is: tool progress goes into a thread, the final text response goes inline in the channel. The channel stays clean; tool noise is isolated.

Fix

When auto-thread creates a thread, keep source.chat_id as the parent channel ID so the final response is delivered inline. source.thread_id already points to the auto-created thread, so tool-progress messages route there via the existing _resolve_progress_thread_id() mechanism.

One-line logic change in plugins/platforms/discord/adapter.py:

if auto_threaded_channel is not None:
    source_chat_id = str(message.channel.id)  # parent channel
else:
    source_chat_id = str(effective_channel.id)  # message channel (unchanged)

No changes to gateway/run.py — progress routing already works correctly when source.thread_id is set.

Changes

  • plugins/platforms/discord/adapter.py: keep chat_id as parent channel when auto-threaded
  • tests/gateway/test_discord_autothread_chatid.py: 4 new tests

Test Plan

  • test_discord_autothread_chatid.py — 4/4 passed
  • test_discord_reply_mode.py — 29/29 passed (no regression)
  • test_discord_thread_persistence.py — 12/12 passed (no regression)
  • CI

When auto-thread fires, the Discord adapter was rewriting
source.chat_id to the auto-created thread ID, causing both tool
progress AND the final text response to go into the thread.

Fix: keep source.chat_id as the parent channel when auto-thread
creates a thread. source.thread_id already points to the thread,
so tool-progress messages route there via _resolve_progress_thread_id.
The final text response now lands inline in the parent channel.

This mirrors Slack's behaviour: tool progress in a thread,
final response flat in the channel.

Tests cover:
- Auto-thread: chat_id is parent channel, not thread
- No auto-thread: chat_id is message channel (unchanged)
- Progress thread routing: thread_id used when auto-thread on
- Progress thread routing: None when auto-thread off
- Existing reply_mode and thread_persistence tests pass (no regression)
Free-response channels were excluded from auto-threading by the
skip_thread check (is_free_channel was OR'd into skip_thread).
This meant tool progress appeared inline in free-response channels
instead of in a thread.

Fix: remove is_free_channel from skip_thread so free-response
channels also get auto-threaded. Channels explicitly listed in
DISCORD_NO_THREAD_CHANNELS still skip auto-threading.

Tests cover:
- Free-response channel not skipped from auto-thread
- Explicit no-thread channel still skipped
- Free channel also in no_thread list still skipped
- Existing reply_mode/thread_persistence/mattermost tests pass
@alt-glitch alt-glitch added type/feature New feature or request comp/plugins Plugin system and bundled plugins platform/discord Discord bot adapter P3 Low — cosmetic, nice to have labels Jul 21, 2026

@teknium1 teknium1 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for isolating the current auto-thread routing behavior. The premise is present on current main: plugins/platforms/discord/adapter.py:7577-7606 assigns the created thread as chat_id and retains it as thread_id.

Problems

  • Changing only chat_id cannot make final text inline. gateway/platforms/base.py:5778 builds final metadata from source.thread_id, and gateway/platforms/base.py:5918-5923 sends with it. Discord then gives metadata.thread_id precedence over chat_id (plugins/platforms/discord/adapter.py:2910-2928). The final response would still target the thread.
  • The skip_thread change reverses an intentional contract: free-response channels are documented as inline lightweight chat (website/docs/user-guide/messaging/discord.md:19) and covered by tests/gateway/test_discord_free_response.py:658-685; commit d557544560b0492be67b320f06033e9362c2cf09 added that behavior.
  • gateway/session.py:1114-1133 keys sessions using both chat_id and thread_id, so this source-shape change needs compatibility coverage for existing auto-thread sessions.

Suggested changes

  • Preserve the free-response skip.
  • Separate final-delivery metadata from progress metadata, including streaming, and add an end-to-end routing test rather than assignment-only tests.

Automated hermes-sweeper review.

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_keys & no_thread_channels) or is_free_channel
skip_thread = bool(channel_keys & no_thread_channels)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This reverses the documented free-response contract. website/docs/user-guide/messaging/discord.md:19 and tests/gateway/test_discord_free_response.py:658-685 require these channels to remain inline so every unmentioned message does not create a new thread; retain or is_free_channel here.

@@ -7265,7 +7275,7 @@ async def _handle_message(
# Build source

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changing chat_id alone does not redirect the final response inline: BasePlatformAdapter builds final metadata from source.thread_id and sends with it (gateway/platforms/base.py:5778-5923), while Discord gives that metadata precedence over chat_id (adapter.py:2910-2928). This needs a separate final-delivery metadata path.

@teknium1 teknium1 added sweeper:risk-session-state Sweeper risk: may lose/corrupt/mis-associate session or context state sweeper:risk-message-delivery Sweeper risk: may drop, duplicate, misroute, or suppress messages sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades sweeper:blast-moderate Sweeper blast radius: moderate — a subsystem or single platform labels Jul 30, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp/plugins Plugin system and bundled plugins P3 Low — cosmetic, nice to have platform/discord Discord bot adapter sweeper:blast-moderate Sweeper blast radius: moderate — a subsystem or single platform sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades sweeper:risk-message-delivery Sweeper risk: may drop, duplicate, misroute, or suppress messages sweeper:risk-session-state Sweeper risk: may lose/corrupt/mis-associate session or context state type/feature New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants