From 9864e00fb42a4c7f49b2767dd58a8a0a1f832865 Mon Sep 17 00:00:00 2001 From: Victor Kyriazakos Date: Mon, 27 Jul 2026 22:59:02 +0000 Subject: [PATCH] =?UTF-8?q?feat(relay):=20flat-DM=20liveliness=20=E2=80=94?= =?UTF-8?q?=20status=20anchors=20to=20the=20triggering=20ts,=20replies=20s?= =?UTF-8?q?tay=20flat=20(QA-8)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Victor's correction: flat DMs CAN have a live thinking status. setStatus on the triggering message's ts renders '… thinking'/per-tool phrases in that message's thread-footer space and clears without leaving a message artifact. Native suppresses this because ITS reply routing could inherit the activated thread; the relay lane's flat-mode sends strip their anchors explicitly (QA-6/7), so the status anchor cannot leak into reply placement — proven by the new leak-guard test. send_typing/stop_typing now anchor the status in flat mode too, gated by platforms.relay.extra.slack.flat_dm_status (default ON; false restores the fully anchorless posture). Thread mode unchanged. --- gateway/relay/adapter.py | 36 ++++++++++++++++--- .../relay/test_relay_slack_prompt_dm_root.py | 31 ++++++++++++++-- 2 files changed, 61 insertions(+), 6 deletions(-) diff --git a/gateway/relay/adapter.py b/gateway/relay/adapter.py index c7eaa1a981c..4651ee1c34e 100644 --- a/gateway/relay/adapter.py +++ b/gateway/relay/adapter.py @@ -311,6 +311,24 @@ class RelayAdapter(BasePlatformAdapter): except Exception: # noqa: BLE001 - config shape is operator-owned return True + def _flat_dm_status_enabled(self) -> bool: + """Liveliness in flat-DM mode: anchor the STATUS (not the reply) to the + triggering message's ts. + + ``assistant.threads.setStatus`` on a message ts renders "… thinking" in + that message's thread-footer space and vanishes on clear — no message + artifact. Native suppresses this in flat mode because ITS response + routing could inherit the activated thread; the relay lane's sends are + explicitly flat in flat mode (QA-6/7 anchor strip), so the status + anchor cannot leak into reply placement here. Default ON — flat DMs + get a live status billboard while replies still post at the DM root. + Opt out: platforms.relay.extra.slack.flat_dm_status: false. + """ + try: + return bool(self._relay_slack_extra().get("flat_dm_status", True)) + except Exception: # noqa: BLE001 - config shape is operator-owned + return True + def _stamp_slack_session_thread(self, event) -> None: """Native session-keying parity for fronted Slack (QA-3). @@ -1023,9 +1041,17 @@ class RelayAdapter(BasePlatformAdapter): and self._platform_by_chat.get(str(chat_id)) == Platform.SLACK.value and self._chat_type_by_chat.get(str(chat_id)) == "dm" ): - reply_in_thread = self._effective_reply_in_thread() anchor = self._last_inbound_ts_by_chat.get(str(chat_id)) - if reply_in_thread and anchor: + # Thread mode: status targets the per-message thread (QA-1). + # Flat mode: the status can STILL anchor to the triggering ts — + # setStatus renders in the footer space and clears without a + # message artifact, and flat sends strip their anchors (QA-6/7) + # so reply placement cannot inherit it. Gated separately + # (flat_dm_status, default on) for a clean opt-out. + if anchor and ( + self._effective_reply_in_thread() + or self._flat_dm_status_enabled() + ): md["thread_id"] = anchor # Rich status parity (QA-1): run.py's live-status lane stashes the # current per-tool phrase via set_status_text() (base class store). @@ -1084,9 +1110,11 @@ class RelayAdapter(BasePlatformAdapter): not (md.get("thread_id") or md.get("thread_ts")) and self._chat_type_by_chat.get(str(chat_id)) == "dm" ): - reply_in_thread = self._effective_reply_in_thread() anchor = self._last_inbound_ts_by_chat.get(str(chat_id)) - if reply_in_thread and anchor: + if anchor and ( + self._effective_reply_in_thread() + or self._flat_dm_status_enabled() + ): md["thread_id"] = anchor try: await self._transport.send_outbound( diff --git a/tests/gateway/relay/test_relay_slack_prompt_dm_root.py b/tests/gateway/relay/test_relay_slack_prompt_dm_root.py index 098b1fcadde..d1c062075b3 100644 --- a/tests/gateway/relay/test_relay_slack_prompt_dm_root.py +++ b/tests/gateway/relay/test_relay_slack_prompt_dm_root.py @@ -277,15 +277,42 @@ async def test_typing_synthesizes_thread_anchor_in_thread_mode(): @pytest.mark.asyncio -async def test_typing_keeps_no_anchor_in_flat_mode(): - """Flat mode: no synthetic thread for the status either (#18859).""" +async def test_typing_flat_mode_status_anchors_to_trigger_ts_by_default(): + """Flat-DM liveliness: the STATUS still anchors to the triggering ts + (renders in the footer space, no message artifact) while replies stay + flat — QA-6/7 strip send anchors, so placement cannot inherit this.""" adapter, stub = _wire_with_ts("D1", "dm", "1700.0042") adapter.config.extra = {"reply_in_thread": False} await adapter.send_typing("D1", metadata=None) typing = [f for f in stub.sent if f["op"] == "typing"] + assert typing and typing[-1]["metadata"].get("thread_id") == "1700.0042" + + +@pytest.mark.asyncio +async def test_typing_flat_mode_opt_out_drops_anchor(): + """flat_dm_status: false restores the fully-anchorless flat posture.""" + adapter, stub = _wire_with_ts("D1", "dm", "1700.0042") + adapter.config.extra = { + "slack": {"reply_in_thread": False, "flat_dm_status": False} + } + await adapter.send_typing("D1", metadata=None) + typing = [f for f in stub.sent if f["op"] == "typing"] assert typing and "thread_id" not in typing[-1]["metadata"] +@pytest.mark.asyncio +async def test_flat_mode_sends_stay_flat_with_status_anchor_active(): + """The liveliness anchor must NOT leak into reply placement: sends in + flat mode still strip the synthetic anchor (QA-6/7 contract).""" + adapter, stub = _wire_with_ts("D1", "dm", "1700.0042") + adapter.config.extra = {"reply_in_thread": False} + await adapter.send_typing("D1", metadata=None) + await adapter.send("D1", "the answer", reply_to="1700.0042") + frame = [f for f in stub.sent if f["op"] == "send"][-1] + assert frame["reply_to"] is None + assert "thread_id" not in (frame["metadata"] or {}) + + @pytest.mark.asyncio async def test_typing_honours_real_thread_anchor(): """Metadata that already names a thread wins over the synthetic cache."""