feat(relay): flat-DM liveliness — status anchors to the triggering ts, replies stay flat (QA-8)

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.
This commit is contained in:
Victor Kyriazakos 2026-07-27 22:59:02 +00:00
parent 71d5c47e21
commit 9864e00fb4
2 changed files with 61 additions and 6 deletions

View file

@ -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(

View file

@ -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."""