fix(bot-mode): qualify a relayed author from the local connection too

delivery_turn_author kept the bare bot:<profile> id when the sender's connection was the Desktop's own "local", so a DM relayed from that machine collided with the recipient's profile of the same name. A relayed DM always crosses gateways, so the connection id is now part of the id whenever the Desktop sends one, and only the direct message_agent path in tools/bot_mode_dm.py stays bare. The relay.ts and session_auto_continue.py comments added earlier are cut to one line each.
This commit is contained in:
Erosika
2026-09-08 16:21:52 -04:00
committed by Teknium
parent d3c8bacbe3
commit 3db4defcc1
4 changed files with 11 additions and 14 deletions
@@ -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)
+4 -3
View File
@@ -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}
+5 -7
View File
@@ -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:<profile>``. 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]:
+1 -2
View File
@@ -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 {}),