feat(honcho): rework the setup wizard's gateway mapping step around Honcho's peer model

The step declares its scope up front: human mapping only, with each
Hermes profile bringing its own AI peer. A note explains aliases as
the join between platform accounts and named peers. Each shape now
says when it fits. Fresh configs default the choice to [1] single
peer — the common personal setup — instead of [3], which silently
fragmented a solo operator's gateway account away from their
peerName history. Configured setups keep their detected shape as
the default.
This commit is contained in:
Erosika
2026-08-10 19:59:45 -04:00
committed by kshitij
parent c3901f7611
commit 21180ae7e8
2 changed files with 54 additions and 16 deletions
+32 -16
View File
@@ -494,34 +494,50 @@ def _setup_identity_mapping(cfg: dict, hermes_host: dict, current_peer: str) ->
print(f"\n Gateway platforms detected: {', '.join(gw_platforms)}")
else:
notice, question = (
("\n Gateway identity mapping routes platform users to memory peers.",
("\n Each gateway account (a Telegram user, a Discord user, ...)\n"
" resolves to a peer. Honcho builds one representation per peer.",
"Running the Hermes gateway (Telegram/Discord/etc.)? (y/N)") if gw_platforms is None else
("\n No gateway platforms connected — identity mapping only affects\n"
" gateway users, so this step doesn't apply here.", "Configure gateway mapping anyway? (y/N)"))
("\n No gateway platforms connected — nothing to map.", "Configure anyway? (y/N)"))
print(notice)
if not _yes(_prompt(question, default="n")):
return
peer_target = hermes_host.get("peerName") or current_peer or "user"
default_choice = {"single": "1", "hybrid": "2"}.get(current_shape, "3")
print("\n How should gateway users map to memory peers?\n"
" [1] just me — every non-agent user collapses to your peer\n"
" [2] me + other people — keep mine pooled, others separate\n"
" [3] only other people — everyone gets their own peer\n"
" [s] skip (leave untouched) [e] edit raw keys")
ai_peer_label = hermes_host.get("aiPeer") or cfg.get("aiPeer") or "hermes"
# Fresh configs default to the personal shape; configured ones keep their detected shape.
identity_configured = any(k in hermes_host or k in cfg for k in _IDENTITY_MAPPING_KEYS)
default_choice = {"single": "1", "hybrid": "2", "multi": "3"}[current_shape] if identity_configured else "1"
print("\n This step covers the HUMAN mapping only. Each account using the\n"
" gateway resolves to a peer — the entity Honcho reasons about over\n"
f" time. This agent is already its own peer ('{ai_peer_label}'), and each\n"
" Hermes profile brings its own AI peer to the gateway.\n"
"\n How should accounts resolve?\n"
" [1] single peer — one person uses this agent; every account\n"
f" resolves to '{peer_target}'. The common personal setup.\n"
" Never for a gateway serving other people — their memory\n"
" would merge into yours\n"
" [2] your peer + one per other account — your accounts are\n"
f" aliased to '{peer_target}'; each other account gets its own\n"
" peer until you alias it. For a gateway you share\n"
" [3] one peer per account — no aliases; every account is its\n"
" own peer. For agents serving other people\n"
" [s] skip (leave untouched) [e] edit raw keys\n"
f"\n Tip: alias your Telegram UID and your Discord ID to '{peer_target}' —\n"
" both accounts then resolve to one peer.")
choice = _prompt("Choice", default=default_choice).strip().lower()
if choice in {"2", "me+others", "both"}:
pooled = _prompt(" Keep my own memory pooled across platforms? (Y/n)", default="y").strip().lower()
pooled = _prompt(" Resolve all YOUR accounts to one peer? (Y/n)", default="y").strip().lower()
shape = "hybrid" if pooled in {"y", "yes", ""} else "multi"
else:
shape = _SHAPE_CHOICES.get(choice, "skip")
# Un-pinning without aliasing strands the pooled peerName history; steer toward pooling.
if current_pin and shape == "multi":
print(f"\n ⚠ Un-pinning will orphan memory accumulated under peer\n"
f" '{peer_target}'. Existing gateway users resolve to fresh,\n empty peers.")
if _prompt(" Pool my own memory instead (alias my IDs to peerName)? (Y/n)", default="y").strip().lower() in {"y", "yes", ""}:
print(f"\n ⚠ The peer '{peer_target}' already has a representation built\n"
f" from your messages. One peer per account means your accounts\n"
f" resolve to new peers with no history.")
if _prompt(f" Keep your accounts resolving to '{peer_target}' instead? (Y/n)", default="y").strip().lower() in {"y", "yes", ""}:
shape = "hybrid"
if shape == "skip":
@@ -538,7 +554,7 @@ def _setup_identity_mapping(cfg: dict, hermes_host: dict, current_peer: str) ->
_scrub_identity_mapping(hermes_host) # each shape starts from a clean slate
hermes_host["pinUserPeer"] = shape == "single"
if shape == "single":
print(f" All non-agent gateway users route to '{peer_target}' (pin overrides aliases).")
print(f" Every gateway account resolves to peer '{peer_target}'.")
else:
aliases = prior_aliases if shape == "multi" else _collect_operator_aliases(prior_aliases, peer_target)
if aliases:
@@ -546,8 +562,8 @@ def _setup_identity_mapping(cfg: dict, hermes_host: dict, current_peer: str) ->
_apply_runtime_prefix(hermes_host, current_prefix, prefix_from_root,
"Runtime peer prefix (e.g. 'telegram_', blank for none)" if shape == "multi" else
"Runtime peer prefix for unknown users (e.g. 'telegram_', blank for none)")
print(" Each gateway user → own peer." if shape == "multi" else
f" Your runtime IDs → '{peer_target}', others → own peer.")
print(" Each gateway account resolves to its own peer." if shape == "multi" else
f" Your accounts resolve to '{peer_target}'; each other account to its own peer.")
_echo_identity_mapping(hermes_host)
+22
View File
@@ -539,6 +539,28 @@ class TestSetupWizardDeploymentShape:
# operator edits live on the host block they're inspecting.
assert host["userPeerAliases"] == {"7654321": "eri"}
def test_fresh_config_defaults_to_single(self, monkeypatch, tmp_path):
"""No identity key anywhere → the choice prompt defaults to [1].
A solo operator pressing Enter gets the pinned personal shape
instead of silently fragmenting their gateway account away from
peerName's history.
"""
answers = ["cloud", "", "eri", "hermetika", "hermes"]
host = self._run_setup(monkeypatch, tmp_path, answers=answers)
assert host["pinUserPeer"] is True
def test_configured_multi_still_defaults_to_multi(self, monkeypatch, tmp_path):
"""An explicit pinUserPeer: false config keeps [3] as its default —
the fresh-config [1] default must not override a chosen shape."""
initial_cfg = {
"apiKey": "***",
"hosts": {"hermes": {"pinUserPeer": False, "peerName": "eri"}},
}
answers = ["cloud", "", "eri", "hermetika", "hermes"]
host = self._run_setup(monkeypatch, tmp_path, answers=answers, initial_cfg=initial_cfg)
assert host["pinUserPeer"] is False
def test_no_gateway_connected_skips_mapping_when_declined(self, monkeypatch, tmp_path):
"""With no gateway platforms connected, the tree is gated off; declining