diff --git a/apps/desktop/src/plugins/hermes-bots/relay.ts b/apps/desktop/src/plugins/hermes-bots/relay.ts index 4f60d85fc7..8641da9756 100644 --- a/apps/desktop/src/plugins/hermes-bots/relay.ts +++ b/apps/desktop/src/plugins/hermes-bots/relay.ts @@ -402,8 +402,7 @@ async function drainRelayOutboxes() { { profile: String(envelope?.target_profile || ''), message: String(envelope?.message || ''), - // The target gateway sets HERMES_TURN_AUTHOR on the delivery turn from these; the - // sender's connection id qualifies a bot on another machine. + // The target gateway builds the delivery turn's HERMES_TURN_AUTHOR from these three. from_profile: String(envelope?.from_profile || ''), from_handle: String(envelope?.from_handle || ''), from_connection: String(sender.id) diff --git a/tests/tools/test_bot_relay.py b/tests/tools/test_bot_relay.py index c880c8c301..270716d46e 100644 --- a/tests/tools/test_bot_relay.py +++ b/tests/tools/test_bot_relay.py @@ -599,11 +599,12 @@ def test_delivery_turn_author_from_envelope_sender_fields(): def test_delivery_turn_author_qualifies_a_remote_sender_by_its_connection(): - """Two machines can both run ``ops``; the Desktop's connection id keeps them apart, and its own gateway - (``local``) keeps the bare id.""" + """A relayed DM always crosses gateways, so the sender's connection id is part of the author id, ``local`` + included; the recipient's own ``ops`` is the only bare ``bot:ops``.""" remote = bot_relay.delivery_turn_author("ops", "ops-bot", "cloud-1") assert remote == {"id": "bot:cloud-1/ops", "name": "ops-bot", "is_bot": True} - assert bot_relay.delivery_turn_author("ops", "ops-bot", "local") == {"id": "bot:ops", "name": "ops-bot", "is_bot": True} + assert bot_relay.delivery_turn_author("ops", "ops-bot", "local") == {"id": "bot:local/ops", "name": "ops-bot", "is_bot": True} + # An older Desktop that sends no connection id still yields an author. assert bot_relay.delivery_turn_author("ops", "ops-bot", "") == {"id": "bot:ops", "name": "ops-bot", "is_bot": True} diff --git a/tools/bot_relay.py b/tools/bot_relay.py index ec0abc685a..0cea28e4b8 100644 --- a/tools/bot_relay.py +++ b/tools/bot_relay.py @@ -401,18 +401,16 @@ class DeliveryAuthor: def delivery_turn_author(from_profile: Any, from_handle: Any, from_connection: Any = None) -> Optional[dict]: - """The author of a relayed DM's recipient turn, built from the envelope's sender fields. A sender on another - machine is qualified by the Desktop's id for its connection, so two machines' ``scout`` profiles stay distinct. - None when the envelope names no sender, so an unattributed delivery stays unattributed.""" + """The author of a relayed DM's recipient turn, built from the envelope's sender fields. A relayed DM always + comes from another gateway, so the id carries the Desktop's id for the sender's connection (``local`` included) + and only the recipient's own profiles are bare ``bot:``. None when the envelope names no sender.""" from agent.turn_author import bot_author_id profile = str(from_profile or "").strip() if not profile: return None - connection = str(from_connection or "").strip() - # The Desktop names its own gateway "local"; that sender keeps the bare id local deliveries always had. - origin = connection if connection != "local" else "" - return {"id": bot_author_id(profile, origin), "name": str(from_handle or "").strip() or profile, "is_bot": True} + return {"id": bot_author_id(profile, str(from_connection or "")), "name": str(from_handle or "").strip() or profile, + "is_bot": True} def delivery_env(author: Optional[dict]) -> dict[str, str]: diff --git a/tui_gateway/session_auto_continue.py b/tui_gateway/session_auto_continue.py index a8bb332edc..fb550c9e1e 100644 --- a/tui_gateway/session_auto_continue.py +++ b/tui_gateway/session_auto_continue.py @@ -137,8 +137,7 @@ def _enqueue_prompt(session: dict, text: Any, transport: Any, image_paths: list[ # See #84417. _drop_queued_duplicates_of_inflight_user(session) text_only = not image_paths and isinstance(text, str) - # Never queue a text-only self-copy of the live prompt: draining it would restart it. Another sender's - # identical text is their message, not a copy. + # A text-only self-copy of the live prompt would restart it on drain; an authored copy is another sender's message. if text_only and not turn_author and text.strip() == _ac_inflight_original(session) != "": return queued = {"text": text, "transport": transport, **({"image_paths": image_paths} if image_paths else {}),