From 4ae3c1a22894fdf753603d6d3fc13a319e653a85 Mon Sep 17 00:00:00 2001 From: mavrickdeveloper Date: Sun, 17 May 2026 10:31:28 +0100 Subject: [PATCH] Avoid Honcho runtime peer collisions --- plugins/memory/honcho/session.py | 40 +++++++++++- tests/honcho_plugin/test_pin_peer_name.py | 77 +++++++++++++++++++++++ 2 files changed, 116 insertions(+), 1 deletion(-) diff --git a/plugins/memory/honcho/session.py b/plugins/memory/honcho/session.py index e4698aa9a30..5436f24fde2 100644 --- a/plugins/memory/honcho/session.py +++ b/plugins/memory/honcho/session.py @@ -2,6 +2,7 @@ from __future__ import annotations +import hashlib import queue import re import logging @@ -19,6 +20,8 @@ logger = logging.getLogger(__name__) # Sentinel to signal the async writer thread to shut down _ASYNC_SHUTDOWN = object() +_PEER_ID_HASH_LEN = 8 +_PEER_ID_HASH_ESCALATION_LENGTHS = (_PEER_ID_HASH_LEN, 12, 16, 24, 32, 64) @dataclass @@ -287,6 +290,41 @@ class HonchoSessionManager: chat_id = parts[1] if len(parts) > 1 else key return self._sanitize_id(f"user-{channel}-{chat_id}") + def _explicit_user_peer_ids(self) -> set[str]: + """Return sanitized user peer IDs that came from explicit config.""" + if self._config is None: + return set() + + explicit_ids: set[str] = set() + peer_name = getattr(self._config, "peer_name", None) + if peer_name: + explicit_ids.add(self._sanitize_id(str(peer_name).strip())) + + aliases = getattr(self._config, "user_peer_aliases", {}) + if isinstance(aliases, dict): + for alias in aliases.values(): + if isinstance(alias, str) and alias.strip(): + explicit_ids.add(self._sanitize_id(alias.strip())) + + return explicit_ids + + def _generated_runtime_peer_id(self, prefix: str, runtime_id: str) -> str: + """Return a stable peer ID for an unknown prefixed runtime user.""" + raw_peer_id = f"{prefix}{runtime_id}" + sanitized_peer_id = self._sanitize_id(raw_peer_id) + explicit_ids = self._explicit_user_peer_ids() + if ( + sanitized_peer_id != raw_peer_id + or sanitized_peer_id in explicit_ids + ): + digest = hashlib.sha256(raw_peer_id.encode("utf-8")).hexdigest() + for hash_len in _PEER_ID_HASH_ESCALATION_LENGTHS: + candidate = f"{sanitized_peer_id}-{digest[:hash_len]}" + if candidate not in explicit_ids: + return candidate + return f"{sanitized_peer_id}-{digest}" + return sanitized_peer_id + def _resolve_user_peer_id(self, key: str) -> str: """Resolve the Honcho user peer ID for this manager/session.""" pin_peer_name = ( @@ -311,7 +349,7 @@ class HonchoSessionManager: prefix = getattr(self._config, "runtime_peer_prefix", "") if self._config else "" prefix = prefix.strip() if isinstance(prefix, str) else "" if prefix: - return self._sanitize_id(f"{prefix}{primary_runtime_id}") + return self._generated_runtime_peer_id(prefix, primary_runtime_id) return self._sanitize_id(primary_runtime_id) if self._config and self._config.peer_name: diff --git a/tests/honcho_plugin/test_pin_peer_name.py b/tests/honcho_plugin/test_pin_peer_name.py index 21b355c8d39..2cfdfc6cdf6 100644 --- a/tests/honcho_plugin/test_pin_peer_name.py +++ b/tests/honcho_plugin/test_pin_peer_name.py @@ -19,6 +19,7 @@ Honcho API calls so we can assert the chosen ``user_peer_id`` without touching the network. """ +import hashlib import json from unittest.mock import MagicMock @@ -276,6 +277,82 @@ class TestPeerResolutionOrder: session = mgr.get_or_create("telegram:86701400") assert session.user_peer_id == "telegram_86701400" + def test_prefixed_runtime_id_hashes_when_sanitization_is_lossy(self): + """Generated prefixed IDs avoid merges caused by lossy sanitization.""" + raw_peer_id = "telegram_user:42" + expected_hash = hashlib.sha256(raw_peer_id.encode("utf-8")).hexdigest()[:8] + mgr = HonchoSessionManager( + honcho=MagicMock(), + config=self._config( + peer_name=None, + pin_peer_name=False, + runtime_peer_prefix="telegram_", + ), + runtime_user_peer_name="user:42", + ) + _patch_manager_for_resolution_test(mgr) + + session = mgr.get_or_create("telegram:user:42") + assert session.user_peer_id == f"telegram_user-42-{expected_hash}" + + def test_prefixed_runtime_id_hashes_when_it_collides_with_peer_name(self): + """Unknown generated peers should not silently merge into peerName.""" + raw_peer_id = "telegram_86701400" + expected_hash = hashlib.sha256(raw_peer_id.encode("utf-8")).hexdigest()[:8] + mgr = HonchoSessionManager( + honcho=MagicMock(), + config=self._config( + peer_name="telegram_86701400", + pin_peer_name=False, + runtime_peer_prefix="telegram_", + ), + runtime_user_peer_name="86701400", + ) + _patch_manager_for_resolution_test(mgr) + + session = mgr.get_or_create("telegram:86701400") + assert session.user_peer_id == f"telegram_86701400-{expected_hash}" + + def test_prefixed_runtime_id_hashes_when_it_collides_with_alias_target(self): + """Unknown generated peers should not silently merge into alias targets.""" + raw_peer_id = "telegram_86701400" + expected_hash = hashlib.sha256(raw_peer_id.encode("utf-8")).hexdigest()[:8] + mgr = HonchoSessionManager( + honcho=MagicMock(), + config=self._config( + peer_name=None, + pin_peer_name=False, + user_peer_aliases={"known-user": "telegram_86701400"}, + runtime_peer_prefix="telegram_", + ), + runtime_user_peer_name="86701400", + ) + _patch_manager_for_resolution_test(mgr) + + session = mgr.get_or_create("telegram:86701400") + assert session.user_peer_id == f"telegram_86701400-{expected_hash}" + + def test_prefixed_runtime_id_extends_hash_when_short_hash_collides(self): + raw_peer_id = "telegram_86701400" + digest = hashlib.sha256(raw_peer_id.encode("utf-8")).hexdigest() + mgr = HonchoSessionManager( + honcho=MagicMock(), + config=self._config( + peer_name=None, + pin_peer_name=False, + user_peer_aliases={ + "known-user": "telegram_86701400", + "reserved-user": f"telegram_86701400-{digest[:8]}", + }, + runtime_peer_prefix="telegram_", + ), + runtime_user_peer_name="86701400", + ) + _patch_manager_for_resolution_test(mgr) + + session = mgr.get_or_create("telegram:86701400") + assert session.user_peer_id == f"telegram_86701400-{digest[:12]}" + def test_alias_value_is_sanitized_after_selection(self): mgr = HonchoSessionManager( honcho=MagicMock(),