fix(slack): respect reply_in_thread and reply_to_mode for channel replies - #8598
fix(slack): respect reply_in_thread and reply_to_mode for channel replies#8598dorukardahan wants to merge 4 commits into
Conversation
…lies When reply_in_thread is false or reply_to_mode is "off", top-level channel messages should receive direct channel replies instead of thread replies. Two runtime bugs prevented this: 1. _resolve_thread_ts returned the message's own ts for top-level messages because the gateway sets thread_id on all channel messages for session keying. The fix distinguishes top-level messages (where reply_to == thread_id, both equal the message ts) from genuine in-thread replies (where they differ). 2. send_typing called assistant_threads_setStatus on top-level messages, which activates a Slack assistant thread and forces subsequent replies into that thread — even when _resolve_thread_ts correctly returns None. The fix uses an emoji reaction (hourglass_flowing_sand) as a lightweight processing indicator instead, cleaned up after the response is sent. Also adds reply_to_mode=="off" as an alternative trigger (in addition to reply_in_thread=false) for consistency with the platform config schema. Fixes NousResearch#7532 (runtime logic — complementary to config-bridging PRs) Fixes NousResearch#8387 (prevention — complementary to cleanup PRs)
dorukardahan
left a comment
There was a problem hiding this comment.
Self-review notes
-
Why
reply_to == existing_threadworks: For top-level channel messages, the gateway setsthread_idto the message's owntsfor session keying (thread_ts = event.get("thread_ts") or tsat line 1001). When the bot replies,reply_tois also the message'sts. Soreply_to == existing_threadis true for top-level messages and false for genuine thread replies (whereexisting_threadis the parent andreply_tois the child). -
Why skip
setStatusinstead of just clearing it: The Slackassistant.threads.setStatusAPI activates an assistant thread on the target message. Even if cleared afterward, the thread is already created. Skipping entirely avoids this side effect. -
Emoji reaction vs setStatus: The hourglass reaction is a lightweight alternative that doesn't create assistant threads. It's cleaned up after the response — guarded by the same
reply_in_thread/reply_to_modecheck so it only runs when the reaction was actually added. -
Backward compatibility: When
reply_in_thread=true(default), the new condition at line 393 short-circuits and the entire default path (lines 399-407) is unchanged. Existing behavior preserved. -
Relationship to #7534/#7561: Those PRs fix config bridging (making sure
reply_in_threadreachesconfig.extra). This PR fixes the runtime logic that consumes it. They're complementary — both are needed for a complete fix.
Address Codex review finding: when reply_to is None but metadata has a real thread_id (internal sends like typing updates, TTS, media), the previous check `not reply_to` incorrectly returned None, causing these messages to go to the channel instead of the thread. Fix: use `reply_to == existing_thread` instead, which correctly handles all four cases: - Top-level: reply_to == thread_id (same ts) → None (channel) - Proactive: reply_to == thread_id (both None) → None (channel) - Internal send: reply_to=None, thread_id=parent → thread reply - Genuine thread: reply_to=child, thread_id=parent → thread reply Add test coverage for the internal send case.
…lies When reply_in_thread is false or reply_to_mode is "off", the bot replies in threads instead of directly in the channel. Root cause: the gateway sets thread_id on ALL channel messages for session keying, and the base class propagates it via _progress_metadata to every send call. _resolve_thread_ts cannot distinguish top-level from in-thread because reply_to is always None and thread_id is always set. Fix: set a per-message _force_channel_reply flag in _handle_slack_message where is_thread_reply context is available. _resolve_thread_ts and send_typing check this flag to suppress threading and assistant status for top-level messages while preserving in-thread delivery for genuine thread replies. Three changes in gateway/platforms/slack.py: 1. _handle_slack_message: set _force_channel_reply flag 2. _resolve_thread_ts: return None when flag is set 3. send_typing: skip setStatus when flag is set (prevents assistant_threads_setStatus from creating a thread) Fixes NousResearch#7532, Fixes NousResearch#8387
Address 3-model consensus review finding: _force_channel_reply as an instance variable creates a race condition when Bolt dispatches concurrent events as separate asyncio tasks. Replace with a contextvars.ContextVar which is isolated per-task. Also use defensive getattr for config.reply_to_mode to avoid AttributeError on older config objects. Add backward compatibility test for default config path.
CI Note68 test failures are all pre-existing upstream issues — none related to this PR's changes. Our 7 new tests ( Failing test modules (all unrelated to
These failures reproduce on upstream |
Summary
_resolve_thread_ts()returning thread_ts for top-level channel messages whenreply_in_thread=falseorreply_to_mode="off", causing unwanted thread repliessend_typing()callingassistant_threads_setStatuson top-level messages, which activates a Slack assistant thread and creates orphaned "Processing..." indicatorsProblem
When
reply_in_threadis set tofalsein the platform extra config (orreply_to_modeis"off"), the bot still replies in threads because:_resolve_thread_tsbug: The gateway setsthread_idon ALL channel messages for session keying (thread_ts = event.get("thread_ts") or ts). The existing guard returnedexisting_threadunconditionally, which was always non-None. The fix distinguishes top-level messages (wherereply_to == existing_thread) from genuine in-thread replies (where they differ).send_typingside effect:assistant_threads_setStatuscreates/activates a Slack assistant thread on the target message. Even when_resolve_thread_tscorrectly returnsNone, the assistant thread forces subsequent messages into a thread. The fix skipssetStatuswhen non-thread config is active and uses an emoji reaction instead.Changes
gateway/platforms/slack.py: 3 targeted changes (25 insertions, 3 deletions)tests/gateway/test_slack.py: 9 new tests forTestResolveThreadTsandTestSendTypingTest plan
TestSendTypingtests pass (backward compatibility)TestResolveThreadTstests verify top-level vs in-thread distinctionTestSendTypingtests verify reaction fallback whenreply_in_thread=falsereply_in_thread: false— reply goes to channel, not threadRelated