diff --git a/plugins/memory/honcho/README.md b/plugins/memory/honcho/README.md index 44c523be8..70fe1fb53 100644 --- a/plugins/memory/honcho/README.md +++ b/plugins/memory/honcho/README.md @@ -138,8 +138,8 @@ In gateway deployments (Telegram, Discord, Slack, etc.) each user arrives with a | Key | Type | Default | Description | |-----|------|---------|-------------| | `pinUserPeer` | bool | `false` | When `true`, every gateway runtime user collapses to `peerName`. Single-operator deployments where you want all your platforms (and any other users) to share one peer | -| `userPeerAliases` | object | `{}` | Map of runtime IDs to peer IDs (`{"86701400": "alice"}`). Many-to-one is the intended pattern — alias all your runtime IDs to one peer name. One-to-many is not supported; one runtime ID resolves to exactly one peer | -| `runtimePeerPrefix` | string | `""` | Prepended to unknown runtime IDs to namespace them (e.g. `"telegram_"` → `telegram_86701400`). Used only when no alias matches. Prevents collisions between platforms whose runtime IDs share the same shape | +| `userPeerAliases` | object | `{}` | Map of runtime IDs to peer IDs (`{"7654321": "alice"}`). Many-to-one is the intended pattern — alias all your runtime IDs to one peer name. One-to-many is not supported; one runtime ID resolves to exactly one peer | +| `runtimePeerPrefix` | string | `""` | Prepended to unknown runtime IDs to namespace them (e.g. `"telegram_"` → `telegram_7654321`). Used only when no alias matches. Prevents collisions between platforms whose runtime IDs share the same shape | > **Deprecated:** `pinPeerName` is a legacy alias for `pinUserPeer`, still read for back-compat (`pinUserPeer` wins where both are set). `hermes honcho setup` migrates it onto `pinUserPeer` on touch and never writes it. diff --git a/plugins/memory/honcho/cli.py b/plugins/memory/honcho/cli.py index 33edcf12d..25460989d 100644 --- a/plugins/memory/honcho/cli.py +++ b/plugins/memory/honcho/cli.py @@ -413,7 +413,7 @@ def _collect_operator_aliases(existing: dict, peer_target: str) -> dict: print(f"\n Add runtime IDs that should alias to peer '{peer_target}'.") print(" Leave blank to skip a platform. Existing aliases are preserved.") for platform_label, alias_hint in ( - ("Telegram UID", "e.g. 86701400"), + ("Telegram UID", "e.g. 7654321"), ("Discord snowflake", "e.g. 491827364"), ("Slack user ID", "e.g. U04ABCDEF"), ("Matrix MXID", "e.g. @you:matrix.org"), diff --git a/tests/gateway/test_agent_cache.py b/tests/gateway/test_agent_cache.py index 37f8b51a4..e3e14c705 100644 --- a/tests/gateway/test_agent_cache.py +++ b/tests/gateway/test_agent_cache.py @@ -1466,7 +1466,7 @@ class TestAgentConfigSignatureUserId: from gateway.run import GatewayRunner runtime = {"provider": "anthropic", "api_key": "k", "base_url": "", "api_mode": "chat_completions"} sig_a = GatewayRunner._agent_config_signature( - "claude-sonnet-4", runtime, ["hermes-telegram"], "", user_id="86701400" + "claude-sonnet-4", runtime, ["hermes-telegram"], "", user_id="7654321" ) sig_b = GatewayRunner._agent_config_signature( "claude-sonnet-4", runtime, ["hermes-telegram"], "", user_id="491827364" @@ -1477,10 +1477,10 @@ class TestAgentConfigSignatureUserId: from gateway.run import GatewayRunner runtime = {"provider": "anthropic", "api_key": "k", "base_url": "", "api_mode": "chat_completions"} sig_1 = GatewayRunner._agent_config_signature( - "claude-sonnet-4", runtime, ["hermes-telegram"], "", user_id="86701400" + "claude-sonnet-4", runtime, ["hermes-telegram"], "", user_id="7654321" ) sig_2 = GatewayRunner._agent_config_signature( - "claude-sonnet-4", runtime, ["hermes-telegram"], "", user_id="86701400" + "claude-sonnet-4", runtime, ["hermes-telegram"], "", user_id="7654321" ) assert sig_1 == sig_2 @@ -1489,11 +1489,11 @@ class TestAgentConfigSignatureUserId: runtime = {"provider": "anthropic", "api_key": "k", "base_url": "", "api_mode": "chat_completions"} sig_a = GatewayRunner._agent_config_signature( "claude-sonnet-4", runtime, ["hermes-telegram"], "", - user_id="86701400", user_id_alt="@igor_tg", + user_id="7654321", user_id_alt="@igor_tg", ) sig_b = GatewayRunner._agent_config_signature( "claude-sonnet-4", runtime, ["hermes-telegram"], "", - user_id="86701400", user_id_alt="@erosika_tg", + user_id="7654321", user_id_alt="@erosika_tg", ) assert sig_a != sig_b diff --git a/tests/honcho_plugin/test_cli.py b/tests/honcho_plugin/test_cli.py index afcc7af07..c021cdb8c 100644 --- a/tests/honcho_plugin/test_cli.py +++ b/tests/honcho_plugin/test_cli.py @@ -263,7 +263,7 @@ class TestCloneHonchoForProfile: "apiKey": "***", "hosts": { "hermes": { - "userPeerAliases": {"86701400": "eri", "discord-491827364": "eri"}, + "userPeerAliases": {"7654321": "eri", "discord-491827364": "eri"}, "peerName": "eri", }, }, @@ -272,7 +272,7 @@ class TestCloneHonchoForProfile: ok = honcho_cli.clone_honcho_for_profile("coder") assert ok is True new_block = written["cfg"]["hosts"]["hermes_coder"] - assert new_block["userPeerAliases"] == {"86701400": "eri", "discord-491827364": "eri"} + assert new_block["userPeerAliases"] == {"7654321": "eri", "discord-491827364": "eri"} def test_runtime_peer_prefix_carries_into_cloned_profile(self, monkeypatch, tmp_path): cfg = { @@ -447,7 +447,7 @@ class TestSetupWizardDeploymentShape: "hermes", # workspace "2", # tree: me + other people "y", # keep my memory pooled? → hybrid - "86701400", # telegram uid + "7654321", # telegram uid "491827364", # discord snowflake "", # slack (skip) "", # matrix (skip) @@ -456,7 +456,7 @@ class TestSetupWizardDeploymentShape: host = self._run_setup(monkeypatch, tmp_path, answers=answers) assert host["pinUserPeer"] is False assert host["userPeerAliases"] == { - "86701400": "eri", + "7654321": "eri", "491827364": "eri", } assert "runtimePeerPrefix" not in host @@ -498,7 +498,7 @@ class TestSetupWizardDeploymentShape: "hermes", # workspace "3", # tree: only others — triggers the orphan guard "y", # pool my own memory instead? → hybrid - "86701400", # telegram uid + "7654321", # telegram uid "", # discord (skip) "", # slack (skip) "", # matrix (skip) @@ -506,7 +506,7 @@ class TestSetupWizardDeploymentShape: ] host = self._run_setup(monkeypatch, tmp_path, answers=answers, initial_cfg=initial_cfg) assert host["pinUserPeer"] is False - assert host["userPeerAliases"] == {"86701400": "eri"} + assert host["userPeerAliases"] == {"7654321": "eri"} def test_unpin_decline_steer_keeps_per_user(self, monkeypatch, tmp_path): """Operator can decline the steer ('n') and accept orphaning, ending @@ -575,7 +575,7 @@ class TestSetupWizardDeploymentShape: """ initial_cfg = { "apiKey": "***", - "userPeerAliases": {"86701400": "eri"}, + "userPeerAliases": {"7654321": "eri"}, "hosts": {"hermes": {"peerName": "eri"}}, } answers = ["cloud", "", "eri", "hermetika", "hermes"] @@ -583,7 +583,7 @@ class TestSetupWizardDeploymentShape: assert host["pinUserPeer"] is False # Hybrid materialises the root aliases into the host so subsequent # operator edits live on the host block they're inspecting. - assert host["userPeerAliases"] == {"86701400": "eri"} + assert host["userPeerAliases"] == {"7654321": "eri"} def test_only_others_does_not_override_root_user_peer_aliases(self, monkeypatch, tmp_path): """Explicitly choosing 'only other people' must leave the host diff --git a/tests/honcho_plugin/test_pin_peer_name.py b/tests/honcho_plugin/test_pin_peer_name.py index 1e72bc97d..1a6e2394a 100644 --- a/tests/honcho_plugin/test_pin_peer_name.py +++ b/tests/honcho_plugin/test_pin_peer_name.py @@ -105,7 +105,7 @@ class TestRuntimePeerMappingConfigParsing: config_file.write_text(json.dumps({ "apiKey": "k", "userPeerAliases": { - " 86701400 ": " Igor ", + " 7654321 ": " Igor ", "": "ignored", "empty-value": " ", "null-value": None, @@ -115,7 +115,7 @@ class TestRuntimePeerMappingConfigParsing: config = HonchoClientConfig.from_global_config(config_path=config_file) - assert config.user_peer_aliases == {"86701400": "Igor"} + assert config.user_peer_aliases == {"7654321": "Igor"} assert config.runtime_peer_prefix == "telegram_" def test_host_aliases_override_root_aliases_as_whole_map(self, tmp_path): @@ -226,12 +226,12 @@ class TestPeerResolutionOrder: mgr = HonchoSessionManager( honcho=MagicMock(), config=self._config(peer_name="Igor", pin_peer_name=False), - runtime_user_peer_name="86701400", # e.g. Telegram UID + runtime_user_peer_name="7654321", # e.g. Telegram UID ) _patch_manager_for_resolution_test(mgr) - session = mgr.get_or_create("telegram:86701400") - assert session.user_peer_id == "86701400", ( + session = mgr.get_or_create("telegram:7654321") + assert session.user_peer_id == "7654321", ( "pin_peer_name=False is the multi-user default — the gateway's " "platform-native user ID must win so each user gets their own " "peer scope. If this regresses, every Telegram/Discord/Slack " @@ -245,14 +245,14 @@ class TestPeerResolutionOrder: config=self._config( peer_name="Igor", pin_peer_name=False, - user_peer_aliases={"86701400": "Igor"}, + user_peer_aliases={"7654321": "Igor"}, runtime_peer_prefix="telegram_", ), - runtime_user_peer_name="86701400", + runtime_user_peer_name="7654321", ) _patch_manager_for_resolution_test(mgr) - session = mgr.get_or_create("telegram:86701400") + session = mgr.get_or_create("telegram:7654321") assert session.user_peer_id == "Igor" def test_unknown_runtime_id_uses_prefix(self): @@ -264,12 +264,12 @@ class TestPeerResolutionOrder: pin_peer_name=False, runtime_peer_prefix="telegram_", ), - runtime_user_peer_name="86701400", + runtime_user_peer_name="7654321", ) _patch_manager_for_resolution_test(mgr) - session = mgr.get_or_create("telegram:86701400") - assert session.user_peer_id == "telegram_86701400" + session = mgr.get_or_create("telegram:7654321") + assert session.user_peer_id == "telegram_7654321" def test_prefixed_runtime_id_hashes_when_sanitization_is_lossy(self): """Generated prefixed IDs avoid merges caused by lossy sanitization.""" @@ -291,43 +291,43 @@ class TestPeerResolutionOrder: 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" + raw_peer_id = "telegram_7654321" expected_hash = hashlib.sha256(raw_peer_id.encode("utf-8")).hexdigest()[:8] mgr = HonchoSessionManager( honcho=MagicMock(), config=self._config( - peer_name="telegram_86701400", + peer_name="telegram_7654321", pin_peer_name=False, runtime_peer_prefix="telegram_", ), - runtime_user_peer_name="86701400", + runtime_user_peer_name="7654321", ) _patch_manager_for_resolution_test(mgr) - session = mgr.get_or_create("telegram:86701400") - assert session.user_peer_id == f"telegram_86701400-{expected_hash}" + session = mgr.get_or_create("telegram:7654321") + assert session.user_peer_id == f"telegram_7654321-{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" + raw_peer_id = "telegram_7654321" 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"}, + user_peer_aliases={"known-user": "telegram_7654321"}, runtime_peer_prefix="telegram_", ), - runtime_user_peer_name="86701400", + runtime_user_peer_name="7654321", ) _patch_manager_for_resolution_test(mgr) - session = mgr.get_or_create("telegram:86701400") - assert session.user_peer_id == f"telegram_86701400-{expected_hash}" + session = mgr.get_or_create("telegram:7654321") + assert session.user_peer_id == f"telegram_7654321-{expected_hash}" def test_prefixed_runtime_id_extends_hash_when_short_hash_collides(self): - raw_peer_id = "telegram_86701400" + raw_peer_id = "telegram_7654321" digest = hashlib.sha256(raw_peer_id.encode("utf-8")).hexdigest() mgr = HonchoSessionManager( honcho=MagicMock(), @@ -335,17 +335,17 @@ class TestPeerResolutionOrder: peer_name=None, pin_peer_name=False, user_peer_aliases={ - "known-user": "telegram_86701400", - "reserved-user": f"telegram_86701400-{digest[:8]}", + "known-user": "telegram_7654321", + "reserved-user": f"telegram_7654321-{digest[:8]}", }, runtime_peer_prefix="telegram_", ), - runtime_user_peer_name="86701400", + runtime_user_peer_name="7654321", ) _patch_manager_for_resolution_test(mgr) - session = mgr.get_or_create("telegram:86701400") - assert session.user_peer_id == f"telegram_86701400-{digest[:12]}" + session = mgr.get_or_create("telegram:7654321") + assert session.user_peer_id == f"telegram_7654321-{digest[:12]}" def test_alias_value_is_sanitized_after_selection(self): mgr = HonchoSessionManager( @@ -353,13 +353,13 @@ class TestPeerResolutionOrder: config=self._config( peer_name=None, pin_peer_name=False, - user_peer_aliases={"86701400": "Alice Smith!"}, + user_peer_aliases={"7654321": "Alice Smith!"}, ), - runtime_user_peer_name="86701400", + runtime_user_peer_name="7654321", ) _patch_manager_for_resolution_test(mgr) - session = mgr.get_or_create("telegram:86701400") + session = mgr.get_or_create("telegram:7654321") assert session.user_peer_id == "Alice-Smith-" def test_alias_keys_match_raw_runtime_id_before_sanitization(self): @@ -391,13 +391,13 @@ class TestPeerResolutionOrder: runtime_peer_prefix="telegram_", session_peer_prefix=True, ), - runtime_user_peer_name="86701400", + runtime_user_peer_name="7654321", ) _patch_manager_for_resolution_test(mgr) - session = mgr.get_or_create("telegram:86701400") - assert session.user_peer_id == "telegram_86701400" - assert session.honcho_session_id == "telegram-86701400" + session = mgr.get_or_create("telegram:7654321") + assert session.user_peer_id == "telegram_7654321" + assert session.honcho_session_id == "telegram-7654321" def test_config_wins_when_pin_is_true(self): """With pin enabled, configured peer_name beats runtime ID.""" @@ -406,14 +406,14 @@ class TestPeerResolutionOrder: config=self._config( peer_name="Igor", pin_peer_name=True, - user_peer_aliases={"86701400": "Alias"}, + user_peer_aliases={"7654321": "Alias"}, runtime_peer_prefix="telegram_", ), - runtime_user_peer_name="86701400", # Telegram pushes this in + runtime_user_peer_name="7654321", # Telegram pushes this in ) _patch_manager_for_resolution_test(mgr) - session = mgr.get_or_create("telegram:86701400") + session = mgr.get_or_create("telegram:7654321") assert session.user_peer_id == "Igor", ( "With pinPeerName=true the user's configured peer_name must " "beat the platform-native runtime ID so memory stays unified " @@ -429,26 +429,26 @@ class TestPeerResolutionOrder: config=self._config( peer_name=None, pin_peer_name=True, - user_peer_aliases={"86701400": "Igor"}, + user_peer_aliases={"7654321": "Igor"}, runtime_peer_prefix="telegram_", ), - runtime_user_peer_name="86701400", + runtime_user_peer_name="7654321", ) _patch_manager_for_resolution_test(mgr) - session = mgr.get_or_create("telegram:86701400") + session = mgr.get_or_create("telegram:7654321") assert session.user_peer_id == "Igor" def test_pin_noop_without_peer_name_or_mapping_preserves_runtime(self): mgr = HonchoSessionManager( honcho=MagicMock(), config=self._config(peer_name=None, pin_peer_name=True), - runtime_user_peer_name="86701400", + runtime_user_peer_name="7654321", ) _patch_manager_for_resolution_test(mgr) - session = mgr.get_or_create("telegram:86701400") - assert session.user_peer_id == "86701400" + session = mgr.get_or_create("telegram:7654321") + assert session.user_peer_id == "7654321" def test_alt_runtime_id_can_match_alias_without_changing_raw_fallback(self): """Stable alternate IDs can map known users while primary ID fallback stays unchanged.""" @@ -526,11 +526,11 @@ class TestPeerResolutionOrder: mgr = HonchoSessionManager( honcho=MagicMock(), config=cfg, - runtime_user_peer_name="86701400", + runtime_user_peer_name="7654321", ) _patch_manager_for_resolution_test(mgr) - session = mgr.get_or_create("telegram:86701400") + session = mgr.get_or_create("telegram:7654321") assert session.user_peer_id == "Igor" assert session.assistant_peer_id == "hermes-assistant" @@ -556,10 +556,10 @@ class TestCrossPlatformMemoryUnification: mgr_telegram = HonchoSessionManager( honcho=MagicMock(), config=self._config_pinned(), - runtime_user_peer_name="86701400", + runtime_user_peer_name="7654321", ) _patch_manager_for_resolution_test(mgr_telegram) - telegram_session = mgr_telegram.get_or_create("telegram:86701400") + telegram_session = mgr_telegram.get_or_create("telegram:7654321") # Discord turn (separate manager instance — simulates a fresh # platform-adapter invocation) @@ -701,20 +701,20 @@ class TestPinTransition: pinned_mgr = HonchoSessionManager( honcho=MagicMock(), config=self._pinned(), - runtime_user_peer_name="86701400", + runtime_user_peer_name="7654321", ) _patch_manager_for_resolution_test(pinned_mgr) - before = pinned_mgr.get_or_create("telegram:86701400") + before = pinned_mgr.get_or_create("telegram:7654321") assert before.user_peer_id == "Igor" unpinned_mgr = HonchoSessionManager( honcho=MagicMock(), config=self._unpinned(), - runtime_user_peer_name="86701400", + runtime_user_peer_name="7654321", ) _patch_manager_for_resolution_test(unpinned_mgr) - after = unpinned_mgr.get_or_create("telegram:86701400") - assert after.user_peer_id == "86701400", ( + after = unpinned_mgr.get_or_create("telegram:7654321") + assert after.user_peer_id == "7654321", ( "After flipping pinPeerName off, the same runtime ID must resolve " "to its own peer — otherwise multi-user mode silently merges users." ) @@ -723,14 +723,14 @@ class TestPinTransition: mgr = HonchoSessionManager( honcho=MagicMock(), config=self._pinned(), - runtime_user_peer_name="86701400", + runtime_user_peer_name="7654321", ) _patch_manager_for_resolution_test(mgr) - first = mgr.get_or_create("telegram:86701400") + first = mgr.get_or_create("telegram:7654321") assert first.user_peer_id == "Igor" mgr._config = self._unpinned() - second = mgr.get_or_create("telegram:86701400") + second = mgr.get_or_create("telegram:7654321") assert second.user_peer_id == "Igor", ( "The per-key session cache is keyed by session-key, not by " "resolved peer. In-process flips don't invalidate it — the " @@ -764,7 +764,7 @@ class TestPinTransition: cfg_path.write_text(json.dumps({ "apiKey": "k", "peerName": "Igor", - "userPeerAliases": {"86701400": "Igor"}, + "userPeerAliases": {"7654321": "Igor"}, })) sig_with_aliases = GatewayRunner._extract_cache_busting_config({"memory": {"provider": "honcho"}}) @@ -839,18 +839,18 @@ class TestProfilePeerUniqueness: mgr_a = HonchoSessionManager( honcho=MagicMock(), config=self._pinned_to("alice"), - runtime_user_peer_name="86701400", + runtime_user_peer_name="7654321", ) _patch_manager_for_resolution_test(mgr_a) - sess_a = mgr_a.get_or_create("telegram:86701400") + sess_a = mgr_a.get_or_create("telegram:7654321") mgr_b = HonchoSessionManager( honcho=MagicMock(), config=self._pinned_to("bob"), - runtime_user_peer_name="86701400", + runtime_user_peer_name="7654321", ) _patch_manager_for_resolution_test(mgr_b) - sess_b = mgr_b.get_or_create("telegram:86701400") + sess_b = mgr_b.get_or_create("telegram:7654321") assert sess_a.user_peer_id == "alice" assert sess_b.user_peer_id == "bob" diff --git a/website/docs/user-guide/features/honcho.md b/website/docs/user-guide/features/honcho.md index 4e8caa43a..a692b26d9 100644 --- a/website/docs/user-guide/features/honcho.md +++ b/website/docs/user-guide/features/honcho.md @@ -130,8 +130,8 @@ When pointing Hermes at a self-hosted Honcho server, `hermes honcho setup` (and | `dialecticMaxInputChars` | `10000` | Max chars for dialectic query input to `peer.chat()` | | `sessionStrategy` | `'per-directory'` | `per-directory`, `per-repo`, `per-session`, or `global` | | `pinUserPeer` | `false` | Gateway only. When `true`, every platform user collapses to `peerName` | -| `userPeerAliases` | `{}` | Gateway only. Map of runtime IDs to peers (`{"86701400": "alice"}`). Many-to-one | -| `runtimePeerPrefix` | `""` | Gateway only. Namespaces unknown runtime IDs (`telegram_86701400`) when no alias matches | +| `userPeerAliases` | `{}` | Gateway only. Map of runtime IDs to peers (`{"7654321": "alice"}`). Many-to-one | +| `runtimePeerPrefix` | `""` | Gateway only. Namespaces unknown runtime IDs (`telegram_7654321`) when no alias matches | **Session strategy** controls how Honcho sessions map to your work: - `per-session` — each `hermes` run gets a fresh session. Clean starts, memory via tools. Recommended for new users.