diff --git a/gateway/relay/adapter.py b/gateway/relay/adapter.py index 8b125a22adc..4fcdb1a2ac5 100644 --- a/gateway/relay/adapter.py +++ b/gateway/relay/adapter.py @@ -784,6 +784,24 @@ class RelayAdapter(BasePlatformAdapter): ) if effective_reply_to is None and reply_to is not None: send_metadata.pop("reply_to_message_id", None) + # QA-7: the connector's Slack sender THREADS ON METADATA ONLY — + # threadTs() reads metadata.thread_id/thread_ts and never looks at + # the frame's reply_to. A send whose only threading signal is + # reply_to (base.py's final-reply and fallback lanes build metadata + # from source.thread_id = None for a top-level DM) would post to the + # home channel even though _resolve_reply_to_for_send kept the + # anchor. Promote the surviving anchor into metadata.thread_id so + # the wire carries it where the connector actually reads it. Only + # when the mode gate kept the anchor (thread-per-message / real + # thread) — flat mode already nulled effective_reply_to above. + if ( + effective_reply_to is not None + and self._platform_by_chat.get(str(chat_id)) == Platform.SLACK.value + and not ( + send_metadata.get("thread_id") or send_metadata.get("thread_ts") + ) + ): + send_metadata["thread_id"] = str(effective_reply_to) result = await self._transport.send_outbound( { "op": "send", diff --git a/tests/gateway/relay/test_relay_slack_dm_streaming.py b/tests/gateway/relay/test_relay_slack_dm_streaming.py index 35b3d5f989c..2438609d89c 100644 --- a/tests/gateway/relay/test_relay_slack_dm_streaming.py +++ b/tests/gateway/relay/test_relay_slack_dm_streaming.py @@ -87,6 +87,10 @@ async def test_slack_dm_reply_keeps_anchor_in_thread_per_message_mode(): assert frame["reply_to"] == "1700.0001", ( "thread-per-message: the triggering ts anchors the final reply" ) + # QA-7: the connector's Slack sender threads on metadata.thread_id ONLY + # (threadTs() never reads the frame's reply_to), so the surviving anchor + # must be promoted into metadata for the send to actually thread. + assert (frame["metadata"] or {}).get("thread_id") == "1700.0001" @pytest.mark.asyncio