feat(honcho): map bot authors onto their profile peer

The bot-mode dispatcher names another profile as bot:<profile>. 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:<profile> 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.
This commit is contained in:
Erosika
2026-09-04 15:32:48 -04:00
committed by Teknium
parent 20b117ad3b
commit 46d625b097
2 changed files with 34 additions and 1 deletions

View File

@@ -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:<profile>`` 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)

View File

@@ -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")