From 46d625b09755885fa60f84de0e5df79b06f6b509 Mon Sep 17 00:00:00 2001 From: Erosika Date: Fri, 4 Sep 2026 15:32:48 -0400 Subject: [PATCH] feat(honcho): map bot authors onto their profile peer The bot-mode dispatcher names another profile as bot:. The resolver treated that like a human runtime id, so a configured runtimePeerPrefix produced peers like telegram_bot:coder and a profile that already owns an AI peer in the same workspace got a second one. A bot: author now resolves in this order: a userPeerAliases entry for the full bot id, else the sanitized profile name. A cloned profile's aiPeer defaults to the profile name, so a same-workspace sender lands on its existing AI peer. Prefixes never apply to bot ids. pinUserPeer still collapses bot authors onto the pinned peer, the same as every other author. --- plugins/memory/honcho/session_peers.py | 10 +++++++- tests/honcho_plugin/test_turn_author_peers.py | 25 +++++++++++++++++++ 2 files changed, 34 insertions(+), 1 deletion(-) diff --git a/plugins/memory/honcho/session_peers.py b/plugins/memory/honcho/session_peers.py index ddb26c52b1..e9b44af0a6 100644 --- a/plugins/memory/honcho/session_peers.py +++ b/plugins/memory/honcho/session_peers.py @@ -13,6 +13,8 @@ if TYPE_CHECKING: logger = logging.getLogger("plugins.memory.honcho.session") _PEER_ID_HASH_ESCALATION_LENGTHS = (8, 12, 16, 24, 32, 64) +# Author ids the bot-mode dispatcher assigns to other Hermes profiles (tools/bot_relay.py). +BOT_AUTHOR_PREFIX = "bot:" class SessionPeersMixin: @@ -103,12 +105,18 @@ class SessionPeersMixin: def _peer_id_for_runtime_id(self, runtime_id: str) -> str: """Map one gateway runtime identity onto its Honcho peer ID with the same alias-then-prefix - order ``_resolve_user_peer_id`` applies, so an aliased account lands on its peer on any turn.""" + order ``_resolve_user_peer_id`` applies, so an aliased account lands on its peer on any turn. + A ``bot:`` author maps onto the profile name: a cloned profile's aiPeer defaults to + that name, so a sender in the same workspace lands on its existing AI peer. Prefixes never + apply to bot ids.""" aliases = getattr(self._config, "user_peer_aliases", {}) if self._config else {} if isinstance(aliases, dict): alias = aliases.get(runtime_id) if isinstance(alias, str) and alias.strip(): return self._sanitize_id(alias.strip()) + if runtime_id.startswith(BOT_AUTHOR_PREFIX): + profile = runtime_id[len(BOT_AUTHOR_PREFIX):].strip() + return self._sanitize_id(profile or runtime_id) prefix = getattr(self._config, "runtime_peer_prefix", "") if self._config else "" prefix = prefix.strip() if isinstance(prefix, str) else "" return self._generated_runtime_peer_id(prefix, runtime_id) if prefix else self._sanitize_id(runtime_id) diff --git a/tests/honcho_plugin/test_turn_author_peers.py b/tests/honcho_plugin/test_turn_author_peers.py index 0a23397046..2895356fc6 100644 --- a/tests/honcho_plugin/test_turn_author_peers.py +++ b/tests/honcho_plugin/test_turn_author_peers.py @@ -78,6 +78,31 @@ class TestResolveAuthorPeerId: mgr = _manager(_config(pin_peer_name=True), runtime_id="7654321") assert mgr.resolve_author_peer_id("telegram:group1", "111222") is None + def test_bot_author_lands_on_its_profile_peer(self): + """A cloned profile's aiPeer defaults to the profile name, so the sender reuses its AI peer.""" + mgr = _manager(_config(), runtime_id="7654321") + assert mgr.resolve_author_peer_id("telegram:dm1", "bot:coder") == "coder" + + def test_bot_author_alias_wins_over_the_profile_name(self): + mgr = _manager(_config(user_peer_aliases={"bot:coder": "hermes-coder"}), runtime_id="7654321") + assert mgr.resolve_author_peer_id("telegram:dm1", "bot:coder") == "hermes-coder" + + def test_bot_author_ignores_the_runtime_prefix(self): + mgr = _manager(_config(runtime_peer_prefix="telegram_"), runtime_id="7654321") + assert mgr.resolve_author_peer_id("telegram:dm1", "bot:coder") == "coder" + + def test_bot_author_profile_name_is_sanitized(self): + mgr = _manager(_config(), runtime_id="7654321") + assert mgr.resolve_author_peer_id("telegram:dm1", "bot:my profile.v2") == "my-profile-v2" + + def test_bot_author_without_a_profile_keeps_the_raw_id(self): + mgr = _manager(_config(), runtime_id="7654321") + assert mgr.resolve_author_peer_id("telegram:dm1", "bot:") == "bot-" + + def test_pin_peer_name_collapses_bot_authors_too(self): + mgr = _manager(_config(pin_peer_name=True), runtime_id="7654321") + assert mgr.resolve_author_peer_id("telegram:dm1", "bot:coder") is None + def test_display_name_never_becomes_a_peer_id(self): """Display names are attacker-influenceable on most platforms.""" mgr = _manager(_config(), runtime_id="7654321")