diff --git a/gateway/platforms/slack.py b/gateway/platforms/slack.py index 61cc7020a2..66c41a9475 100644 --- a/gateway/platforms/slack.py +++ b/gateway/platforms/slack.py @@ -450,8 +450,18 @@ class SlackAdapter(BasePlatformAdapter): """ # When reply_in_thread is disabled (default: True for backward compat), # only thread messages that are already part of an existing thread. + # For top-level channel messages, the inbound handler sets + # metadata.thread_id to the message's own ts as a session-keying + # fallback (see the `thread_ts = event.get("thread_ts") or ts` branch), + # so metadata alone can't distinguish a real thread reply from a + # top-level message. reply_to is the incoming message's own id, so + # when thread_id == reply_to the "thread" is synthetic and we reply + # directly in the channel instead. if not self.config.extra.get("reply_in_thread", True): - existing_thread = (metadata or {}).get("thread_id") or (metadata or {}).get("thread_ts") + md = metadata or {} + existing_thread = md.get("thread_id") or md.get("thread_ts") + if existing_thread and reply_to and existing_thread == reply_to: + existing_thread = None return existing_thread or None if metadata: diff --git a/tests/gateway/test_slack_mention.py b/tests/gateway/test_slack_mention.py index d127d7726e..8cfa9d98c8 100644 --- a/tests/gateway/test_slack_mention.py +++ b/tests/gateway/test_slack_mention.py @@ -334,7 +334,19 @@ def test_config_bridges_slack_reply_in_thread(monkeypatch, tmp_path): adapter = SlackAdapter(slack_config) assert adapter._resolve_thread_ts(reply_to="171.000", metadata={}) is None + + # Top-level channel messages arrive with metadata.thread_id == reply_to + # because the inbound handler uses event.ts as a session-keying fallback. + # Those must be treated as non-threaded so reply_in_thread=false takes + # effect in channels, not just DMs. assert adapter._resolve_thread_ts( reply_to="171.000", metadata={"thread_id": "171.000"}, + ) is None + + # Real thread replies (reply_to differs from thread parent) must still + # resolve to the parent thread so conversation context is preserved. + assert adapter._resolve_thread_ts( + reply_to="171.500", + metadata={"thread_id": "171.000"}, ) == "171.000"