From a51a17ebe30960f79c7c1cddd2225e5215e27681 Mon Sep 17 00:00:00 2001 From: Victor Kyriazakos Date: Mon, 27 Jul 2026 15:56:10 +0000 Subject: [PATCH] fix(relay): promote the surviving reply_to anchor into metadata.thread_id on Slack sends (QA-7) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The connector's Slack sender threads on metadata ONLY: threadTs() reads metadata.thread_id/thread_ts and never the frame's reply_to. base.py's final-reply lane (and its stream-fallback 'first response' resend) builds metadata from source.thread_id — None for a top-level DM — so its sends carried reply_to as the sole threading signal and posted to the home channel (2026-07-27 post-approval report; the 15:17:03 frame showed meta_keys=['notify','user_id']). After the QA-6 mode gate keeps the anchor, copy it into metadata.thread_id so the wire carries the signal where the connector reads it. Flat mode unaffected (anchor already nulled); explicit thread metadata wins; non-Slack untouched. --- gateway/relay/adapter.py | 18 ++++++++++++++++++ .../relay/test_relay_slack_dm_streaming.py | 4 ++++ 2 files changed, 22 insertions(+) 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