fix(honcho): peers map reads one SDK page at a time; legacy root-level installs keep their shape

Iterating a honcho SyncPage walks every following page, so _api_workspace_peers
pulled the whole workspace on its first call, never hit the `< 50` break, re-walked
pages 2..N and returned duplicates; the 200-peer cap that exists so a public bot's
workspace cannot stall the CLI was defeated and the `p<N>` picks pointed at the
wrong peers. It now reads `.items` per page and stops at the cap; `_api_workspaces`
reads `.items` too.

The wizard's `new_host` probe looked only at the host block, so an install that
keeps peerName/enabled/workspace at the root with no hosts.hermes block read as
fresh and Enter defaulted to pinning every account onto one peer. The probe
includes the root.

`_seen_gateway_accounts` dropped rows whose origin said `is_bot`, but
SessionSource.to_dict never serializes that field, so the filter was dead; removed
with its test row. `_sanitize_peer_id` was a copy of session_peers.sanitize_peer_id.
A confirmed workspace repoint is persisted, so the exit line no longer says
"Nothing changed" after one.
This commit is contained in:
kshitijk4poor
2026-09-12 23:05:12 +05:30
committed by kshitij
parent e05f6d82ce
commit 71f3516df2
4 changed files with 69 additions and 35 deletions
+23 -29
View File
@@ -10,6 +10,7 @@ from pathlib import Path
from hermes_constants import get_hermes_home
from plugins.memory.honcho.client import _first_parsed, _host_block, profile_host_key, resolve_active_host, resolve_config_path, HOST
from plugins.memory.honcho.session_peers import sanitize_peer_id
from hermes_cli.config import cfg_get
from utils import read_json_or_empty
@@ -837,7 +838,7 @@ def _setup_wizard(args) -> None:
_migrate_pin_key(cfg) # canonicalize legacy pinPeerName before detection/writes
_migrate_pin_key(hermes_host)
# Taken before the prompts populate the block: an existing install must not default to pinning every account.
new_host = not any(k in hermes_host for k in (*_IDENTITY_MAPPING_KEYS, "peerName", "workspace", "enabled"))
new_host = not any(k in hermes_host or k in cfg for k in (*_IDENTITY_MAPPING_KEYS, "peerName", "workspace", "enabled"))
# --- 1. Cloud or local? ---
print(" Deployment:\n cloud -- Honcho cloud (api.honcho.dev)\n local -- self-hosted Honcho server")
@@ -1060,10 +1061,10 @@ def _state_db_path() -> Path:
def _seen_gateway_accounts(db_path: Path) -> list[dict]:
"""Gateway accounts recorded in state.db, most recent first; bot authors are skipped.
"""Gateway accounts recorded in state.db, most recent first.
A session row keeps only its last routing peer, so a shared thread contributes
its most recent author and not every participant.
A session row keeps only its last routing peer, so a shared thread contributes its most recent
author and not every participant. The row's origin does not record whether the author was a bot.
"""
if not db_path.exists():
return []
@@ -1096,8 +1097,6 @@ def _seen_gateway_accounts(db_path: Path) -> list[dict]:
origin = dict(json.loads(origin_json)) if origin_json else {}
except Exception:
origin = {}
if origin.get("is_bot"):
continue
accounts.append({
"platform": source or "?",
"user_id": str(user_id),
@@ -1109,11 +1108,6 @@ def _seen_gateway_accounts(db_path: Path) -> list[dict]:
return accounts
def _sanitize_peer_id(s: str) -> str:
import re
return re.sub(r'[^a-zA-Z0-9_-]', '-', s)
def _preview_peer_resolution(
user_id: str, *, pin: bool, aliases: dict, prefix: str, peer_name: str,
user_id_alt: str = "",
@@ -1167,14 +1161,13 @@ def _api_workspace_peers(client) -> list[str] | None:
return None
try:
peers: list[str] = []
page = 1
while len(peers) < _PEERS_MAP_FETCH_CAP:
batch = list(client.peers(page=page, size=50))
peers += [str(p.id) for p in batch]
if len(batch) < 50:
break
page += 1
return peers
page = client.peers(page=1, size=50)
# Iterating a SyncPage walks every following page; .items is the one page asked for.
while True:
peers += [str(p.id) for p in page.items]
if len(peers) >= _PEERS_MAP_FETCH_CAP or not page.has_next_page():
return peers[:_PEERS_MAP_FETCH_CAP]
page = page.get_next_page()
except Exception:
return None
@@ -1184,7 +1177,7 @@ def _api_workspaces(client) -> list[str] | None:
if client is None:
return None
try:
return [str(w) for w in client.workspaces(size=50)]
return [str(w) for w in client.workspaces(size=50).items]
except Exception:
return None
@@ -1217,12 +1210,12 @@ def _classify_workspace_peers(
ai = block.get("aiPeer") or cfg.get("aiPeer") or hostk
if pn:
labels.setdefault(
_sanitize_peer_id(pn),
sanitize_peer_id(pn),
"your peer (peerName)" if hostk == active_host
else f"peerName of profile {name}",
)
who = "this profile" if hostk == active_host else f"profile {name}"
labels.setdefault(_sanitize_peer_id(ai), f"AI peer · {who}")
labels.setdefault(sanitize_peer_id(ai), f"AI peer · {who}")
# Host blocks that are not Hermes profiles: other apps sharing the config.
for hostk, block in (cfg.get("hosts") or {}).items():
@@ -1231,16 +1224,16 @@ def _classify_workspace_peers(
for key, kind in (("peerName", "peer"), ("aiPeer", "AI peer")):
val = block.get(key)
if isinstance(val, str) and val.strip():
labels.setdefault(_sanitize_peer_id(val.strip()), f"{kind} of app '{hostk}'")
labels.setdefault(sanitize_peer_id(val.strip()), f"{kind} of app '{hostk}'")
for target in aliases.values():
if isinstance(target, str) and target.strip():
labels.setdefault(_sanitize_peer_id(target.strip()), "alias target")
labels.setdefault(sanitize_peer_id(target.strip()), "alias target")
for acct in accounts:
rid = acct["user_id"]
for candidate in ([rid, prefix + rid] if prefix else [rid]):
labels.setdefault(_sanitize_peer_id(candidate), f"runtime peer · {acct['platform']} {rid}")
labels.setdefault(sanitize_peer_id(candidate), f"runtime peer · {acct['platform']} {rid}")
return {
pid: labels.get(pid) or ("fallback peer (pre-identity traffic)" if pid.startswith("user-") else "unrecognized")
@@ -1420,7 +1413,7 @@ def cmd_peers_map(args) -> None:
labels = show()
print("\n Map: account number or a runtime ID · pN inspects a peer ·")
print(" w lists workspaces · blank finishes.")
changed = False
changed = repointed = False
while True:
sel = _prompt("Account (blank to finish)", default="").strip()
if not sel:
@@ -1432,6 +1425,7 @@ def cmd_peers_map(args) -> None:
if switched:
workspace, client, ws_peers = switched
labels = show()
repointed = True
continue
if low.startswith("p") and low[1:].isdigit() and ws_peers:
@@ -1467,13 +1461,13 @@ def cmd_peers_map(args) -> None:
working[rid] = entered
changed = True
print(f" {rid} → {entered} — future messages resolve to '{entered}'")
if ws_peers is not None and _sanitize_peer_id(entered) not in ws_peers:
if ws_peers is not None and sanitize_peer_id(entered) not in ws_peers:
print(f" '{entered}' is a new peer — created on first message.")
if prev_resolved in (ws_peers or ()) and prev_resolved != _sanitize_peer_id(entered):
if prev_resolved in (ws_peers or ()) and prev_resolved != sanitize_peer_id(entered):
print(f" peer '{prev_resolved}' keeps its existing history.")
if not changed:
print(" Nothing changed.\n")
print(" Aliases unchanged.\n" if repointed else " Nothing changed.\n")
return
_save_alias_map(cfg, host, working, aliases_from_root)