mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-31 19:16:29 +00:00
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:
parent
71d5c47e21
commit
9864e00fb4
2 changed files with 61 additions and 6 deletions
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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."""
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue