refactor(relay): remove dead _strip_synthetic_dm_thread; pin the run.py anchor-suppression boundary

Review finding (2026-07-28): every path through _strip_synthetic_dm_thread
returned metadata unmodified — the actual strip was removed when prompts
switched to trusting the run.py thread stamp, leaving a 50-line no-op and
four tests that passed against it (verified by reviewer's negative control).

- delete the function + its _send_prompt call site (verbatim pass-through
  with a pointer comment to the single mode authority)
- rewrite the three pass-through tests as end-to-end placement contracts
  (forward run.py's stamp untouched)
- NEW boundary tests pinning run.py._resolve_progress_thread_id itself:
  flat mode suppresses the synthetic self-anchor / preserves real threads;
  thread mode keeps the first-turn self-anchor. This is the cross-module
  coupling the review flagged as unpinned — if the upstream suppression
  regresses, these fail instead of prompts silently threading.
This commit is contained in:
Victor Kyriazakos 2026-07-28 11:33:01 +00:00
parent a09015d31e
commit 09c4a1d349
2 changed files with 61 additions and 65 deletions

View file

@ -1448,61 +1448,6 @@ class RelayAdapter(BasePlatformAdapter):
return None
return state
def _strip_synthetic_dm_thread(
self, chat_id: str, metadata: Optional[Dict[str, Any]]
) -> Optional[Dict[str, Any]]:
"""Drop the synthetic DM thread anchor from an interactive prompt's metadata.
A clarify/approval/confirm prompt is emitted mid-turn in reply to the
triggering inbound event, so ``metadata`` carries that event's thread
context run.py's ``_thread_metadata_for_source`` stamps
``metadata["thread_id"]`` (and, for Slack, ``metadata["message_id"]`` =
the triggering message ts). For a Slack DM with no REAL thread, that
``thread_id`` is the message's own synthetic self-anchor (a session-keying
fallback), and forwarding it makes the connector's slackRestSender thread
the prompt card UNDER the user's message instead of posting it flat at the
DM root the reported bug ("approval block was put in a thread").
Native Slack Hermes already suppresses this synthetic DM thread anchor
(``SlackAdapter._resolve_thread_ts`` returns ``None`` for a top-level / DM
message). We reproduce it here with the same discipline used on the
streaming path (``_resolve_reply_to_for_send``):
Slack DM + thread_id is the synthetic self-anchor strip thread_id.
A REAL thread (``thread_id`` distinct from the triggering message ts) is
left untouched so a prompt raised inside a thread stays in that thread;
non-DM / non-Slack chats are never matched. Only the threading keys are
removed tenant scope (``scope_id`` / ``slack_team_id``) and everything
else survive so egress routing is unaffected.
"""
if not metadata:
return metadata
if self._platform_by_chat.get(str(chat_id)) != Platform.SLACK.value:
return metadata
if self._chat_type_by_chat.get(str(chat_id)) != "dm":
return metadata
thread_id = metadata.get("thread_id")
if not thread_id:
return metadata
# Trust the run.py stamp (QA-5). The threading MODE is decided in ONE
# place — run.py's _resolve_progress_thread_id, which reads
# platforms.slack.extra.reply_in_thread:
# * flat mode (reply_in_thread=false): the synthetic self-anchor is
# suppressed THERE, so prompt metadata arrives with NO thread_id and
# this helper is a no-op — the card posts flat at the DM root;
# * thread-per-message mode (default): metadata.thread_id is stamped
# for the whole turn, and on the FIRST turn it legitimately equals
# the triggering message's ts (the synthetic root IS the thread).
# The previous unconditional thread_id == message_id strip re-derived
# the mode here and got it wrong for thread-per-message: the approval
# card (and its resolved-state swap) was exiled to the DM root while
# progress bubbles honoured the thread (2026-07-27 mixed-placement
# screenshot). Mirror native SlackAdapter._resolve_thread_ts, which
# only performs the self-anchor strip when reply_in_thread=false — a
# state this lane never sees with an anchor present, per the above.
return metadata
async def _send_prompt(
self,
chat_id: str,
@ -1533,7 +1478,11 @@ class RelayAdapter(BasePlatformAdapter):
# of posting it flat at the DM root (the reported bug). Native Slack
# Hermes suppresses this synthetic DM thread anchor; drop it here for the
# same Slack-DM-with-no-real-thread case, matching _resolve_reply_to_for_send.
prompt_metadata = self._strip_synthetic_dm_thread(chat_id, metadata)
# Prompt metadata is forwarded VERBATIM. The threading mode is decided
# in exactly one place — run.py's _resolve_progress_thread_id (flat mode
# suppresses the synthetic self-anchor there; thread mode stamps the
# turn's thread). Boundary pinned by test_run_py_suppresses_self_anchor*.
prompt_metadata = metadata
action: Dict[str, Any] = {
"op": "prompt",
"chat_id": chat_id,

View file

@ -110,14 +110,16 @@ async def test_exec_approval_flat_mode_posts_at_dm_root():
# ---------------------------------------------------------------------------
# Thread-per-message mode: the first-turn self-anchor (thread_id == message_id)
# IS the thread root — the prompt must stay in the thread (QA-5 regression).
# Thread-per-message mode, end-to-end placement contract: run.py stamps the
# turn's thread (first turn: the triggering message's own ts) and the adapter
# forwards prompt metadata UNTOUCHED — no re-derivation, no strip. Mixed
# placement (progress threaded, card at root) was the 2026-07-27 regression.
# ---------------------------------------------------------------------------
@pytest.mark.asyncio
async def test_exec_approval_first_turn_self_anchor_stays_in_thread():
"""Thread-per-message first turn: run.py stamps thread_id = the triggering
message's own ts. The approval card must post INTO that thread — stripping
it exiled the card to the home channel (2026-07-27 report)."""
async def test_exec_approval_forwards_run_py_thread_stamp_untouched():
"""The adapter must forward run.py's thread stamp verbatim: the approval
card posts INTO the stamped thread. Any adapter-side re-derivation or
strip exiled the card to the home channel (2026-07-27 report)."""
adapter, stub = _wire("D1", "dm", scope_id="T1")
md = {
"thread_id": "1700000000.000100",
@ -137,7 +139,7 @@ async def test_exec_approval_first_turn_self_anchor_stays_in_thread():
@pytest.mark.asyncio
async def test_clarify_first_turn_self_anchor_stays_in_thread():
async def test_clarify_forwards_run_py_thread_stamp_untouched():
adapter, stub = _wire("D1", "dm", scope_id="T1")
md = {
"thread_id": "1700000000.000200",
@ -155,8 +157,8 @@ async def test_clarify_first_turn_self_anchor_stays_in_thread():
@pytest.mark.asyncio
async def test_slash_confirm_first_turn_self_anchor_stays_in_thread():
"""The stamp-trusting rule covers every prompt surface (single
async def test_slash_confirm_forwards_run_py_thread_stamp_untouched():
"""The forward-untouched rule covers every prompt surface (single
_send_prompt choke point)."""
adapter, stub = _wire("D1", "dm")
md = {"thread_id": "1700000000.000300", "message_id": "1700000000.000300"}
@ -403,3 +405,48 @@ def test_nested_relay_slack_config_subset_wins():
# Default: thread-per-message.
adapter.config.extra = {}
assert adapter._effective_reply_in_thread() is True
# ---------------------------------------------------------------------------
# Cross-module boundary pin (review 2026-07-28): the adapter deliberately has
# NO prompt-side strip — flat-mode placement depends entirely on run.py's
# _resolve_progress_thread_id suppressing the synthetic self-anchor upstream.
# If that suppression regresses, prompt cards silently thread again. These
# tests pin the boundary in BOTH modes so the coupling is load-bearing.
# ---------------------------------------------------------------------------
def test_run_py_suppresses_self_anchor_in_flat_mode():
from gateway.run import _resolve_progress_thread_id
# Flat mode + synthetic self-anchor (thread_id == own message id) => None:
# prompt/progress metadata arrives at the adapter with NO thread anchor.
assert (
_resolve_progress_thread_id(
"slack", "1700.001", "1700.001", reply_in_thread=False
)
is None
)
# Flat mode + REAL thread (ids differ) => the real thread survives.
assert (
_resolve_progress_thread_id(
"slack", "1699.000", "1700.001", reply_in_thread=False
)
== "1699.000"
)
def test_run_py_keeps_self_anchor_in_thread_mode():
from gateway.run import _resolve_progress_thread_id
# Thread-per-message mode: the first-turn self-anchor IS the thread root
# and must flow through to the adapter unchanged.
assert (
_resolve_progress_thread_id(
"slack", "1700.001", "1700.001", reply_in_thread=True
)
== "1700.001"
)
# No source thread at all: Slack synthesizes the root from the message id.
assert (
_resolve_progress_thread_id("slack", None, "1700.001", reply_in_thread=True)
== "1700.001"
)