Add Honcho runtime peer mapping

This commit is contained in:
mavrickdeveloper 2026-05-17 10:08:51 +01:00
parent 519657aa98
commit 864cdb3d2e
No known key found for this signature in database
10 changed files with 376 additions and 33 deletions

View file

@ -360,6 +360,7 @@ class HonchoMemoryProvider(MemoryProvider):
config=cfg,
context_tokens=cfg.context_tokens,
runtime_user_peer_name=kwargs.get("user_id") or None,
runtime_user_peer_name_alt=kwargs.get("user_id_alt") or None,
)
# ----- B3: resolve_session_name -----

View file

@ -122,6 +122,34 @@ def _parse_int_config(host_val, root_val, default: int) -> int:
return default
def _parse_string_map(host_obj: dict, root_obj: dict, key: str) -> dict[str, str]:
"""Parse a string-to-string map with host-level whole-map override."""
source = host_obj[key] if key in host_obj else root_obj.get(key)
if not isinstance(source, dict):
return {}
result: dict[str, str] = {}
for raw_key, raw_value in source.items():
alias_key = str(raw_key).strip()
alias_value = str(raw_value).strip() if raw_value is not None else ""
if alias_key and alias_value:
result[alias_key] = alias_value
return result
def _parse_optional_string(
host_obj: dict, root_obj: dict, key: str, default: str = ""
) -> str:
"""Parse a string field where host-level empty string can override root."""
if key in host_obj:
value = host_obj.get(key)
else:
value = root_obj.get(key, default)
if value is None:
return default
return str(value).strip()
def _parse_dialectic_depth(host_val, root_val) -> int:
"""Parse dialecticDepth: host wins, then root, then 1. Clamped to 1-3."""
for val in (host_val, root_val):
@ -259,6 +287,12 @@ class HonchoClientConfig:
# each platform would fork memory into its own peer (#14984). Default
# ``False`` preserves existing multi-user behaviour.
pin_peer_name: bool = False
# Map gateway runtime user IDs to stable Honcho user peers. Host-level
# config replaces the root map as a whole so profiles can intentionally
# own their identity mappings.
user_peer_aliases: dict[str, str] = field(default_factory=dict)
# Optional prefix for unknown gateway runtime user IDs, e.g. "telegram_".
runtime_peer_prefix: str = ""
# Toggles
enabled: bool = False
save_messages: bool = True
@ -458,6 +492,16 @@ class HonchoClientConfig:
raw.get("pinPeerName"),
default=False,
),
user_peer_aliases=_parse_string_map(
host_block,
raw,
"userPeerAliases",
),
runtime_peer_prefix=_parse_optional_string(
host_block,
raw,
"runtimePeerPrefix",
),
enabled=enabled,
save_messages=save_messages,
write_frequency=write_frequency,

View file

@ -79,6 +79,7 @@ class HonchoSessionManager:
context_tokens: int | None = None,
config: Any | None = None,
runtime_user_peer_name: str | None = None,
runtime_user_peer_name_alt: str | None = None,
):
"""
Initialize the session manager.
@ -89,11 +90,13 @@ class HonchoSessionManager:
config: HonchoClientConfig from global config (provides peer_name, ai_peer,
write_frequency, observation, etc.).
runtime_user_peer_name: Gateway user identity for per-user memory scoping.
runtime_user_peer_name_alt: Optional stable alternate gateway identity.
"""
self._honcho = honcho
self._context_tokens = context_tokens
self._config = config
self._runtime_user_peer_name = runtime_user_peer_name
self._runtime_user_peer_name_alt = runtime_user_peer_name_alt
self._cache: dict[str, HonchoSession] = {}
self._cache_lock = threading.RLock()
self._peers_cache: dict[str, Any] = {}
@ -267,6 +270,55 @@ class HonchoSessionManager:
"""Sanitize an ID to match Honcho's pattern: ^[a-zA-Z0-9_-]+"""
return re.sub(r'[^a-zA-Z0-9_-]', '-', id_str)
def _runtime_user_ids(self) -> list[str]:
"""Return runtime identity candidates in lookup order."""
candidates: list[str] = []
for value in (self._runtime_user_peer_name, self._runtime_user_peer_name_alt):
if value is None:
continue
candidate = str(value).strip()
if candidate and candidate not in candidates:
candidates.append(candidate)
return candidates
def _session_key_fallback_peer_id(self, key: str) -> str:
parts = key.split(":", 1)
channel = parts[0] if len(parts) > 1 else "default"
chat_id = parts[1] if len(parts) > 1 else key
return self._sanitize_id(f"user-{channel}-{chat_id}")
def _resolve_user_peer_id(self, key: str) -> str:
"""Resolve the Honcho user peer ID for this manager/session."""
pin_peer_name = (
self._config is not None
and bool(getattr(self._config, "peer_name", None))
and getattr(self._config, "pin_peer_name", False) is True
)
if pin_peer_name:
return self._sanitize_id(self._config.peer_name)
runtime_ids = self._runtime_user_ids()
if runtime_ids:
aliases = getattr(self._config, "user_peer_aliases", {}) if self._config else {}
if not isinstance(aliases, dict):
aliases = {}
for runtime_id in runtime_ids:
alias = aliases.get(runtime_id)
if isinstance(alias, str) and alias.strip():
return self._sanitize_id(alias.strip())
primary_runtime_id = runtime_ids[0]
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._sanitize_id(primary_runtime_id)
if self._config and self._config.peer_name:
return self._sanitize_id(self._config.peer_name)
return self._session_key_fallback_peer_id(key)
def get_or_create(self, key: str) -> HonchoSession:
"""
Get an existing session or create a new one.
@ -285,31 +337,11 @@ class HonchoSessionManager:
# Determine peer IDs — no lock needed (read-only, no shared state mutation).
# Gateway sessions normally use the runtime user identity (the
# platform-native ID: Telegram UID, Discord snowflake, Slack user,
# etc.) so multi-user bots scope memory per user. For a single-user
# deployment the config-supplied ``peer_name`` is an unambiguous
# identity and we should keep it unified across platforms — see
# #14984. Opt into that with ``hosts.<host>.pinPeerName: true`` in
# ``honcho.json`` (or root-level ``pinPeerName: true``).
# `is True` (not `bool(...)`) is deliberate: several multi-user tests
# pass a ``MagicMock`` for ``config`` where ``mock.pin_peer_name``
# silently returns another MagicMock — truthy by default. Requiring
# strict ``True`` keeps pinning as opt-in even for callers that
# haven't updated their mocks yet; real configs built via
# ``from_global_config`` always produce a proper boolean.
pin_peer_name = (
self._config is not None
and bool(getattr(self._config, "peer_name", None))
and getattr(self._config, "pin_peer_name", False) is True
)
if self._runtime_user_peer_name and not pin_peer_name:
user_peer_id = self._sanitize_id(self._runtime_user_peer_name)
elif self._config and self._config.peer_name:
user_peer_id = self._sanitize_id(self._config.peer_name)
else:
parts = key.split(":", 1)
channel = parts[0] if len(parts) > 1 else "default"
chat_id = parts[1] if len(parts) > 1 else key
user_peer_id = self._sanitize_id(f"user-{channel}-{chat_id}")
# etc.) so multi-user bots scope memory per user. Config can alias
# known runtime IDs or prefix unknown IDs. For a single-user
# deployment, ``pinPeerName`` still pins all runtime identities to
# ``peerName`` (see #14984).
user_peer_id = self._resolve_user_peer_id(key)
assistant_peer_id = self._sanitize_id(
self._config.ai_peer if self._config else "hermes-assistant"