# NousResearch/hermes-agent · Honcho memory provider cache and setup hardening

## Open now

- Nothing open on GitHub.

These items address refactoring cache-busting logic into the memory provider layer, skipping unnecessary disk reads when Honcho is inactive, and hardening self-hosted Honcho setup paths and peer mapping.

## TL;DR

- 7 member(s): 0 open issue(s), 2 closed issue(s), 0 open PR(s), 3 merged PR(s), 2 PR(s) closed without merging.
- A fix was merged to main in PR #30077. Verify it addresses your case.
- **Recommended:** read PR #30077 (and any competing PRs below) before filing. Confirm the merged change covers your symptom.

## Maintainer resolution verdict

- Internal-tracker verdict over this cluster's issue members: `resolved` · deciding PR(s): `pr-30077,pr-107547`. This is distinct from the fix status above. Weigh it yourself.

## How this cluster was grouped

- Link evidence (`STRUCTURAL_PARTIAL`): some members are connected by structural evidence (shared changed files, or a closing/superseded reference); the rest are joined by an automated relevance judgement over their text.
- Members outside that structural core may belong to a different underlying problem.
- Pairwise link density `0.26`: the share of member pairs that carry a link, counting a structural link in full and a relevance-only link at 0.36. `1.00` means every pair is linked.

## Closed / duplicate issues

- #33381: memory: move provider-specific cache-busting keys out of gateway/run.py into a MemoryProvider hook
- #33382: honcho: skip per-gateway-message disk read in _extract_cache_busting_config when honcho isn't the active memory provider

## Prior & competing PRs

- **#30077: fix(honcho): runtime peer mapping — correctness follow-ups + setup wizard + docs**. merged to main · fix tier `verified`
  - maintainer direction: category `merged`; implication `superseded`
  - files: `agent/agent_init.py`, `agent/memory_provider.py`, `gateway/run.py`, `plugins/memory/honcho/README.md`, `plugins/memory/honcho/__init__.py`, `plugins/memory/honcho/cli.py`, `plugins/memory/honcho/client.py`, `plugins/memory/honcho/session.py`, `run_agent.py`, `scripts/release.py`, `tests/gateway/test_agent_cache.py`, `tests/honcho_plugin/test_cli.py`, `tests/honcho_plugin/test_pin_peer_name.py`, `tests/honcho_plugin/test_session.py`, `tests/run_agent/test_memory_provider_init.py`, `tests/test_honcho_session_context.py`
  - fix hunks:
  - `agent/agent_init.py`

    ```diff
    @@ -183,6 +183,7 @@ def init_agent(
         prefill_messages: List[Dict[str, Any]] = None,
         platform: str = None,
         user_id: str = None,
    +    user_id_alt: str = None,
         user_name: str = None,
         chat_id: str = None,
         chat_name: str = None,
    ```

    ```diff
    @@ -265,6 +266,7 @@ def init_agent(
         agent.ephemeral_system_prompt = ephemeral_system_prompt
         agent.platform = platform  # "cli", "telegram", "discord", "whatsapp", etc.
         agent._user_id = user_id  # Platform user identifier (gateway sessions)
    +    agent._user_id_alt = user_id_alt  # Optional stable alternate platform identifier
         agent._user_name = user_name
         agent._chat_id = chat_id
         agent._chat_name = chat_name
    ```

    ```diff
    @@ -1089,6 +1091,8 @@ def init_agent(
                         # Thread gateway user identity for per-user memory scoping
                         if agent._user_id:
                             _init_kwargs["user_id"] = agent._user_id
    +                    if agent._user_id_alt:
    +                        _init_kwargs["user_id_alt"] = agent._user_id_alt
                         if agent._user_name:
                             _init_kwargs["user_name"] = agent._user_name
                         if agent._chat_id:
    ```
  - `agent/memory_provider.py`

    ```diff
    @@ -78,6 +78,7 @@ def initialize(self, session_id: str, **kwargs) -> None:
               - agent_workspace (str): Shared workspace name (e.g. "hermes").
               - parent_session_id (str): For subagents, the parent's session_id.
               - user_id (str): Platform user identifier (gateway sessions).
    +          - user_id_alt (str): Optional alternate stable platform user identifier.
             """
     
         def system_prompt_block(self) -> str:
    ```
  - `gateway/run.py`

    ```diff
    @@ -11436,6 +11436,7 @@ def run_sync():
                         session_id=task_id,
                         platform=platform_key,
                         user_id=source.user_id,
    +                    user_id_alt=source.user_id_alt,
                         user_name=source.user_name,
                         chat_id=source.chat_id,
                         chat_name=source.chat_name,
    ```

    ```diff
    @@ -14672,6 +14673,29 @@ def _extract_cache_busting_config(cls, user_config: dict | None) -> dict:
                 out["tools.registry_generation"] = getattr(registry, "_generation", None)
             except Exception:
                 out["tools.registry_generation"] = None
    +
    +        # Honcho identity-mapping keys live in honcho.json, not user_config.
    +        # HonchoSessionManager freezes the resolved peer_name / ai_peer /
    +        # pin / aliases / prefix at construction; without busting here,
    +        # mid-flight honcho.json edits go unread until the next unrelated
    +        # cache eviction.
    +        try:
    +            from plugins.memory.honcho.client import HonchoClientConfig
    +
    +            hcfg = HonchoClientConfig.from_global_config()
    +            out["honcho.peer_name"] = hcfg.peer_name
    +            out["honcho.ai_peer"] = hcfg.ai_peer
    +            out["honcho.pin_peer_name"] = bool(hcfg.pin_peer_name)
    +            out["honcho.runtime_peer_prefix"] = hcfg.runtime_peer_prefix or ""
    +            aliases = hcfg.user_peer_aliases or {}
    +            out["honcho.user_peer_aliases"] = sorted(aliases.items()) if isinstance(aliases, dict) else []
    +        except Exception:
    +            out["honcho.peer_name"] = None
    +            out["honcho.ai_peer"] = None
    +            out["honcho.pin_peer_name"] = None
    +            out["honcho.runtime_peer_prefix"] = None
    +            out["honcho.user_peer_aliases"] = None
    +
             return out
     
         @staticmethod
    ```

    ```diff
    @@ -14681,6 +14705,8 @@ def _agent_config_signature(
             enabled_toolsets: list,
             ephemeral_prompt: str,
             cache_keys: dict | None = None,
    +        user_id: str | None = None,
    +        user_id_alt: str | None = None,
         ) -> str:
             """Compute a stable string key from agent config values.
     
    ```

    ```diff
    @@ -14694,6 +14720,20 @@ def _agent_config_signature(
             the output of ``_extract_cache_busting_config(user_config)`` so
             edits to model.context_length / compression.* in config.yaml are
             picked up on the next gateway message without a manual restart.
    +
    +        ``user_id`` and ``user_id_alt`` are the runtime user identities
    +        carried by the current message's gateway source.  They participate
    +        in the cache key because the Honcho memory provider freezes them
    +        into ``HonchoSessionManager`` at first-message init (see
    +        ``plugins/memory/honcho/__init__.py::_do_session_init``).  Without
    +        them in the signature, a shared-thread session_key (one in which
    +        ``build_session_key`` intentionally omits the participant ID,
    +        e.g. ``thread_sessions_per_user=False``) would reuse the cached
    +        AIAgent across distinct users, causing the second user's messages
    +        to be attributed to the first user's resolved Honcho peer.  This
    +        broke #27371's per-user-peer contract in multi-user gateways.
    +        Per-user agent rebuilds in shared threads trade prompt-cache
    +        warmth for correct memory attribution.
             """
             import hashlib, json as _j
     
    ```

    ```diff
    @@ -14718,6 +14758,8 @@ def _agent_config_signature(
                     # cached agent and doesn't affect system prompt or tools.
                     ephemeral_prompt or "",
                     _cache_keys_sorted,
    +                str(user_id or ""),
    +                str(user_id_alt or ""),
                 ],
                 sort_keys=True,
                 default=str,
    ```

    ```diff
    @@ -16259,6 +16301,8 @@ def _interim_assistant_cb(text: str, *, already_streamed: bool = False) -> None:
                     enabled_toolsets,
                     combined_ephemeral,
                     cache_keys=self._extract_cache_busting_config(user_config),
    +                user_id=getattr(source, "user_id", None),
    +                user_id_alt=getattr(source, "user_id_alt", None),
                 )
                 agent = None
                 _cache_lock = getattr(self, "_agent_cache_lock", None)
    ```

    ```diff
    @@ -16302,6 +16346,7 @@ def _interim_assistant_cb(text: str, *, already_streamed: bool = False) -> None:
                         session_id=session_id,
                         platform=platform_key,
                         user_id=source.user_id,
    +                    user_id_alt=source.user_id_alt,
                         user_name=source.user_name,
                         chat_id=source.chat_id,
                         chat_name=source.chat_name,
    ```
  - `plugins/memory/honcho/README.md`

    ```diff
    @@ -127,6 +127,41 @@ For every key, resolution order is: **host block > root > env var > default**.
     | `peerName` | string | — | User peer identity |
     | `aiPeer` | string | host key | AI peer identity |
     
    +### Identity Mapping (Gateway Multi-User)
    +
    +In gateway deployments (Telegram, Discord, Slack, etc.) each user arrives with a platform-native runtime ID (Telegram UID, Discord snowflake, Slack user). These three keys control how those runtime IDs map to Honcho peers. The resolver is config-driven and deterministic — no automatic merging or runtime inference.
    +
    +| 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. Also accepted as `pinPeerName` |
    +| `pinPeerName` | bool | `false` | Alias for `pinUserPeer`; same effect |
    +| `userPeerAliases` | object | `{}` | Map of runtime IDs to peer IDs (`{"86701400": "eri"}`). 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 |
    +
    +**Resolver ladder** (first match wins):
    +
    +```
    +1. pinUserPeer / pinPeerName=true → return peerName (ignore runtime ID)
    +2. userPeerAliases[runtime_id]   → return aliased peer
    +3. userPeerAliases[runtime_id_alt] → check alt-ID too (Telegram UID + username, etc.)
    +4. runtimePeerPrefix + runtime_id → namespaced peer, with sha256 collision escalation
    +5. raw sanitized runtime_id      → fallback peer
    +6. peerName                      → no runtime ID at all (CLI/TUI)
    +7. session-key fallback          → no config either
    +```
    +
    +**Why no `pinAiPeer`?** The AI peer is already pinned by construction — `aiPeer` is the only AI-side identity setting and the resolver never overrides it. Only the user-side peer has the runtime-vs-config tension that `pinUserPeer` resolves.
    +
    +**Host vs root semantics.** All three keys are accepted at both root and `hosts.<host>` levels. Host-level wins. For maps and prefixes, host-level *replaces* the root value as a whole (not merge), so a host can intentionally own its identity universe or wipe it with `userPeerAliases: {}` / `runtimePeerPrefix: ""`.
    +
    +**Deployment shapes** (`hermes honcho setup` asks one prompt to set these):
    +
    +- **Single-operator** — `pinUserPeer: true`. All gateway users → `peerName`. Recommended for personal use where you connect Hermes to your own Telegram/Discord/etc.
    +- **Multi-user gateway** — `pinUserPeer: false`, optional `runtimePeerPrefix`. Each runtime user → own peer. Recommended for bots serving many humans.
    +- **Hybrid** — `pinUserPeer: false`, `userPeerAliases` mapping the operator's runtime IDs to `peerName`. Multi-user gateway where YOU are routed but others stay distinct.
    +
    +**Migrating single → multi.** Flipping `pinUserPeer` from `true` to `false` does not migrate data. Memory accumulated under `peerName` while pinned stays there; runtime users now resolve to fresh, empty peers. To preserve your own continuity, use the **hybrid** shape — alias your runtime IDs back to `peerName` so your turns keep landing on the pooled history while other users get their own peers. The setup wizard offers this path automatically when it detects a single → multi transition.
    +
     ### Memory & Recall
     
     | Key | Type | Default | Description |
    ```
  - `plugins/memory/honcho/__init__.py`

    ```diff
    @@ -321,10 +321,8 @@ def initialize(self, session_id: str, **kwargs) -> None:
                 except Exception as e:
                     logger.debug("Honcho cost-awareness config parse error: %s", e)
     
    -            # ----- Port #1969: aiPeer sync from SOUL.md — REMOVED -----
    -            # SOUL.md is persona content, not identity config. aiPeer should
    -            # only come from honcho.json (host block or root) or the default.
    -            # See scratch/memory-plugin-ux-specs.md #10 for rationale.
    +            # aiPeer comes from honcho.json (host block or root) only.
    +            # SOUL.md is persona content, not identity config.
     
                 # ----- Port #1957: lazy session init for tools-only mode -----
                 if self._recall_mode == "tools":
    ```

    ```diff
    @@ -360,6 +358,7 @@ def _do_session_init(self, cfg, session_id: str, **kwargs) -> None:
                 config=cfg,
                 context_tokens=[redacted:secret-assignment]
                 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 -----
    ```
  - `plugins/memory/honcho/cli.py`

    ```diff
    @@ -40,12 +40,20 @@ def clone_honcho_for_profile(profile_name: str) -> bool:
         if new_host in hosts:
             return False  # already exists
     
    -    # Clone settings from default block, override identity fields
    +    # Clone settings from default block, override identity fields.
    +    # Identity-mapping keys (pinPeerName/pinUserPeer, userPeerAliases,
    +    # runtimePeerPrefix) carry the operator's runtime-to-peer routing
    +    # intent from #27371.  Both pin keys are inherited because
    +    # HonchoClientConfig prefers pinUserPeer over pinPeerName — leaving
    +    # the canonical key off this allowlist silently drops the pin on
    +    # cloned profiles when the default uses the newer name.
         new_block = {}
         for key in ("recallMode", "writeFrequency", "sessionStrategy",
                     "sessionPeerPrefix", "contextTokens", "dialecticReasoningLevel",
                     "dialecticDynamic", "dialecticMaxChars", "messageMaxChars",
    -                "dialecticMaxInputChars", "saveMessages", "observation"):
    +                "dialecticMaxInputChars", "saveMessages", "observation",
    +                "pinPeerName", "pinUserPeer", "userPeerAliases",
    +                "runtimePeerPrefix"):
             val = default_block.get(key)
             if val is not None:
                 new_block[key] = val
    ```

    ```diff
    @@ -308,6 +316,72 @@ def _resolve_api_key(cfg: dict) -> str:
         return key
     
     
    +_IDENTITY_MAPPING_KEYS = (
    +    "pinPeerName",
    +    "pinUserPeer",
    +    "userPeerAliases",
    +    "runtimePeerPrefix",
    +)
    +
    +
    +def _resolve_effective_identity_mapping(
    +    cfg: dict, hermes_host: dict
    +) -> tuple[bool, dict, str, bool, bool]:
    +    """Resolve the effective identity-mapping state for the active host.
    +
    +    Matches the precedence used by ``HonchoClientConfig.from_global_config``
    +    so the wizard reads the same shape the gateway will actually run with.
    +    Without this, root-level overrides and ``pinUserPeer`` (which wins over
    +    ``pinPeerName`` at the same level) are invisible to detection, letting
    +    setup mis-classify the current shape and silently change effective
    +    routing on the next save.
    +
    +    Returns ``(pin, aliases, prefix, aliases_from_root, prefix_from_root)``.
    +    The ``*_from_root`` flags let the write step skip touching host keys
    +    whose value is actually inherited.
    +    """
    +    pin = False
    +    for val in (
    +        hermes_host.get("pinUserPeer"),
    +        hermes_host.get("pinPeerName"),
    +        cfg.get("pinUserPeer"),
    +        cfg.get("pinPeerName"),
    +    ):
    +        if val is not None:
    +            pin = bool(val)
    +            break
    +
    +    if "userPeerAliases" in hermes_host:
    +        aliases_src = hermes_host.get("userPeerAliases")
    +        aliases_from_root = False
    +    else:
    +        aliases_src = cfg.get("userPeerAliases")
    +        aliases_from_root = aliases_src is not None
    +    aliases = aliases_src if isinstance(aliases_src, dict) else {}
    +
    +    if "runtimePeerPrefix" in hermes_host:
    +        prefix_src = hermes_host.get("runtimePeerPrefix")
    +        prefix_from_root = False
    +    else:
    +        prefix_src = cfg.get("runtimePeerPrefix")
    +        prefix_from_root = prefix_src is not None
    +    prefix = str(prefix_src or "")
    +
    +    return pin, aliases, prefix, aliases_from_root, prefix_from_root
    +
    +
    +def _scrub_identity_mapping(hermes_host: dict) -> None:
    +    """Drop every peer-mapping key from the host block.
    +
    +    Called before the wizard writes a chosen shape so latent precedence
    +    conflicts can't survive — e.g. a stray host ``pinUserPeer: false``
    +    that would silently outrank a freshly written ``pinPeerName: true``
    +    (host ``pinUserPeer`` is first in the resolver ladder).
    +    """
    +    for key in _IDENTITY_MAPPING_KEYS:
    +        hermes_host.pop(key, None)
    +
    +
     def _prompt(label: str, default: str | None = None, secret: bool = False) -> str:
         suffix = f" [{default}]" if default else ""
         sys.stdout.write(f"  {label}{suffix}: ")
    ```

    ```diff
    @@ -435,6 +509,131 @@ def cmd_setup(args) -> None:
         if new_workspace:
             hermes_host["workspace"] = new_workspace
     
    +    # --- 3b. Deployment shape ---
    +    # Determines how runtime user identities (Telegram UIDs, Discord
    +    # snowflakes, etc.) map to Honcho peers in gateway sessions.  Three
    +    # shapes cover the realistic deployments; each writes a different
    +    # combination of pinPeerName / userPeerAliases / runtimePeerPrefix.
    +    # See plugins/memory/honcho/README.md for the resolver ladder.
    +    #
    +    # Detection must mirror the gateway resolver: root-level config and
    +    # ``pinUserPeer`` (which outranks ``pinPeerName`` at the same level)
    +    # both affect effective routing, so reading host-only fields would
    +    # mis-classify a profile that inherits its mapping from root or uses
    +    # the newer canonical key.
    +    (
    +        current_pin,
    +        current_aliases,
    +        current_prefix,
    +        aliases_from_root,
    +        prefix_from_root,
    +    ) = _resolve_effective_identity_mapping(cfg, hermes_host)
    +
    +    if current_pin:
    +        current_shape = "single"
    +    elif current_aliases:
    +        current_shape = "hybrid"
    +    else:
    +        current_shape = "multi"
    +
    +    print("\n  Deployment shape (how gateway users map to peers):")
    +    print("    single -- all platforms route to your peer (recommended for personal use)")
    +    print("    multi  -- each platform user gets their own peer (multi-user bots)")
    +    print("    hybrid -- multi-user, but YOUR runtime IDs alias to your peer")
    +    print("    skip   -- don't touch identity-mapping config")
    +    new_shape = _prompt("Deployment shape", default=current_shape).strip().lower()
    +
    +    # Transitioning single → multi orphans the peerName pool for runtime users
    +    # (their resolved peers go from peerName to runtime-derived IDs with empty
    +    # history).  Steer the operator toward hybrid so their own continuity is
    +    # preserved via alias mappings.
    +    if current_shape == "single" and new_shape == "multi":
    +        peer_target = hermes_host.get("peerName") or current_peer or "user"
    +        print(
    +            f"\n  ⚠ Switching from single to multi will orphan memory accumulated\n"
    +            f"    under peer '{peer_target}'.  Existing runtime users (Telegram,\n"
    +            f"    Discord, etc.) will resolve to fresh, empty peers."
    +        )
    +        print("    To keep your own continuity, choose 'hybrid' and alias your\n"
    +              "    runtime IDs back to peerName.")
    +        confirm = _prompt("Continue with multi anyway? (yes/hybrid/no)", default="hybrid").strip().lower()
    +        if confirm in {"hybrid", "h"}:
    +            new_shape = "hybrid"
    +        elif confirm not in {"yes", "y"}:
    +            new_shape = "skip"
    +
    +    # Each shape branch scrubs every peer-mapping key before writing its own,
    +    # so a stale ``pinUserPeer`` left behind by an earlier setup run can't
    +    # outrank the freshly written ``pinPeerName`` via host-level precedence.
    +    if new_shape == "single":
    +        _scrub_identity_mapping(hermes_host)
    +        hermes_host["pinPeerName"] = True
    +        print(f"  pinPeerName=true → all gateway users route to '{hermes_host.get('peerName', '?')}'.")
    +    elif new_shape == "multi":
    +        # Preserve operator-curated, host-level aliases so multi → multi
    +        # re-runs don't drop them.  Root-sourced aliases are left to
    +        # cascade naturally and are NOT copied down into the host.
    +        prior_aliases = (
    +            dict(current_aliases)
    +            if isinstance(current_aliases, dict) and not aliases_from_root
    +            else {}
    +        )
    +        _scrub_identity_mapping(hermes_host)
    +        hermes_host["pinPeerName"] = False
    +        # Do NOT auto-write ``userPeerAliases: {}``: an empty host map
    +        # would override any root-level ``userPeerAliases`` the operator
    +        # set as a cross-host baseline, silently disabling those aliases.
    +        # Absence is the right "no host opinion" signal.
    +        if prior_aliases:
    +            hermes_host["userPeerAliases"] = prior_aliases
    +        _prefix_default = current_prefix or ""
    +        _new_prefix = _prompt(
    +            "Runtime peer prefix (e.g. 'telegram_', blank for none)",
    +            default=_prefix_default,
    +        ).strip()
    +        # Only write a host-level prefix when the operator typed one that
    +        # diverges from the inherited root value; otherwise let the root
    +        # cascade continue unmodified.
    +        if _new_prefix and not (prefix_from_root and _new_prefix == current_prefix):
    +            hermes_host["runtimePeerPrefix"] = _new_prefix
    +        print("  Multi-user mode: each runtime ID → own peer. Use 'hermes honcho status' to inspect.")
    +    elif new_shape == "hybrid":
    +        # Hybrid encodes operator intent at the host level: collect existing
    +        # entries (host or root) so the wizard never silently drops a known
    +        # alias, then write the combined map.  Materialising root entries
    +        # into the host is the right move here — once the operator answers
    +        # the alias prompts for a host, they're declaring "this host owns
    +        # the mapping".
    +        existing_aliases = dict(current_aliases) if isinstance(current_aliases, dict) else {}
    +        _scrub_identity_mapping(hermes_host)
    +        hermes_host["pinPeerName"] = False
    +        peer_target = hermes_host.get("peerName") or current_peer or "user"
    +        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"),
    +            ("Discord snowflake", "e.g. 491827364"),
    +            ("Slack user ID", "e.g. U04ABCDEF"),
    +            ("Matrix MXID", "e.g. @you:matrix.org"),
    +        ):
    +            entered = _prompt(f"  {platform_label} ({alias_hint})", default="").strip()
    +            if entered:
    +                existing_aliases[entered] = peer_target
    +        if existing_aliases:
    +            hermes_host["userPeerAliases"] = existing_aliases
    +        _prefix_default = current_prefix or ""
    +        _new_prefix = _prompt(
    +            "Runtime peer prefix for unknown users (e.g. 'telegram_', blank for none)",
    +            default=_prefix_default,
    +        ).strip()
    +        if _new_prefix and not (prefix_from_root and _new_prefix == current_prefix):
    +            hermes_host["runtimePeerPrefix"] = _new_prefix
    +        print(f"  Hybrid mode: your runtime IDs → '{peer_target}', others → own peer.")
    +    elif new_shape == "skip":
    +        pass  # leave config untouched
    +    else:
    +        print(f"  Unknown shape '{new_shape}' — leaving identity-mapping config untouched.")
    +
         # --- 4. Observation mode ---
         current_obs = hermes_host.get("observationMode") or cfg.get("observationMode", "directional")
         print("\n  Observation mode:")
    ```
  - `plugins/memory/honcho/client.py`

    ```diff
    @@ -91,12 +91,17 @@ def _normalize_recall_mode(val: str) -> str:
         return val if val in _VALID_RECALL_MODES else "hybrid"
     
     
    -def _resolve_bool(host_val, root_val, *, default: bool) -> bool:
    -    """Resolve a bool config field: host wins, then root, then default."""
    -    if host_val is not None:
    -        return bool(host_val)
    -    if root_val is not None:
    -        return bool(root_val)
    +def _resolve_bool(*vals, default: bool) -> bool:
    +    """Resolve a bool config field: first non-None wins, else default.
    +
    +    Variadic to support aliased keys (e.g. ``pinUserPeer`` shadowing
    +    ``pinPeerName`` for backwards compatibility).  Pass values in
    +    precedence order: caller's preferred alias first, then fallback
    +    aliases, in (host, root) interleaving as needed.
    +    """
    +    for val in vals:
    +        if val is not None:
    +            return bool(val)
         return default
     
     
    ```

    ```diff
    @@ -122,6 +127,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):
    ```

    ```diff
    @@ -259,6 +292,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
    ```

    ```diff
    @@ -454,10 +493,28 @@ def from_global_config(
                 peer_name=host_block.get("peerName") or raw.get("peerName"),
                 ai_peer=ai_peer,
                 pin_peer_name=_resolve_bool(
    +                # ``pinUserPeer`` is the clearer name (the resolver pins
    +                # the user-side peer to ``peerName``, ignoring runtime
    +                # identity).  ``pinPeerName`` is the original key from
    +                # #14984 and stays accepted for backward compatibility.
    +                # Host-level keys win over root-level; among same-level
    +                # keys, ``pinUserPeer`` wins over ``pinPeerName``.
    +                host_block.get("pinUserPeer"),
                     host_block.get("pinPeerName"),
    +                raw.get("pinUserPeer"),
                     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,
    ```
  - `plugins/memory/honcho/session.py`

    ```diff
    @@ -2,6 +2,7 @@
     
     from __future__ import annotations
     
    +import hashlib
     import queue
     import re
     import logging
    ```

    ```diff
    @@ -19,6 +20,8 @@
     
     # 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
    ```

    ```diff
    @@ -79,6 +82,7 @@ def __init__(
             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.
    ```

    ```diff
    @@ -89,11 +93,13 @@ def __init__(
                 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 = [redacted:secret-assignment]
             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] = {}
    ```

    ```diff
    @@ -267,6 +273,90 @@ def _sanitize_id(self, id_str: str) -> str:
             """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 _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 = (
    +            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._generated_runtime_peer_id(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.
    ```

    ```diff
    @@ -285,31 +375,11 @@ def get_or_create(self, key: str) -> HonchoSession:
             # 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"
    ```

    ```diff
    @@ -937,11 +1007,11 @@ def get_session_context(self, session_key: str, peer: str = "user") -> dict[str,
                 return self._fetch_peer_context(peer_id, target=peer_id)
     
             try:
    -            peer_id = self._resolve_peer_id(session, peer)
    +            observer_peer_id, target_peer_id = self._resolve_observer_target(session, peer)
                 ctx = honcho_session.context(
                     summary=True,
    -                peer_target=peer_id,
    -                peer_perspective=session.user_peer_id if peer == "user" else session.assistant_peer_id,
    +                peer_target=target_peer_id or observer_peer_id,
    +                peer_perspective=observer_peer_id,
                 )
     
                 result: dict[str, Any] = {}
    ```

    ```diff
    @@ -1017,7 +1087,14 @@ def get_peer_card(self, session_key: str, peer: str = "user") -> list[str]:
     
             try:
                 observer_peer_id, target_peer_id = self._resolve_observer_target(session, peer)
    -            return self._fetch_peer_card(observer_peer_id, target=target_peer_id)
    +            card = self._fetch_peer_card(observer_peer_id, target=target_peer_id)
    +            if card:
    +                return card
    +            # Some backends store cards directly on the target peer, not the
    +            # observer-target slot. Fall back so honcho_profile still works.
    +            if target_peer_id:
    +                return self._fetch_peer_card(target_peer_id)
    +            return []
             except Exception as e:
                 logger.debug("Failed to fetch peer card from Honcho: %s", e)
                 return []
    ```

    ```diff
    @@ -1164,13 +1241,22 @@ def set_peer_card(self, session_key: str, card: list[str], peer: str = "user") -
             if not session:
                 return None
             try:
    -            peer_id = self._resolve_peer_id(session, peer)
    -            if peer_id is None:
    +            observer_peer_id, target_peer_id = self._resolve_observer_target(session, peer)
    +            if observer_peer_id is None:
                     logger.warning("Could not resolve peer '%s' for set_peer_card in session '%s'", peer, session_key)
                     return None
    -            peer_obj = self._get_or_create_peer(peer_id)
    -            result = peer_obj.set_card(card)
    -            logger.info("Updated peer card for %s (%d facts)", peer_id, len(card))
    +            peer_obj = self._get_or_create_peer(observer_peer_id)
    +            result = (
    +                peer_obj.set_card(card, target=target_peer_id)
    +                if target_peer_id is not None
    +                else peer_obj.set_card(card)
    +            )
    +            logger.info(
    +                "Updated peer card observer=%s target=%s (%d facts)",
    +                observer_peer_id,
    +                target_peer_id or observer_peer_id,
    +                len(card),
    +            )
                 return result
             except Exception as e:
                 logger.error("Failed to set peer card: %s", e)
    ```
  - `run_agent.py`

    ```diff
    @@ -393,6 +393,7 @@ def __init__(
             prefill_messages: List[Dict[str, Any]] = None,
             platform: str = None,
             user_id: str = None,
    +        user_id_alt: str = None,
             user_name: str = None,
             chat_id: str = None,
             chat_name: str = None,
    ```

    ```diff
    @@ -462,6 +463,7 @@ def __init__(
                 prefill_messages=prefill_messages,
                 platform=platform,
                 user_id=user_id,
    +            user_id_alt=user_id_alt,
                 user_name=user_name,
                 chat_id=chat_id,
                 chat_name=chat_name,
    ```
  - `scripts/release.py`

    ```diff
    @@ -213,6 +213,8 @@
         "[redacted:email]": "maciekczech",
         "[redacted:email]": "LeonSGP43",
         "[redacted:email]": "LeonSGP43",
    +    "[redacted:email]": "cyb0rgk1tty",
    +    "[redacted:email]": "cmullins70",
         "[redacted:email]": "zeejaytan",
         "[redacted:email]": "Aslaaen",
         "[redacted:email]": "Tkander1715",
    ```
  - `tests/gateway/test_agent_cache.py`

    ```diff
    @@ -1344,3 +1344,71 @@ def test_watchdog_accumulation_across_recursive_turns(self):
                 f"Watchdog would see {idle_secs:.0f}s idle, expected ~{STUCK_FOR}s. "
                 "Inactivity timeout could not fire for a stuck interrupted turn."
             )
    +
    +
    +class TestAgentConfigSignatureUserId:
    +    """Shared-thread cache must not reuse an agent across users.
    +
    +    HonchoSessionManager freezes the resolved runtime user identity at
    +    first-message init.  When the gateway session_key omits the participant
    +    ID (``thread_sessions_per_user=False``), a cached AIAgent created by
    +    user A would otherwise be reused for user B, attributing B's writes to
    +    A's resolved peer.  Including ``user_id`` / ``user_id_alt`` in the
    +    signature forces per-user agent builds in shared threads.
    +
    +    Tradeoff: cold prompt cache for each user's first turn in a shared
    +    thread, in exchange for correct memory attribution.
    +    """
    +
    +    def test_signature_changes_with_user_id(self):
    +        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"
    +        )
    +        sig_b = GatewayRunner._agent_config_signature(
    +            "claude-sonnet-4", runtime, ["hermes-telegram"], "", user_id="491827364"
    +        )
    +        assert sig_a != sig_b
    +
    +    def test_signature_stable_with_same_user_id(self):
    +        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"
    +        )
    +        sig_2 = GatewayRunner._agent_config_signature(
    +            "claude-sonnet-4", runtime, ["hermes-telegram"], "", user_id="86701400"
    +        )
    +        assert sig_1 == sig_2
    +
    +    def test_signature_changes_with_user_id_alt(self):
    +        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", 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",
    +        )
    +        assert sig_a != sig_b
    +
    +    def test_signature_omits_user_id_when_absent(self):
    +        """Default-None user_id must not change signatures vs unset call.
    +
    +        Callers that pass no user_id kwarg must produce a signature
    +        byte-identical to ``user_id=None`` so in-flight caches survive
    +        the rollout of this fix.
    +        """
    +        from gateway.run import GatewayRunner
    +        runtime = {"provider": "anthropic", "api_key": "k", "base_url": "", "api_mode": "chat_completions"}
    +        sig_implicit = GatewayRunner._agent_config_signature(
    +            "claude-sonnet-4", runtime, ["hermes-telegram"], "",
    +        )
    +        sig_explicit_none = GatewayRunner._agent_config_signature(
    +            "claude-sonnet-4", runtime, ["hermes-telegram"], "",
    +            user_id=None, user_id_alt=None,
    +        )
    +        assert sig_implicit == sig_explicit_none
    ```
  - `tests/honcho_plugin/test_cli.py`

    ```diff
    @@ -153,4 +153,424 @@ def _boom(hcfg, client):
     
             out = capsys.readouterr().out
             assert "FAILED (Invalid API key)" in out
    -        assert "Connection... OK" not in out
    \ No newline at end of file
    +        assert "Connection... OK" not in out
    +
    +
    +class TestCloneHonchoForProfile:
    +    """Identity-key carryover during profile cloning.
    +
    +    The host-scoped identity-mapping keys (``userPeerAliases``,
    +    ``runtimePeerPrefix``, ``pinPeerName``) must survive a clone; otherwise
    +    the new profile silently fragments memory by resolving gateway users to
    +    raw runtime IDs instead of operator-declared peers.
    +    """
    +
    +    def _setup_clone_env(self, monkeypatch, tmp_path, cfg):
    +        import plugins.memory.honcho.cli as honcho_cli
    +        cfg_path = tmp_path / "config.json"
    +        cfg_path.write_text("{}")
    +        monkeypatch.setattr(honcho_cli, "_read_config", lambda: cfg)
    +        monkeypatch.setattr(honcho_cli, "_config_path", lambda: cfg_path)
    +        monkeypatch.setattr(honcho_cli, "_local_config_path", lambda: cfg_path)
    +        monkeypatch.setattr(honcho_cli, "_ensure_peer_exists", lambda host_key=None: True)
    +        written = {}
    +        def _write(c, path=None):
    +            written["cfg"] = c
    +        monkeypatch.setattr(honcho_cli, "_write_config", _write)
    +        return honcho_cli, written
    +
    +    def test_user_peer_aliases_carry_into_cloned_profile(self, monkeypatch, tmp_path):
    +        cfg = {
    +            "apiKey": "***",
    +            "hosts": {
    +                "hermes": {
    +                    "userPeerAliases": {"86701400": "eri", "discord-491827364": "eri"},
    +                    "peerName": "eri",
    +                },
    +            },
    +        }
    +        honcho_cli, written = self._setup_clone_env(monkeypatch, tmp_path, cfg)
    +        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"}
    +
    +    def test_runtime_peer_prefix_carries_into_cloned_profile(self, monkeypatch, tmp_path):
    +        cfg = {
    +            "apiKey": "***",
    +            "hosts": {
    +                "hermes": {
    +                    "runtimePeerPrefix": "telegram_",
    +                    "peerName": "eri",
    +                },
    +            },
    +        }
    +        honcho_cli, written = self._setup_clone_env(monkeypatch, tmp_path, cfg)
    +        ok = honcho_cli.clone_honcho_for_profile("coder")
    +        assert ok is True
    +        new_block = written["cfg"]["hosts"]["hermes.coder"]
    +        assert new_block["runtimePeerPrefix"] == "telegram_"
    +
    +    def test_pin_peer_name_carries_into_cloned_profile(self, monkeypatch, tmp_path):
    +        cfg = {
    +            "apiKey": "***",
    +            "hosts": {
    +                "hermes": {
    +                    "pinPeerName": True,
    +                    "peerName": "eri",
    +                },
    +            },
    +        }
    +        honcho_cli, written = self._setup_clone_env(monkeypatch, tmp_path, cfg)
    +        ok = honcho_cli.clone_honcho_for_profile("coder")
    +        assert ok is True
    +        new_block = written["cfg"]["hosts"]["hermes.coder"]
    +        assert new_block["pinPeerName"] is True
    +
    +    def test_unset_identity_keys_do_not_appear_in_cloned_profile(self, monkeypatch, tmp_path):
    +        cfg = {
    +            "apiKey": "***",
    +            "hosts": {"hermes": {"peerName": "eri"}},
    +        }
    +        honcho_cli, written = self._setup_clone_env(monkeypatch, tmp_path, cfg)
    +        ok = honcho_cli.clone_honcho_for_profile("coder")
    +        assert ok is True
    +        new_block = written["cfg"]["hosts"]["hermes.coder"]
    +        assert "userPeerAliases" not in new_block
    +        assert "runtimePeerPrefix" not in new_block
    +        assert "pinPeerName" not in new_block
    +
    +
    +class TestSetupWizardDeploymentShape:
    +    """The deployment-shape step writes pinPeerName / userPeerAliases /
    +    runtimePeerPrefix based on the operator's chosen shape.
    +
    +    Single-operator deployments collapse all platforms to peerName.
    +    Multi-user gateways leave the resolver to route per-runtime.
    +    Hybrid deployments alias the operator's own runtime IDs only.
    +
    +    These tests script the interactive _prompt calls and assert the
    +    resulting hermes_host block, so the wizard's deployment-shape
    +    semantics stay locked even as adjacent prompts are added.
    +    """
    +
    +    def _run_setup(self, monkeypatch, tmp_path, *, answers, initial_cfg=None):
    +        import plugins.memory.honcho.cli as honcho_cli
    +
    +        cfg_path = tmp_path / "config.json"
    +        cfg_path.write_text("{}")
    +        cfg = initial_cfg if initial_cfg is not None else {"apiKey": "***"}
    +
    +        monkeypatch.setattr(honcho_cli, "_read_config", lambda: cfg)
    +        monkeypatch.setattr(honcho_cli, "_config_path", lambda: cfg_path)
    +        monkeypatch.setattr(honcho_cli, "_local_config_path", lambda: cfg_path)
    +        monkeypatch.setattr(honcho_cli, "_host_key", lambda: "hermes")
    +        monkeypatch.setattr(honcho_cli, "_ensure_sdk_installed", lambda: True)
    +        monkeypatch.setattr(honcho_cli, "_write_config", lambda *a, **k: None)
    +
    +        # Bypass config.yaml + connection test side effects.
    +        monkeypatch.setattr(
    +            "hermes_cli.config.load_config", lambda: {"memory": {}}, raising=False,
    +        )
    +        monkeypatch.setattr(
    +            "hermes_cli.config.save_config", lambda c: None, raising=False,
    +        )
    +
    +        class _FakeClientCfg:
    +            def resolve_session_name(self):
    +                return "hermes-test"
    +            workspace_id = "hermes"
    +            peer_name = "eri"
    +            ai_peer = "hermetika"
    +            observation_mode = "directional"
    +            write_frequency = "async"
    +            recall_mode = "hybrid"
    +            session_strategy = "per-session"
    +
    +        monkeypatch.setattr(
    +            "plugins.memory.honcho.client.HonchoClientConfig.from_global_config",
    +            lambda host=None: _FakeClientCfg(),
    +        )
    +        monkeypatch.setattr(
    +            "plugins.memory.honcho.client.reset_honcho_client",
    +            lambda: None,
    +        )
    +        monkeypatch.setattr(
    +            "plugins.memory.honcho.client.get_honcho_client",
    +            lambda hcfg: object(),
    +        )
    +
    +        # Scripted _prompt: pop answers in order. Default-return for unconsumed prompts.
    +        answer_iter = iter(answers)
    +        def _scripted_prompt(label, default=None, secret=[redacted:secret-assignment]
    +            try:
    +                return next(answer_iter)
    +            except StopIteration:
    +                return default if default is not None else ""
    +        monkeypatch.setattr(honcho_cli, "_prompt", _scripted_prompt)
    +
    +        honcho_cli.cmd_setup(SimpleNamespace())
    +        return cfg["hosts"]["hermes"]
    +
    +    def test_single_shape_sets_pin_peer_name_and_clears_aliases(self, monkeypatch, tmp_path):
    +        answers = [
    +            "cloud",           # deployment
    +            "",                # api key (keep)
    +            "eri",             # peer name
    +            "hermetika",       # ai peer
    +            "hermes",          # workspace
    +            "single",          # deployment shape ← key answer
    +            # remaining prompts fall through to defaults
    +        ]
    +        initial_cfg = {
    +            "apiKey": "***",
    +            "hosts": {"hermes": {
    +                "userPeerAliases": {"old": "stale"},
    +                "runtimePeerPrefix": "old_",
    +            }},
    +        }
    +        host = self._run_setup(monkeypatch, tmp_path, answers=answers, initial_cfg=initial_cfg)
    +        assert host["pinPeerName"] is True
    +        assert "userPeerAliases" not in host
    +        assert "runtimePeerPrefix" not in host
    +
    +    def test_multi_shape_leaves_pin_false_and_accepts_prefix(self, monkeypatch, tmp_path):
    +        answers = [
    +            "cloud",           # deployment
    +            "",                # api key (keep)
    +            "eri",             # peer name
    +            "hermetika",       # ai peer
    +            "hermes",          # workspace
    +            "multi",           # deployment shape
    +            "telegram_",       # runtime peer prefix
    +        ]
    +        host = self._run_setup(monkeypatch, tmp_path, answers=answers)
    +        assert host["pinPeerName"] is False
    +        # Multi must NOT auto-write ``userPeerAliases: {}``: an empty host
    +        # map would silently override a root-level baseline.  Absence is
    +        # the correct "no host opinion" signal.
    +        assert "userPeerAliases" not in host
    +        assert host["runtimePeerPrefix"] == "telegram_"
    +
    +    def test_hybrid_shape_aliases_operator_runtime_ids_to_peer_name(self, monkeypatch, tmp_path):
    +        answers = [
    +            "cloud",           # deployment
    +            "",                # api key (keep)
    +            "eri",             # peer name
    +            "hermetika",       # ai peer
    +            "hermes",          # workspace
    +            "hybrid",          # deployment shape
    +            "86701400",        # telegram uid
    +            "491827364",       # discord snowflake
    +            "",                # slack (skip)
    +            "",                # matrix (skip)
    +            "",                # runtime peer prefix (skip)
    +        ]
    +        host = self._run_setup(monkeypatch, tmp_path, answers=answers)
    +        assert host["pinPeerName"] is False
    +        assert host["userPeerAliases"] == {
    +            "86701400": "eri",
    +            "491827364": "eri",
    +        }
    +        assert "runtimePeerPrefix" not in host
    +
    +    def test_skip_shape_preserves_existing_identity_config(self, monkeypatch, tmp_path):
    +        initial_cfg = {
    +            "apiKey": "***",
    +            "hosts": {"hermes": {
    +                "pinPeerName": True,
    +                "userPeerAliases": {"keep": "me"},
    +                "runtimePeerPrefix": "keep_",
    +            }},
    +        }
    +        answers = [
    +            "cloud", "", "eri", "hermetika", "hermes", "skip",
    +        ]
    +        host = self._run_setup(monkeypatch, tmp_path, answers=answers, initial_cfg=initial_cfg)
    +        assert host["pinPeerName"] is True
    +        assert host["userPeerAliases"] == {"keep": "me"}
    +        assert host["runtimePeerPrefix"] == "keep_"
    +
    +    def test_single_to_multi_steers_to_hybrid_by_default(self, monkeypatch, tmp_path):
    +        """Flipping single → multi triggers a warning that auto-steers the
    +        operator to ``hybrid`` (default), so their own runtime IDs keep
    +        landing on peerName instead of orphaning the pinned-pool history.
    +        """
    +        initial_cfg = {
    +            "apiKey": "***",
    +            "hosts": {"hermes": {"pinPeerName": True, "peerName": "eri"}},
    +        }
    +        answers = [
    +            "cloud",           # deployment
    +            "",                # api key (keep)
    +            "eri",             # peer name
    +            "hermetika",       # ai peer
    +            "hermes",          # workspace
    +            "multi",           # deployment shape — triggers the guard
    +            "hybrid",          # guard response: accept the steer
    +            "86701400",        # telegram uid
    +            "",                # discord (skip)
    +            "",                # slack (skip)
    +            "",                # matrix (skip)
    +            "",                # runtime prefix (skip)
    +        ]
    +        host = self._run_setup(monkeypatch, tmp_path, answers=answers, initial_cfg=initial_cfg)
    +        assert host["pinPeerName"] is False
    +        assert host["userPeerAliases"] == {"86701400": "eri"}
    +
    +    def test_single_to_multi_yes_override_keeps_multi(self, monkeypatch, tmp_path):
    +        """Operator can override the steer by answering ``yes`` and accept
    +        the orphaning consequences.  This is the explicit undo-the-pin path.
    +        """
    +        initial_cfg = {
    +            "apiKey": "***",
    +            "hosts": {"hermes": {"pinPeerName": True, "peerName": "eri"}},
    +        }
    +        answers = [
    +            "cloud", "", "eri", "hermetika", "hermes",
    +            "multi",           # deployment shape — triggers the guard
    +            "yes",             # guard response: confirm multi
    +            "telegram_",       # runtime peer prefix
    +        ]
    +        host = self._run_setup(monkeypatch, tmp_path, answers=answers, initial_cfg=initial_cfg)
    +        assert host["pinPeerName"] is False
    +        # See test_multi_shape_leaves_pin_false_and_accepts_prefix.
    +        assert "userPeerAliases" not in host
    +        assert host["runtimePeerPrefix"] == "telegram_"
    +
    +    def test_host_pin_user_peer_true_is_detected_as_single(self, monkeypatch, tmp_path):
    +        """Host-level ``pinUserPeer: true`` must classify as ``single``.
    +
    +        Pressing Enter at the shape prompt then preserves the pin instead
    +        of falling through to ``multi`` and orphaning the user's memory
    +        pool — the bug the wizard regressed when ``pinUserPeer`` landed
    +        as a higher-precedence alias.
    +        """
    +        initial_cfg = {
    +            "apiKey": "***",
    +            "hosts": {"hermes": {"pinUserPeer": True, "peerName": "eri"}},
    +        }
    +        # Exhaust the iterator before the shape prompt so the scripted
    +        # mock falls through to the prompt's default (which is the
    +        # wizard-detected shape).  Scripting an explicit "" would NOT
    +        # exercise that fallthrough — the mock returns it literally.
    +        answers = ["cloud", "", "eri", "hermetika", "hermes"]
    +        host = self._run_setup(monkeypatch, tmp_path, answers=answers, initial_cfg=initial_cfg)
    +        # Scrub-then-write normalises onto pinPeerName and drops the alias
    +        # so resolver precedence can't reintroduce ambiguity.
    +        assert host["pinPeerName"] is True
    +        assert "pinUserPeer" not in host
    +
    +    def test_host_pin_user_peer_false_overrides_root_pin_peer_name(
    +        self, monkeypatch, tmp_path
    +    ):
    +        """Host ``pinUserPeer: false`` outranks host ``pinPeerName`` in the
    +        resolver.  Detection must agree, otherwise the wizard would offer
    +        ``single`` as the default and silently re-pin a profile the
    +        operator explicitly unpinned via the newer key.
    +        """
    +        initial_cfg = {
    +            "apiKey": "***",
    +            "hosts": {"hermes": {
    +                "pinUserPeer": False,
    +                "pinPeerName": True,
    +                "peerName": "eri",
    +            }},
    +        }
    +        answers = ["cloud", "", "eri", "hermetika", "hermes"]
    +        host = self._run_setup(monkeypatch, tmp_path, answers=answers, initial_cfg=initial_cfg)
    +        assert host["pinPeerName"] is False
    +        assert "pinUserPeer" not in host
    +
    +    def test_root_user_peer_aliases_detected_as_hybrid(self, monkeypatch, tmp_path):
    +        """Root-level ``userPeerAliases`` must classify as ``hybrid`` even
    +        when the host block has no aliases of its own.
    +        """
    +        initial_cfg = {
    +            "apiKey": "***",
    +            "userPeerAliases": {"86701400": "eri"},
    +            "hosts": {"hermes": {"peerName": "eri"}},
    +        }
    +        answers = ["cloud", "", "eri", "hermetika", "hermes"]
    +        host = self._run_setup(monkeypatch, tmp_path, answers=answers, initial_cfg=initial_cfg)
    +        assert host["pinPeerName"] 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"}
    +
    +    def test_multi_does_not_override_root_user_peer_aliases(self, monkeypatch, tmp_path):
    +        """Explicit ``multi`` must leave the host ``userPeerAliases`` key
    +        absent, preserving any root-level aliases as a cross-host baseline.
    +
    +        Picking ``multi`` here is an active choice — detection would have
    +        defaulted to ``hybrid`` because root aliases exist — so the
    +        operator's intent is to drop the alias mapping for this host.
    +        We honor that by writing ``pinPeerName: false`` only, and rely
    +        on the host's absence of ``userPeerAliases`` to inherit root.
    +        That inheritance is intentional: a true wipe would require the
    +        operator to delete the root key explicitly.
    +        """
    +        initial_cfg = {
    +            "apiKey": "***",
    +            "userPeerAliases": {"baseline": "eri"},
    +            "hosts": {"hermes": {"peerName": "eri"}},
    +        }
    +        answers = [
    +            "cloud", "", "eri", "hermetika", "hermes",
    +            "multi",           # explicit multi override of detected hybrid
    +        ]
    +        host = self._run_setup(monkeypatch, tmp_path, answers=answers, initial_cfg=initial_cfg)
    +        assert host["pinPeerName"] is False
    +        assert "userPeerAliases" not in host
    +
    +    def test_single_scrubs_stale_pin_user_peer_false(self, monkeypatch, tmp_path):
    +        """Choosing ``single`` must drop any host-level ``pinUserPeer``,
    +        otherwise an existing ``pinUserPeer: false`` would outrank the
    +        freshly written ``pinPeerName: true`` and leave the profile
    +        effectively unpinned (the P1 latent-precedence regression).
    +        """
    +        initial_cfg = {
    +            "apiKey": "***",
    +            "hosts": {"hermes": {
    +                "pinUserPeer": False,
    +                "peerName": "eri",
    +            }},
    +        }
    +        answers = [
    +            "cloud", "", "eri", "hermetika", "hermes",
    +            "single",
    +        ]
    +        host = self._run_setup(monkeypatch, tmp_path, answers=answers, initial_cfg=initial_cfg)
    +        assert host["pinPeerName"] is True
    +        assert "pinUserPeer" not in host
    +
    +
    +class TestCloneCarriesPinUserPeer:
    +    """``pinUserPeer`` (canonical name for ``pinPeerName``) must survive a
    +    profile clone.  Without this, a default profile that uses the newer
    +    key would silently produce cloned profiles without the pin even
    +    though the resolver prefers ``pinUserPeer`` over ``pinPeerName``.
    +    """
    +
    +    def test_clone_inherits_host_pin_user_peer(self, monkeypatch, tmp_path):
    +        import plugins.memory.honcho.cli as honcho_cli
    +
    +        cfg = {
    +            "apiKey": "***",
    +            "hosts": {"hermes": {"pinUserPeer": True, "peerName": "eri"}},
    +        }
    +        cfg_path = tmp_path / "config.json"
    +        cfg_path.write_text("{}")
    +        monkeypatch.setattr(honcho_cli, "_read_config", lambda: cfg)
    +        monkeypatch.setattr(honcho_cli, "_config_path", lambda: cfg_path)
    +        monkeypatch.setattr(honcho_cli, "_local_config_path", lambda: cfg_path)
    +        monkeypatch.setattr(honcho_cli, "_ensure_peer_exists", lambda host_key=None: True)
    +        written = {}
    +        monkeypatch.setattr(
    +            honcho_cli, "_write_config", lambda c, path=None: written.setdefault("cfg", c),
    +        )
    +
    +        ok = honcho_cli.clone_honcho_for_profile("partner")
    +        assert ok is True
    +        new_block = written["cfg"]["hosts"]["hermes.partner"]
    +        assert new_block["pinUserPeer"] is True
    ```
  - `tests/honcho_plugin/test_pin_peer_name.py`

    ```diff
    @@ -1,24 +1,20 @@
    -"""Tests for the ``pinPeerName`` config flag (#14984).
    -
    -By default, when Hermes runs under a gateway (Telegram, Discord, Slack, ...)
    -it passes the platform-native user ID as ``runtime_user_peer_name`` into
    -``HonchoSessionManager``.  That ID wins over any configured ``peer_name``
    -so multi-user bots scope memory per user.
    -
    -For a single-user personal deployment where the user connects over multiple
    -platforms, that default forks memory into one Honcho peer per platform
    -(Telegram UID, Discord snowflake, Slack user ID, ...).  The user asked for
    -an opt-in knob that pins the user peer to ``peer_name`` from ``honcho.json``
    -so the same person's memory stays unified regardless of which platform the
    -turn arrived on — ``hosts.<host>.pinPeerName: true`` (or root-level
    -``pinPeerName: true``).
    -
    -These tests exercise both the config parsing (``client.py::from_global_config``)
    -and the resolution order (``session.py::get_or_create``).  We stub the
    -Honcho API calls so we can assert the chosen ``user_peer_id`` without
    -touching the network.
    +"""Tests for the ``pinPeerName`` / ``pinUserPeer`` config flag.
    +
    +Under a gateway (Telegram, Discord, Slack, ...) Hermes passes the
    +platform-native user ID as ``runtime_user_peer_name`` into
    +``HonchoSessionManager``.  By default that ID wins over any configured
    +``peer_name`` so multi-user bots scope memory per user.
    +
    +For single-user deployments connecting over multiple platforms,
    +``pinUserPeer: true`` pins the user peer to ``peer_name`` so memory stays
    +unified across platforms.
    +
    +Tests cover config parsing (``client.py::from_global_config``) and resolver
    +order (``session.py::get_or_create``), stubbing Honcho API calls so the
    +chosen ``user_peer_id`` can be asserted without touching the network.
     """
     
    +import hashlib
     import json
     from unittest.mock import MagicMock
     
    ```

    ```diff
    @@ -99,6 +95,90 @@ def test_explicit_false_parses(self, tmp_path, monkeypatch):
             assert config.pin_peer_name is False
     
     
    +class TestRuntimePeerMappingConfigParsing:
    +    def test_defaults_are_empty(self):
    +        config = HonchoClientConfig()
    +        assert config.user_peer_aliases == {}
    +        assert config.runtime_peer_prefix == ""
    +
    +    def test_root_level_aliases_and_prefix_parse(self, tmp_path):
    +        config_file = tmp_path / "honcho.json"
    +        config_file.write_text(json.dumps({
    +            "apiKey": "k",
    +            "userPeerAliases": {
    +                " 86701400 ": " Igor ",
    +                "": "ignored",
    +                "empty-value": " ",
    +                "null-value": None,
    +            },
    +            "runtimePeerPrefix": "telegram_",
    +        }))
    +
    +        config = HonchoClientConfig.from_global_config(config_path=config_file)
    +
    +        assert config.user_peer_aliases == {"86701400": "Igor"}
    +        assert config.runtime_peer_prefix == "telegram_"
    +
    +    def test_host_aliases_override_root_aliases_as_whole_map(self, tmp_path):
    +        config_file = tmp_path / "honcho.json"
    +        config_file.write_text(json.dumps({
    +            "apiKey": "k",
    +            "userPeerAliases": {"root-user": "root-peer"},
    +            "hosts": {
    +                "hermes": {
    +                    "userPeerAliases": {"host-user": "host-peer"},
    +                },
    +            },
    +        }))
    +
    +        config = HonchoClientConfig.from_global_config(config_path=config_file)
    +
    +        assert config.user_peer_aliases == {"host-user": "host-peer"}
    +
    +    def test_host_empty_aliases_disable_root_aliases(self, tmp_path):
    +        config_file = tmp_path / "honcho.json"
    +        config_file.write_text(json.dumps({
    +            "apiKey": "k",
    +            "userPeerAliases": {"root-user": "root-peer"},
    +            "hosts": {
    +                "hermes": {
    +                    "userPeerAliases": {},
    +                },
    +            },
    +        }))
    +
    +        config = HonchoClientConfig.from_global_config(config_path=config_file)
    +
    +        assert config.user_peer_aliases == {}
    +
    +    def test_host_empty_prefix_disables_root_prefix(self, tmp_path):
    +        config_file = tmp_path / "honcho.json"
    +        config_file.write_text(json.dumps({
    +            "apiKey": "k",
    +            "runtimePeerPrefix": "telegram_",
    +            "hosts": {
    +                "hermes": {
    +                    "runtimePeerPrefix": "",
    +                },
    +            },
    +        }))
    +
    +        config = HonchoClientConfig.from_global_config(config_path=config_file)
    +
    +        assert config.runtime_peer_prefix == ""
    +
    +    def test_malformed_alias_config_is_ignored(self, tmp_path):
    +        config_file = tmp_path / "honcho.json"
    +        config_file.write_text(json.dumps({
    +            "apiKey": "k",
    +            "userPeerAliases": ["not", "a", "map"],
    +        }))
    +
    +        config = HonchoClientConfig.from_global_config(config_path=config_file)
    +
    +        assert config.user_peer_aliases == {}
    +
    +
     # ---------------------------------------------------------------------------
     # Peer resolution (the actual bug fix)
     # ---------------------------------------------------------------------------
    ```

    ```diff
    @@ -119,13 +199,24 @@ def _patch_manager_for_resolution_test(mgr: HonchoSessionManager) -> None:
     class TestPeerResolutionOrder:
         """Matrix of (runtime_id, pin_peer_name, peer_name) → expected user_peer_id."""
     
    -    def _config(self, *, peer_name: str | None, pin_peer_name: bool) -> HonchoClientConfig:
    +    def _config(
    +        self,
    +        *,
    +        peer_name: str | None,
    +        pin_peer_name: bool,
    +        user_peer_aliases: dict[str, str] | None = None,
    +        runtime_peer_prefix: str = "",
    +        session_peer_prefix: bool = False,
    +    ) -> HonchoClientConfig:
             # The test doesn't need auth / Honcho — disable the provider so
             # the manager doesn't try to open a real client.
             return HonchoClientConfig(
                 api_key="[redacted:secret-assignment]",
                 peer_name=peer_name,
                 pin_peer_name=pin_peer_name,
    +            user_peer_aliases=user_peer_aliases or {},
    +            runtime_peer_prefix=runtime_peer_prefix,
    +            session_peer_prefix=session_peer_prefix,
                 enabled=False,
                 write_frequency="turn",  # avoid spawning the async writer thread
             )
    ```

    ```diff
    @@ -148,11 +239,177 @@ def test_runtime_wins_when_pin_is_false(self):
                 "bot immediately merges memory across users."
             )
     
    +    def test_alias_wins_for_known_runtime_id(self):
    +        """Known platform IDs can preserve an existing stable Honcho peer."""
    +        mgr = HonchoSessionManager(
    +            honcho=MagicMock(),
    +            config=self._config(
    +                peer_name="Igor",
    +                pin_peer_name=False,
    +                user_peer_aliases={"86701400": "Igor"},
    +                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 == "Igor"
    +
    +    def test_unknown_runtime_id_uses_prefix(self):
    +        """Unknown gateway users stay isolated but become platform-scoped."""
    +        mgr = HonchoSessionManager(
    +            honcho=MagicMock(),
    +            config=self._config(
    +                peer_name="Igor",
    +                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 == "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(),
    +            config=self._config(
    +                peer_name=None,
    +                pin_peer_name=False,
    +                user_peer_aliases={"86701400": "Alice Smith!"},
    +            ),
    +            runtime_user_peer_name="86701400",
    +        )
    +        _patch_manager_for_resolution_test(mgr)
    +
    +        session = mgr.get_or_create("telegram:86701400")
    +        assert session.user_peer_id == "Alice-Smith-"
    +
    +    def test_alias_keys_match_raw_runtime_id_before_sanitization(self):
    +        """Alias selection is exact on platform IDs before Honcho ID cleanup."""
    +        mgr = HonchoSessionManager(
    +            honcho=MagicMock(),
    +            config=self._config(
    +                peer_name=None,
    +                pin_peer_name=False,
    +                user_peer_aliases={
    +                    "user:42": "raw-match",
    +                    "user-42": "sanitized-match",
    +                },
    +            ),
    +            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 == "raw-match"
    +
    +    def test_session_peer_prefix_is_orthogonal_to_runtime_peer_prefix(self):
    +        """sessionPeerPrefix scopes session IDs; runtimePeerPrefix scopes user peers."""
    +        mgr = HonchoSessionManager(
    +            honcho=MagicMock(),
    +            config=self._config(
    +                peer_name="Igor",
    +                pin_peer_name=False,
    +                runtime_peer_prefix="telegram_",
    +                session_peer_prefix=True,
    +            ),
    +            runtime_user_peer_name="86701400",
    +        )
    +        _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"
    +
         def test_config_wins_when_pin_is_true(self):
    -        """The #14984 fix: single-user deployments opt into config pinning."""
    +        """With pin enabled, configured peer_name beats runtime ID."""
             mgr = HonchoSessionManager(
                 honcho=MagicMock(),
    -            config=self._config(peer_name="Igor", pin_peer_name=True),
    +            config=self._config(
    +                peer_name="Igor",
    +                pin_peer_name=True,
    +                user_peer_aliases={"86701400": "Alias"},
    +                runtime_peer_prefix="telegram_",
    +            ),
                 runtime_user_peer_name="86701400",  # Telegram pushes this in
             )
             _patch_manager_for_resolution_test(mgr)
    ```

    ```diff
    @@ -167,7 +424,23 @@ def test_config_wins_when_pin_is_true(self):
         def test_pin_noop_when_peer_name_missing(self):
             """Safety: pinPeerName alone (no peer_name) must not silently drop
             the runtime identity.  Without a configured peer_name there's
    -        nothing to pin to — fall back to runtime as before."""
    +        nothing to pin to — fall through to runtime mapping."""
    +        mgr = HonchoSessionManager(
    +            honcho=MagicMock(),
    +            config=self._config(
    +                peer_name=None,
    +                pin_peer_name=True,
    +                user_peer_aliases={"86701400": "Igor"},
    +                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 == "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),
    ```

    ```diff
    @@ -176,11 +449,42 @@ def test_pin_noop_when_peer_name_missing(self):
             _patch_manager_for_resolution_test(mgr)
     
             session = mgr.get_or_create("telegram:86701400")
    -        assert session.user_peer_id == "86701400", (
    -            "pin_peer_name=True with no peer_name set must not strip the "
    -            "runtime ID — otherwise the user peer would collapse to the "
    -            "session-key fallback and lose per-user scoping entirely"
    +        assert session.user_peer_id == "86701400"
    +
    +    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."""
    +        mgr = HonchoSessionManager(
    +            honcho=MagicMock(),
    +            config=self._config(
    +                peer_name=None,
    +                pin_peer_name=False,
    +                user_peer_aliases={"union-user": "Igor"},
    +                runtime_peer_prefix="feishu_",
    +            ),
    +            runtime_user_peer_name="open-id",
    +            runtime_user_peer_name_alt="union-user",
             )
    +        _patch_manager_for_resolution_test(mgr)
    +
    +        session = mgr.get_or_create("feishu:chat")
    +        assert session.user_peer_id == "Igor"
    +
    +    def test_alt_runtime_id_does_not_replace_primary_prefix_fallback(self):
    +        mgr = HonchoSessionManager(
    +            honcho=MagicMock(),
    +            config=self._config(
    +                peer_name=None,
    +                pin_peer_name=False,
    +                user_peer_aliases={"other-union": "Igor"},
    +                runtime_peer_prefix="feishu_",
    +            ),
    +            runtime_user_peer_name="open-id",
    +            runtime_user_peer_name_alt="union-user",
    +        )
    +        _patch_manager_for_resolution_test(mgr)
    +
    +        session = mgr.get_or_create("feishu:chat")
    +        assert session.user_peer_id == "feishu_open-id"
     
         def test_runtime_missing_falls_back_to_peer_name(self):
             """CLI-mode (no gateway runtime identity) uses config peer_name —
    ```

    ```diff
    @@ -233,9 +537,8 @@ def test_pin_does_not_affect_assistant_peer(self):
     
     
     class TestCrossPlatformMemoryUnification:
    -    """The user-visible outcome of the #14984 fix: the same physical user
    -    talking to Hermes via Telegram AND Discord should land on ONE peer
    -    (not two) when pinPeerName is opted in.
    +    """The same physical user talking to Hermes via Telegram AND Discord
    +    lands on ONE peer when ``pinPeerName`` is opted in.
         """
     
         def _config_pinned(self) -> HonchoClientConfig:
    ```

    ```diff
    @@ -305,3 +608,277 @@ def test_multiuser_default_keeps_platforms_separate(self):
                 "multi-user default MUST keep users separate — a regression "
                 "here would silently merge unrelated users' memory"
             )
    +
    +
    +class TestPinUserPeerAlias:
    +    """``pinUserPeer`` and ``pinPeerName`` both resolve to the same internal
    +    ``pin_peer_name`` field.  Precedence when both appear: host pinUserPeer →
    +    host pinPeerName → root pinUserPeer → root pinPeerName → default.
    +    """
    +
    +    def test_root_pinUserPeer_true_pins(self, tmp_path):
    +        from plugins.memory.honcho.client import HonchoClientConfig
    +        import json
    +        config_file = tmp_path / "honcho.json"
    +        config_file.write_text(json.dumps({
    +            "apiKey": "***",
    +            "peerName": "eri",
    +            "pinUserPeer": True,
    +        }))
    +        config = HonchoClientConfig.from_global_config(config_path=config_file)
    +        assert config.pin_peer_name is True
    +
    +    def test_host_pinUserPeer_wins_over_root_pinPeerName(self, tmp_path):
    +        from plugins.memory.honcho.client import HonchoClientConfig
    +        import json
    +        config_file = tmp_path / "honcho.json"
    +        config_file.write_text(json.dumps({
    +            "apiKey": "***",
    +            "peerName": "eri",
    +            "pinPeerName": False,
    +            "hosts": {"hermes": {"pinUserPeer": True}},
    +        }))
    +        config = HonchoClientConfig.from_global_config(config_path=config_file)
    +        assert config.pin_peer_name is True
    +
    +    def test_host_pinUserPeer_false_disables_root_pinPeerName(self, tmp_path):
    +        from plugins.memory.honcho.client import HonchoClientConfig
    +        import json
    +        config_file = tmp_path / "honcho.json"
    +        config_file.write_text(json.dumps({
    +            "apiKey": "***",
    +            "peerName": "eri",
    +            "pinPeerName": True,
    +            "hosts": {"hermes": {"pinUserPeer": False}},
    +        }))
    +        config = HonchoClientConfig.from_global_config(config_path=config_file)
    +        assert config.pin_peer_name is False, (
    +            "Host-level pinUserPeer=false must override root-level "
    +            "pinPeerName=true so a host can unpin a globally-pinned profile."
    +        )
    +
    +    def test_pinPeerName_still_works_unchanged(self, tmp_path):
    +        from plugins.memory.honcho.client import HonchoClientConfig
    +        import json
    +        config_file = tmp_path / "honcho.json"
    +        config_file.write_text(json.dumps({
    +            "apiKey": "***",
    +            "peerName": "eri",
    +            "hosts": {"hermes": {"pinPeerName": True}},
    +        }))
    +        config = HonchoClientConfig.from_global_config(config_path=config_file)
    +        assert config.pin_peer_name is True
    +
    +
    +class TestPinTransition:
    +    """Behavior when honcho.json flips ``pinPeerName`` true → false.
    +
    +    Covers two contracts:
    +      1. A freshly-built manager picks up the flipped config and resolves
    +         the same runtime ID to a new peer (no resolver staleness).
    +      2. The gateway's agent-cache signature reflects honcho identity-mapping
    +         changes, so a config edit busts the cached AIAgent on the next turn.
    +    """
    +
    +    def _pinned(self) -> HonchoClientConfig:
    +        return HonchoClientConfig(
    +            api_key="k",
    +            peer_name="Igor",
    +            pin_peer_name=True,
    +            enabled=False,
    +            write_frequency="turn",
    +        )
    +
    +    def _unpinned(self) -> HonchoClientConfig:
    +        return HonchoClientConfig(
    +            api_key="k",
    +            peer_name="Igor",
    +            pin_peer_name=False,
    +            enabled=False,
    +            write_frequency="turn",
    +        )
    +
    +    def test_fresh_manager_after_flip_resolves_to_runtime(self):
    +        pinned_mgr = HonchoSessionManager(
    +            honcho=MagicMock(),
    +            config=self._pinned(),
    +            runtime_user_peer_name="86701400",
    +        )
    +        _patch_manager_for_resolution_test(pinned_mgr)
    +        before = pinned_mgr.get_or_create("telegram:86701400")
    +        assert before.user_peer_id == "Igor"
    +
    +        unpinned_mgr = HonchoSessionManager(
    +            honcho=MagicMock(),
    +            config=self._unpinned(),
    +            runtime_user_peer_name="86701400",
    +        )
    +        _patch_manager_for_resolution_test(unpinned_mgr)
    +        after = unpinned_mgr.get_or_create("telegram:86701400")
    +        assert after.user_peer_id == "86701400", (
    +            "After flipping pinPeerName off, the same runtime ID must resolve "
    +            "to its own peer — otherwise multi-user mode silently merges users."
    +        )
    +
    +    def test_cached_session_survives_config_flip_in_same_manager(self):
    +        mgr = HonchoSessionManager(
    +            honcho=MagicMock(),
    +            config=self._pinned(),
    +            runtime_user_peer_name="86701400",
    +        )
    +        _patch_manager_for_resolution_test(mgr)
    +        first = mgr.get_or_create("telegram:86701400")
    +        assert first.user_peer_id == "Igor"
    +
    +        mgr._config = self._unpinned()
    +        second = mgr.get_or_create("telegram:86701400")
    +        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 "
    +            "gateway cache must bust the whole manager instead."
    +        )
    +
    +    def test_cache_busting_signature_reflects_pin_peer_name(self, tmp_path, monkeypatch):
    +        """Gateway agent cache must bust when honcho.json's pinPeerName flips."""
    +        from gateway.run import GatewayRunner
    +
    +        cfg_path = tmp_path / "honcho.json"
    +        monkeypatch.setenv("HERMES_HOME", str(tmp_path))
    +
    +        cfg_path.write_text(json.dumps({"apiKey": "k", "peerName": "Igor", "pinPeerName": True}))
    +        sig_pinned = GatewayRunner._extract_cache_busting_config({})
    +
    +        cfg_path.write_text(json.dumps({"apiKey": "k", "peerName": "Igor", "pinPeerName": False}))
    +        sig_unpinned = GatewayRunner._extract_cache_busting_config({})
    +
    +        assert sig_pinned["honcho.pin_peer_name"] != sig_unpinned["honcho.pin_peer_name"]
    +
    +    def test_cache_busting_signature_reflects_user_peer_aliases(self, tmp_path, monkeypatch):
    +        from gateway.run import GatewayRunner
    +
    +        cfg_path = tmp_path / "honcho.json"
    +        monkeypatch.setenv("HERMES_HOME", str(tmp_path))
    +
    +        cfg_path.write_text(json.dumps({"apiKey": "k", "peerName": "Igor"}))
    +        sig_no_aliases = GatewayRunner._extract_cache_busting_config({})
    +
    +        cfg_path.write_text(json.dumps({
    +            "apiKey": "k",
    +            "peerName": "Igor",
    +            "userPeerAliases": {"86701400": "Igor"},
    +        }))
    +        sig_with_aliases = GatewayRunner._extract_cache_busting_config({})
    +
    +        assert sig_no_aliases["honcho.user_peer_aliases"] != sig_with_aliases["honcho.user_peer_aliases"]
    +
    +    def test_cache_busting_signature_reflects_runtime_peer_prefix(self, tmp_path, monkeypatch):
    +        from gateway.run import GatewayRunner
    +
    +        cfg_path = tmp_path / "honcho.json"
    +        monkeypatch.setenv("HERMES_HOME", str(tmp_path))
    +
    +        cfg_path.write_text(json.dumps({"apiKey": "k", "peerName": "Igor"}))
    +        sig_no_prefix = GatewayRunner._extract_cache_busting_config({})
    +
    +        cfg_path.write_text(json.dumps({
    +            "apiKey": "k",
    +            "peerName": "Igor",
    +            "runtimePeerPrefix": "telegram_",
    +        }))
    +        sig_with_prefix = GatewayRunner._extract_cache_busting_config({})
    +
    +        assert sig_no_prefix["honcho.runtime_peer_prefix"] != sig_with_prefix["honcho.runtime_peer_prefix"]
    +
    +    def test_cache_busting_signature_reflects_ai_peer(self, tmp_path, monkeypatch):
    +        """Editing ``aiPeer`` mid-flight must invalidate the cached agent.
    +
    +        ``HonchoSessionManager`` freezes ``cfg.ai_peer`` at construction —
    +        without busting here, assistant writes keep landing on the old
    +        peer until an unrelated cache eviction.
    +        """
    +        from gateway.run import GatewayRunner
    +
    +        cfg_path = tmp_path / "honcho.json"
    +        monkeypatch.setenv("HERMES_HOME", str(tmp_path))
    +
    +        cfg_path.write_text(json.dumps({
    +            "apiKey": "k",
    +            "peerName": "Igor",
    +            "aiPeer": "hermes",
    +        }))
    +        sig_before = GatewayRunner._extract_cache_busting_config({})
    +
    +        cfg_path.write_text(json.dumps({
    +            "apiKey": "k",
    +            "peerName": "Igor",
    +            "aiPeer": "hermetika",
    +        }))
    +        sig_after = GatewayRunner._extract_cache_busting_config({})
    +
    +        assert sig_before["honcho.ai_peer"] != sig_after["honcho.ai_peer"]
    +
    +
    +class TestProfilePeerUniqueness:
    +    """Each Hermes profile can pin to its own unique peerName.
    +
    +    Profile cloning copies host blocks, but operators routinely diverge them
    +    afterwards (e.g. `hermes -p partner` pinned to a different person's peer).
    +    The resolver must honor host-level ``peerName`` so two profiles in the
    +    same workspace stay scoped to different Honcho peers.
    +    """
    +
    +    def _pinned_to(self, name: str) -> HonchoClientConfig:
    +        return HonchoClientConfig(
    +            api_key="k",
    +            peer_name=name,
    +            pin_peer_name=True,
    +            enabled=False,
    +            write_frequency="turn",
    +        )
    +
    +    def test_two_profiles_pinned_to_different_peer_names_resolve_distinctly(self):
    +        mgr_a = HonchoSessionManager(
    +            honcho=MagicMock(),
    +            config=self._pinned_to("alice"),
    +            runtime_user_peer_name="86701400",
    +        )
    +        _patch_manager_for_resolution_test(mgr_a)
    +        sess_a = mgr_a.get_or_create("telegram:86701400")
    +
    +        mgr_b = HonchoSessionManager(
    +            honcho=MagicMock(),
    +            config=self._pinned_to("bob"),
    +            runtime_user_peer_name="86701400",
    +        )
    +        _patch_manager_for_resolution_test(mgr_b)
    +        sess_b = mgr_b.get_or_create("telegram:86701400")
    +
    +        assert sess_a.user_peer_id == "alice"
    +        assert sess_b.user_peer_id == "bob"
    +        assert sess_a.user_peer_id != sess_b.user_peer_id, (
    +            "Profiles pinned to distinct peer names must not collapse to "
    +            "the same Honcho peer — otherwise profile isolation is fictional."
    +        )
    +
    +    def test_host_peer_name_overrides_root_when_pinned(self, tmp_path, monkeypatch):
    +        """Host-level peerName wins so each profile can pin uniquely while
    +        sharing a single root-level apiKey and workspace.
    +        """
    +        config_file = tmp_path / "honcho.json"
    +        config_file.write_text(json.dumps({
    +            "apiKey": "k",
    +            "peerName": "default-user",
    +            "hosts": {
    +                "hermes.partner": {
    +                    "peerName": "partner-user",
    +                    "pinPeerName": True,
    +                },
    +            },
    +        }))
    +        monkeypatch.setenv("HERMES_HOME", str(tmp_path / "isolated"))
    +
    +        cfg = HonchoClientConfig.from_global_config(
    +            host="hermes.partner", config_path=config_file,
    +        )
    +        assert cfg.peer_name == "partner-user"
    +        assert cfg.pin_peer_name is True
    ```
  - `tests/honcho_plugin/test_session.py`

    ```diff
    @@ -212,6 +212,39 @@ def test_get_peer_card_uses_direct_peer_lookup(self):
             assert mgr.get_peer_card(session.key) == ["Name: Robert"]
             assistant_peer.get_card.assert_called_once_with(target=session.user_peer_id)
     
    +    def test_get_peer_card_falls_back_to_target_peer_own_card(self):
    +        # When the observer-target card slot is empty (returns None/[]), fall
    +        # back to the target peer's own card. Self-hosted Honcho v3 stores the
    +        # peer card on the peer itself; the observer-target slot is only
    +        # populated when writes also go through that path.
    +        mgr, session = self._make_cached_manager()
    +        assistant_peer = MagicMock()
    +        assistant_peer.get_card.return_value = None  # observer-target slot empty
    +        user_peer = MagicMock()
    +        user_peer.get_card.return_value = ["Prefers: dark mode"]
    +
    +        def _peer(peer_id: str) -> MagicMock:
    +            return assistant_peer if peer_id == session.assistant_peer_id else user_peer
    +
    +        mgr._get_or_create_peer = MagicMock(side_effect=_peer)
    +
    +        assert mgr.get_peer_card(session.key) == ["Prefers: dark mode"]
    +        assistant_peer.get_card.assert_called_once_with(target=session.user_peer_id)
    +        user_peer.get_card.assert_called_once_with()
    +
    +    def test_set_peer_card_uses_observer_target_in_ai_observe_others_mode(self):
    +        # Writes must go to the same observer-target slot that reads check,
    +        # so that a subsequent honcho_profile read returns what was written.
    +        mgr, session = self._make_cached_manager()
    +        assistant_peer = MagicMock()
    +        assistant_peer.set_card.return_value = ["Role: user"]
    +        mgr._get_or_create_peer = MagicMock(return_value=assistant_peer)
    +
    +        result = mgr.set_peer_card(session.key, ["Role: user"])
    +
    +        assert result == ["Role: user"]
    +        assistant_peer.set_card.assert_called_once_with(["Role: user"], target=session.user_peer_id)
    +
         def test_search_context_uses_assistant_perspective_with_target(self):
             mgr, session = self._make_cached_manager()
             assistant_peer = MagicMock()
    ```

    ```diff
    @@ -573,7 +606,7 @@ class TestToolsModeInitBehavior:
         """Verify initOnSessionStart controls session init timing in tools mode."""
     
         def _make_provider_with_config(self, recall_mode="tools", init_on_session_start=False,
    -                                    peer_name=None, user_id=None):
    +                                    peer_name=None, user_id=None, user_id_alt=None):
             """Create a HonchoMemoryProvider with mocked config and dependencies."""
             from plugins.memory.honcho.client import HonchoClientConfig
     
    ```

    ```diff
    @@ -598,6 +631,8 @@ def _make_provider_with_config(self, recall_mode="tools", init_on_session_start=
             init_kwargs = {}
             if user_id:
                 init_kwargs["user_id"] = user_id
    +        if user_id_alt:
    +            init_kwargs["user_id_alt"] = user_id_alt
     
             with patch("plugins.memory.honcho.client.HonchoClientConfig.from_global_config", return_value=cfg), \
                  patch("plugins.memory.honcho.client.get_honcho_client", return_value=MagicMock()), \
    ```

    ```diff
    @@ -655,6 +690,15 @@ def test_user_id_used_when_no_peer_name(self):
             assert cfg.peer_name is None
             assert mock_manager_cls.call_args.kwargs["runtime_user_peer_name"] == "8439114563"
     
    +    def test_user_id_alt_is_passed_to_session_manager(self):
    +        """Gateway alternate user IDs are available for Honcho alias matching."""
    +        _, _, mock_manager_cls = self._make_provider_with_config(
    +            recall_mode="tools", init_on_session_start=True,
    +            peer_name=None, user_id="open-id", user_id_alt="union-id",
    +        )
    +        assert mock_manager_cls.call_args.kwargs["runtime_user_peer_name"] == "open-id"
    +        assert mock_manager_cls.call_args.kwargs["runtime_user_peer_name_alt"] == "union-id"
    +
     
     class TestPerSessionMigrateGuard:
         """Verify migrate_memory_files is skipped under per-session strategy.
    ```
  - `tests/run_agent/test_memory_provider_init.py`

    ```diff
    @@ -4,6 +4,27 @@
     from unittest.mock import patch
     
     
    +class RecordingMemoryProvider:
    +    name = "recording"
    +
    +    def __init__(self):
    +        self.init_kwargs = None
    +        self.init_session_id = None
    +
    +    def is_available(self):
    +        return True
    +
    +    def initialize(self, session_id, **kwargs):
    +        self.init_session_id = session_id
    +        self.init_kwargs = dict(kwargs)
    +
    +    def get_tool_schemas(self):
    +        return []
    +
    +    def shutdown(self):
    +        pass
    +
    +
     def test_blank_memory_provider_does_not_auto_enable_honcho():
         """Blank memory.provider should remain opt-out even if Honcho fallback looks configured."""
         cfg = {"memory": {"provider": ""}, "agent": {}}
    ```

    ```diff
    @@ -37,3 +58,35 @@ def test_blank_memory_provider_does_not_auto_enable_honcho():
         load_memory_provider.assert_not_called()
         save_config.assert_not_called()
     
    +
    +def test_aiagent_forwards_user_id_alt_to_memory_provider():
    +    provider = RecordingMemoryProvider()
    +    cfg = {"memory": {"provider": "recording"}, "agent": {}}
    +
    +    with (
    +        patch("hermes_cli.config.load_config", return_value=cfg),
    +        patch("plugins.memory.load_memory_provider", return_value=provider),
    +        patch("agent.model_metadata.get_model_context_length", return_value=204_800),
    +        patch("run_agent.get_tool_definitions", return_value=[]),
    +        patch("run_agent.check_toolset_requirements", return_value={}),
    +        patch("run_agent.OpenAI"),
    +    ):
    +        from run_agent import AIAgent
    +
    +        agent = AIAgent(
    +            api_key="[redacted:secret-assignment]",
    +            base_url="https://openrouter.ai/api/v1",
    +            quiet_mode=True,
    +            skip_context_files=True,
    +            skip_memory=False,
    +            session_id="sess-alt",
    +            platform="feishu",
    +            user_id="open-id",
    +            user_id_alt="union-id",
    +        )
    +
    +    assert agent._memory_manager is not None
    +    assert provider.init_session_id == "sess-alt"
    +    assert provider.init_kwargs["user_id"] == "open-id"
    +    assert provider.init_kwargs["user_id_alt"] == "union-id"
    +    assert provider.init_kwargs["platform"] == "feishu"
    ```
  - `tests/test_honcho_session_context.py`

    ```diff
    @@ -0,0 +1,95 @@
    +"""Tests for Honcho session context peer resolution."""
    +
    +from types import SimpleNamespace
    +
    +from plugins.memory.honcho.session import HonchoSession, HonchoSessionManager
    +
    +
    +class _FakeSummary:
    +    content = "summary"
    +
    +
    +class _FakeContext:
    +    summary = _FakeSummary()
    +    peer_representation = "representation"
    +    peer_card = ["fact"]
    +    messages = []
    +
    +
    +class _RecordingHonchoSession:
    +    def __init__(self):
    +        self.calls = []
    +
    +    def context(self, **kwargs):
    +        self.calls.append(kwargs)
    +        return _FakeContext()
    +
    +
    +def _manager_with_cached_session(*, ai_observe_others=True):
    +    cfg = SimpleNamespace(
    +        write_frequency="turn",
    +        dialectic_reasoning_level="low",
    +        dialectic_dynamic=True,
    +        dialectic_max_chars=600,
    +        observation_mode="directional",
    +        user_observe_me=True,
    +        user_observe_others=True,
    +        ai_observe_me=True,
    +        ai_observe_others=ai_observe_others,
    +        message_max_chars=25000,
    +        dialectic_max_input_chars=10000,
    +    )
    +    mgr = HonchoSessionManager(honcho=SimpleNamespace(), config=cfg)
    +    session = HonchoSession(
    +        key="test-session",
    +        user_peer_id="chris",
    +        assistant_peer_id="hermes",
    +        honcho_session_id="test-session",
    +    )
    +    fake_honcho_session = _RecordingHonchoSession()
    +    mgr._cache[session.key] = session
    +    mgr._sessions_cache[session.honcho_session_id] = fake_honcho_session
    +    return mgr, fake_honcho_session
    +
    +
    +def test_session_context_user_alias_uses_assistant_observer_when_ai_can_observe_others():
    +    mgr, fake = _manager_with_cached_session(ai_observe_others=True)
    +
    +    result = mgr.get_session_context("test-session", peer="user")
    +
    +    assert result["summary"] == "summary"
    +    assert fake.calls == [
    +        {
    +            "summary": True,
    +            "peer_target": "chris",
    +            "peer_perspective": "hermes",
    +        }
    +    ]
    +
    +
    +def test_session_context_explicit_user_peer_matches_user_alias():
    +    mgr, fake = _manager_with_cached_session(ai_observe_others=True)
    +
    +    mgr.get_session_context("test-session", peer="chris")
    +
    +    assert fake.calls == [
    +        {
    +            "summary": True,
    +            "peer_target": "chris",
    +            "peer_perspective": "hermes",
    +        }
    +    ]
    +
    +
    +def test_session_context_user_alias_uses_user_self_observer_when_ai_cannot_observe_others():
    +    mgr, fake = _manager_with_cached_session(ai_observe_others=False)
    +
    +    mgr.get_session_context("test-session", peer="user")
    +
    +    assert fake.calls == [
    +        {
    +            "summary": True,
    +            "peer_target": "chris",
    +            "peer_perspective": "chris",
    +        }
    +    ]
    ```
- **#35170: fix(honcho): harden self-hosted setup paths**. merged to main · fix tier `verified`
  - maintainer direction: category `merged`; implication `superseded`
  - files: `gateway/run.py`, `hermes_cli/main.py`, `hermes_cli/memory_setup.py`, `hermes_cli/profiles.py`, `optional-skills/autonomous-ai-agents/honcho/SKILL.md`, `plugins/memory/hindsight/__init__.py`, `plugins/memory/honcho/README.md`, `plugins/memory/honcho/__init__.py`, `plugins/memory/honcho/cli.py`, `plugins/memory/honcho/client.py`, `plugins/memory/mem0/__init__.py`, `plugins/memory/supermemory/__init__.py`, `tests/gateway/test_agent_cache.py`, `tests/hermes_cli/test_memory_setup_provider_arg.py`, `tests/hermes_cli/test_profiles.py`, `tests/honcho_plugin/test_cli.py`, `tests/honcho_plugin/test_client.py`, `tests/honcho_plugin/test_pin_peer_name.py`, `tests/plugins/memory/test_hindsight_provider.py`, `tests/plugins/memory/test_mem0_v2.py` (+5 more)
  - fix hunks:
  - `gateway/run.py`

    ```diff
    @@ -15331,8 +15331,52 @@ async def _run_process_watcher(self, watcher: dict) -> None:
             ("compression", "target_ratio"),
             ("compression", "protect_last_n"),
             ("agent", "disabled_toolsets"),
    +        ("memory", "provider"),
         )
     
    +    _HONCHO_CACHE_BUSTING_KEYS = (
    +        "honcho.peer_name",
    +        "honcho.ai_peer",
    +        "honcho.pin_peer_name",
    +        "honcho.runtime_peer_prefix",
    +        "honcho.user_peer_aliases",
    +    )
    +    _HONCHO_CACHE_BUSTING_MEMO: dict[tuple[str, int | None], dict[str, Any]] = {}
    +
    +    @classmethod
    +    def _empty_honcho_cache_busting_config(cls) -> dict[str, Any]:
    +        return {key: None for key in cls._HONCHO_CACHE_BUSTING_KEYS}
    +
    +    @classmethod
    +    def _extract_honcho_cache_busting_config(cls) -> dict[str, Any]:
    +        """Extract Honcho identity keys, memoized by honcho.json mtime."""
    +        try:
    +            from plugins.memory.honcho.client import HonchoClientConfig, resolve_config_path
    +
    +            path = resolve_config_path()
    +            try:
    +                mtime_ns = path.stat().st_mtime_ns
    +            except OSError:
    +                mtime_ns = None
    +            memo_key = (str(path), mtime_ns)
    +            cached = cls._HONCHO_CACHE_BUSTING_MEMO.get(memo_key)
    +            if cached is not None:
    +                return dict(cached)
    +
    +            hcfg = HonchoClientConfig.from_global_config(config_path=path)
    +            aliases = hcfg.user_peer_aliases or {}
    +            values = {
    +                "honcho.peer_name": hcfg.peer_name,
    +                "honcho.ai_peer": hcfg.ai_peer,
    +                "honcho.pin_peer_name": bool(hcfg.pin_peer_name),
    +                "honcho.runtime_peer_prefix": hcfg.runtime_peer_prefix or "",
    +                "honcho.user_peer_aliases": sorted(aliases.items()) if isinstance(aliases, dict) else [],
    +            }
    +            cls._HONCHO_CACHE_BUSTING_MEMO = {memo_key: values}
    +            return dict(values)
    +        except Exception:
    +            return cls._empty_honcho_cache_busting_config()
    +
         @classmethod
         def _extract_cache_busting_config(cls, user_config: dict | None) -> dict:
             """Pull values that must bust the cached agent.
    ```

    ```diff
    @@ -15363,26 +15407,12 @@ def _extract_cache_busting_config(cls, user_config: dict | None) -> dict:
                 out["tools.registry_generation"] = None
     
             # Honcho identity-mapping keys live in honcho.json, not user_config.
    -        # HonchoSessionManager freezes the resolved peer_name / ai_peer /
    -        # pin / aliases / prefix at construction; without busting here,
    -        # mid-flight honcho.json edits go unread until the next unrelated
    -        # cache eviction.
    -        try:
    -            from plugins.memory.honcho.client import HonchoClientConfig
    -
    -            hcfg = HonchoClientConfig.from_global_config()
    -            out["honcho.peer_name"] = hcfg.peer_name
    -            out["honcho.ai_peer"] = hcfg.ai_peer
    -            out["honcho.pin_peer_name"] = bool(hcfg.pin_peer_name)
    -            out["honcho.runtime_peer_prefix"] = hcfg.runtime_peer_prefix or ""
    -            aliases = hcfg.user_peer_aliases or {}
    -            out["honcho.user_peer_aliases"] = sorted(aliases.items()) if isinstance(aliases, dict) else []
    -        except Exception:
    -            out["honcho.peer_name"] = None
    -            out["honcho.ai_peer"] = None
    -            out["honcho.pin_peer_name"] = None
    -            out["honcho.runtime_peer_prefix"] = None
    -            out["honcho.user_peer_aliases"] = None
    +        # Only read that file when Honcho is the active memory provider.
    +        provider = cfg_get(cfg, "memory", "provider")
    +        if isinstance(provider, str) and provider.lower() == "honcho":
    +            out.update(cls._extract_honcho_cache_busting_config())
    +        else:
    +            out.update(cls._empty_honcho_cache_busting_config())
     
             return out
     
    ```
  - `hermes_cli/main.py`

    ```diff
    @@ -13029,9 +13029,15 @@ def cmd_plugins(args):
             ),
         )
         memory_sub = memory_parser.add_subparsers(dest="memory_command")
    -    memory_sub.add_parser(
    +    _setup_parser = memory_sub.add_parser(
             "setup", help="Interactive provider selection and configuration"
         )
    +    _setup_parser.add_argument(
    +        "provider",
    +        nargs="?",
    +        default=None,
    +        help="Provider to configure directly (e.g. honcho), skipping the picker",
    +    )
         memory_sub.add_parser("status", help="Show current memory provider config")
         memory_sub.add_parser("off", help="Disable external provider (built-in only)")
         _reset_parser = memory_sub.add_parser(
    ```
  - `hermes_cli/memory_setup.py`

    ```diff
    @@ -452,7 +452,11 @@ def memory_command(args) -> None:
         """Route memory subcommands."""
         sub = getattr(args, "memory_command", None)
         if sub == "setup":
    -        cmd_setup(args)
    +        provider = getattr(args, "provider", None)
    +        if provider:
    +            cmd_setup_provider(provider)
    +        else:
    +            cmd_setup(args)
         elif sub == "status":
             cmd_status(args)
         else:
    ```
  - `hermes_cli/profiles.py`

    ```diff
    @@ -1471,8 +1471,9 @@ def import_profile(archive_path: str, name: Optional[str] = None) -> Path:
     
     def _migrate_honcho_profile_host(old_name: str, new_name: str, new_dir: Path) -> None:
         """Rename Honcho host blocks for a renamed profile without changing peers."""
    -    old_host = f"hermes.{old_name}"
    -    new_host = f"hermes.{new_name}"
    +    old_host = f"hermes_{old_name}"
    +    legacy_old_host = f"hermes.{old_name}"
    +    new_host = f"hermes_{new_name}"
     
         candidates = [
             new_dir / "honcho.json",
    ```

    ```diff
    @@ -1496,18 +1497,24 @@ def _migrate_honcho_profile_host(old_name: str, new_name: str, new_dir: Path) ->
                 continue
     
             hosts = raw.get("hosts")
    -        if not isinstance(hosts, dict) or old_host not in hosts:
    +        if not isinstance(hosts, dict):
    +            continue
    +        source_host = old_host if old_host in hosts else legacy_old_host
    +        if source_host not in hosts:
                 continue
     
             if new_host in hosts:
                 print(f"⚠ Honcho host block not migrated: {new_host} already exists in {path}")
                 continue
     
    -        block = hosts[old_host]
    +        block = hosts[source_host]
             if isinstance(block, dict) and "aiPeer" not in block:
    -            bare = old_host.split(".", 1)[1] if "." in old_host else old_host
    +            if source_host.startswith("hermes_"):
    +                bare = source_host.split("_", 1)[1]
    +            else:
    +                bare = source_host.split(".", 1)[1] if "." in source_host else source_host
                 block["aiPeer"] = bare
    -        hosts[new_host] = hosts.pop(old_host)
    +        hosts[new_host] = hosts.pop(source_host)
             tmp = path.with_suffix(path.suffix + ".tmp")
             try:
                 tmp.write_text(json.dumps(raw, indent=2, ensure_ascii=False) + "\n", encoding="utf-8")
    ```

    ```diff
    @@ -1519,7 +1526,7 @@ def _migrate_honcho_profile_host(old_name: str, new_name: str, new_dir: Path) ->
                     pass
                 continue
     
    -        print(f"✓ Honcho host updated: {old_host} → {new_host}")
    +        print(f"✓ Honcho host updated: {source_host} → {new_host}")
     
     
     def rename_profile(old_name: str, new_name: str) -> Path:
    ```
  - `optional-skills/autonomous-ai-agents/honcho/SKILL.md`

    ```diff
    @@ -32,14 +32,14 @@ Honcho provides AI-native cross-session user modeling. It learns who the user is
     ### Cloud (app.honcho.dev)
     
     ```bash
    -hermes honcho setup
    +hermes memory setup honcho
     # select "cloud", paste API key from https://app.honcho.dev
     ```
     
     ### Self-hosted
     
     ```bash
    -hermes honcho setup
    +hermes memory setup honcho
     # select "local", enter base URL (e.g. http://localhost:8000)
     ```
     
    ```
  - `plugins/memory/hindsight/__init__.py`

    ```diff
    @@ -633,7 +633,8 @@ def save_config(self, values, hermes_home):
                 except Exception:
                     pass
             existing.update(values)
    -        config_path.write_text(json.dumps(existing, indent=2))
    +        from utils import atomic_json_write
    +        atomic_json_write(config_path, existing, mode=0o600)
     
         def post_setup(self, hermes_home: str, config: dict) -> None:
             """Custom setup wizard — installs only the deps needed for the selected mode."""
    ```
  - `plugins/memory/honcho/README.md`

    ```diff
    @@ -12,8 +12,8 @@ AI-native cross-session user modeling with multi-pass dialectic reasoning, sessi
     ## Setup
     
     ```bash
    -hermes honcho setup    # full interactive wizard (cloud or local)
    -hermes memory setup    # generic picker, also works
    +hermes memory setup honcho   # configure Honcho directly (works on a fresh install)
    +hermes memory setup          # generic picker, choose Honcho from the list
     ```
     
     Or manually:
    ```

    ```diff
    @@ -22,6 +22,10 @@ hermes config set memory.provider honcho
     echo "HONCHO_API_KEY=***" >> ~/.hermes/.env
     ```
     
    +> `hermes honcho setup` also works, but only **after** Honcho is the active
    +> memory provider — the `honcho` subcommand is registered for the active
    +> provider only. On a fresh install, use `hermes memory setup honcho`.
    +
     ## Architecture Overview
     
     ### Two-Layer Context Injection
    ```

    ```diff
    @@ -109,7 +113,7 @@ Config is read from the first file that exists:
     | 2 | `~/.hermes/honcho.json` | Default profile (shared host blocks) |
     | 3 | `~/.honcho/config.json` | Global (cross-app interop) |
     
    -Host key is derived from the active Hermes profile: `hermes` (default) or `hermes.<profile>`.
    +Host key is derived from the active Hermes profile: `hermes` (default) or `hermes_<profile>`.
     
     For every key, resolution order is: **host block > root > env var > default**.
     
    ```

    ```diff
    @@ -154,7 +158,7 @@ In gateway deployments (Telegram, Discord, Slack, etc.) each user arrives with a
     
     **Host vs root semantics.** All three keys are accepted at both root and `hosts.<host>` levels. Host-level wins. For maps and prefixes, host-level *replaces* the root value as a whole (not merge), so a host can intentionally own its identity universe or wipe it with `userPeerAliases: {}` / `runtimePeerPrefix: ""`.
     
    -**Deployment shapes** (`hermes honcho setup` asks one prompt to set these):
    +**Deployment shapes** (`hermes memory setup honcho` asks one prompt to set these):
     
     - **Single-operator** — `pinUserPeer: true`. All gateway users → `peerName`. Recommended for personal use where you connect Hermes to your own Telegram/Discord/etc.
     - **Multi-user gateway** — `pinUserPeer: false`, optional `runtimePeerPrefix`. Each runtime user → own peer. Recommended for bots serving many humans.
    ```

    ```diff
    @@ -225,7 +229,7 @@ Multiple Hermes profiles can share one workspace while maintaining separate AI i
           "recallMode": "hybrid",
           "sessionStrategy": "per-directory"
         },
    -    "hermes.coder": {
    +    "hermes_coder": {
           "aiPeer": "coder",
           "recallMode": "tools",
           "sessionStrategy": "per-repo"
    ```

    ```diff
    @@ -236,7 +240,7 @@ Multiple Hermes profiles can share one workspace while maintaining separate AI i
     
     Both profiles see the same user (`yourname`) in the same shared environment (`hermes`), but each AI peer builds its own observations, conclusions, and behavior patterns. The coder's memory stays code-oriented; the main agent's stays broad.
     
    -Host key is derived from the active Hermes profile: `hermes` (default) or `hermes.<profile>` (e.g. `hermes -p coder` → host key `hermes.coder`).
    +Host key is derived from the active Hermes profile: `hermes` (default) or `hermes_<profile>` (e.g. `hermes -p coder` -> host key `hermes_coder`). Older `hermes.<profile>` host blocks are still read for compatibility and are migrated when the CLI writes profile-scoped Honcho config.
     
     ### Dialectic & Reasoning
     
    ```

    ```diff
    @@ -307,7 +311,8 @@ Presets:
     
     | Command | Description |
     |---------|-------------|
    -| `hermes honcho setup` | Full interactive setup wizard |
    +| `hermes memory setup honcho` | Configure Honcho directly — works on a fresh install |
    +| `hermes honcho setup` | Interactive setup wizard (only registered once Honcho is the active provider; redirects to `hermes memory setup`) |
     | `hermes honcho status` | Show resolved config for active profile |
     | `hermes honcho enable` / `disable` | Toggle Honcho for active profile |
     | `hermes honcho mode <mode>` | Change recall or observation mode |
    ```

    ```diff
    @@ -344,7 +349,7 @@ Presets:
           "dialecticMaxChars": 600,
           "saveMessages": true
         },
    -    "hermes.coder": {
    +    "hermes_coder": {
           "enabled": true,
           "aiPeer": "coder",
           "sessionStrategy": "per-repo",
    ```
  - `plugins/memory/honcho/__init__.py`

    ```diff
    @@ -249,6 +249,7 @@ def is_available(self) -> bool:
         def save_config(self, values, hermes_home):
             """Write config to $HERMES_HOME/honcho.json (Honcho SDK native format)."""
             import json
    +        import os
             from pathlib import Path
             config_path = Path(hermes_home) / "honcho.json"
             existing = {}
    ```

    ```diff
    @@ -258,7 +259,8 @@ def save_config(self, values, hermes_home):
                 except Exception:
                     pass
             existing.update(values)
    -        config_path.write_text(json.dumps(existing, indent=2))
    +        from utils import atomic_json_write
    +        atomic_json_write(config_path, existing, mode=0o600)
     
         def get_config_schema(self):
             return [
    ```
  - `plugins/memory/honcho/cli.py`

    ```diff
    @@ -11,7 +11,7 @@
     from pathlib import Path
     
     from hermes_constants import get_hermes_home
    -from plugins.memory.honcho.client import resolve_active_host, resolve_config_path, HOST
    +from plugins.memory.honcho.client import _host_block, profile_host_key, resolve_active_host, resolve_config_path, HOST
     from hermes_cli.config import cfg_get
     
     
    ```

    ```diff
    @@ -36,7 +36,7 @@ def clone_honcho_for_profile(profile_name: str) -> bool:
         if not default_block and not has_key:
             return False
     
    -    new_host = f"{HOST}.{profile_name}"
    +    new_host = profile_host_key(profile_name)
         if new_host in hosts:
             return False  # already exists
     
    ```

    ```diff
    @@ -192,7 +192,7 @@ def cmd_sync(args) -> None:
             if p.name == "default":
                 continue
             if clone_honcho_for_profile(p.name):
    -            print(f"  + {p.name} -> hermes.{p.name}")
    +            print(f"  + {p.name} -> {profile_host_key(p.name)}")
                 created += 1
             else:
                 skipped += 1
    ```

    ```diff
    @@ -243,7 +243,7 @@ def _host_key() -> str:
         if _profile_override:
             if _profile_override in {"default", "custom"}:
                 return HOST
    -        return f"{HOST}.{_profile_override}"
    +        return profile_host_key(_profile_override)
         return resolve_active_host()
     
     
    ```

    ```diff
    @@ -275,10 +275,8 @@ def _read_config() -> dict:
     def _write_config(cfg: dict, path: Path | None = None) -> None:
         path = path or _local_config_path()
         path.parent.mkdir(parents=True, exist_ok=True)
    -    path.write_text(
    -        json.dumps(cfg, indent=2, ensure_ascii=False) + "\n",
    -        encoding="utf-8",
    -    )
    +    from utils import atomic_json_write
    +    atomic_json_write(path, cfg, mode=0o600)
     
     
     def _resolve_api_key(cfg: dict) -> str:
    ```

    ```diff
    @@ -292,7 +290,7 @@ def _resolve_api_key(cfg: dict) -> str:
         config shapes, e.g. ``localhost:8000``) still pass — the Honcho SDK
         will reject them itself with a clearer error than ours.
         """
    -    host_key = ((cfg.get("hosts") or {}).get(_host_key()) or {}).get("apiKey")
    +    host_key = _host_block(cfg, _host_key()).get("apiKey")
         key = host_key or cfg.get("apiKey", "") or os.environ.get("HONCHO_API_KEY", "")
         if not key:
             base_url = cfg.get("baseUrl") or cfg.get("base_url") or os.environ.get("HONCHO_BASE_URL", "")
    ```

    ```diff
    @@ -462,21 +460,58 @@ def cmd_setup(args) -> None:
         cfg.pop("base_url", None)
     
         if is_local:
    -        # --- Local: ask for base URL, skip or clear API key ---
    +        # --- Local: ask for base URL, optionally accept a JWT for auth ---
             current_url = cfg.get("baseUrl") or ""
             new_url = _prompt("Base URL", default=current_url or "http://localhost:8000")
             if new_url:
                 cfg["baseUrl"] = new_url
     
    -        # For local no-auth, the SDK must not send an API key.
    -        # We keep the key in config (for cloud switching later) but
    -        # the client should skip auth when baseUrl is local.
    -        current_key = cfg.get("apiKey", "")
    -        if current_key:
    -            print(f"\n  API key present in config (kept for cloud/hybrid use).")
    -            print("  Local connections will skip auth automatically.")
    +        # Self-hosted Honcho can run with AUTH_USE_AUTH=true and an
    +        # AUTH_JWT_SECRET on the server side. In that case clients must
    +        # send a JWT signed with that secret as the bearer token (the
    +        # Honcho SDK takes it via ``api_key=``). Cloud users got prompted
    +        # for a key already; the local path historically skipped this and
    +        # forced users to disable auth on the server. Offer the prompt
    +        # here too. We store it under the host block (not the top-level
    +        # apiKey) so ``get_honcho_client`` recognises it as an explicit
    +        # local auth opt-in (see ``_host_has_key`` in client.py) and
    +        # cloud/hybrid switching is unaffected.
    +        current_host_key = hermes_host.get("apiKey", "")
    +        masked = (
    +            f"...{current_host_key[-8:]}"
    +            if len(current_host_key) > 8
    +            else ("set" if current_host_key else "not set")
    +        )
    +        print(
    +            "\n  Local Honcho auth (JWT signed with the server's "
    +            "AUTH_JWT_SECRET)."
    +        )
    +        print(
    +            "  Leave blank if your server runs with AUTH_USE_AUTH=false. "
    +            f"Current: {masked}"
    +        )
    +        new_local_key = _prompt(
    +            "Local JWT / bearer token (blank to skip / keep current)",
    +            secret=True,
    +        )
    +        if new_local_key:
    +            hermes_host["apiKey"] = new_local_key
    +        elif current_host_key:
    +            print("  Keeping existing local JWT.")
             else:
    -            print("\n  No API key set. Local no-auth ready.")
    +            # Surface the top-level key situation for transparency.
    +            top_key = cfg.get("apiKey", "")
    +            if top_key:
    +                print(
    +                    "\n  Top-level API key present in config (kept for "
    +                    "cloud/hybrid use)."
    +                )
    +                print(
    +                    "  Local connections will skip auth automatically "
    +                    "until a local JWT is set above."
    +                )
    +            else:
    +                print("\n  No local JWT set. Local no-auth ready.")
         else:
             # --- Cloud: set default base URL, require API key ---
             cfg.pop("baseUrl", None)  # cloud uses SDK default
    ```
  - `plugins/memory/honcho/client.py`

    ```diff
    @@ -32,6 +32,24 @@
     HOST = "hermes"
     
     
    +def profile_host_key(profile: str | None) -> str:
    +    """Return the safe Honcho host key for a Hermes profile."""
    +    if not profile or profile in {"default", "custom"}:
    +        return HOST
    +    sanitized = "".join(c if c.isalnum() or c in "_-" else "_" for c in profile).strip("_")
    +    return f"{HOST}_{sanitized or 'profile'}"
    +
    +
    +def _host_block(raw: dict, host: str) -> dict:
    +    """Return host config, accepting legacy dot-form profile host keys."""
    +    hosts = raw.get("hosts") or {}
    +    block = hosts.get(host, {})
    +    if block or not host.startswith(f"{HOST}_"):
    +        return block
    +    legacy = f"{HOST}.{host[len(HOST) + 1:]}"
    +    return hosts.get(legacy, {})
    +
    +
     def resolve_active_host() -> str:
         """Derive the Honcho host key from the active Hermes profile.
     
    ```

    ```diff
    @@ -47,8 +65,7 @@ def resolve_active_host() -> str:
         try:
             from hermes_cli.profiles import get_active_profile_name
             profile = get_active_profile_name()
    -        if profile and profile not in {"default", "custom"}:
    -            return f"{HOST}.{profile}"
    +        return profile_host_key(profile)
         except Exception:
             pass
         return HOST
    ```

    ```diff
    @@ -406,7 +423,7 @@ def from_global_config(
                 logger.warning("Failed to read %s: %s, falling back to env", path, e)
                 return cls.from_env(host=resolved_host)
     
    -        host_block = (raw.get("hosts") or {}).get(resolved_host, {})
    +        host_block = _host_block(raw, resolved_host)
             # A hosts.hermes block or explicit enabled flag means the user
             # intentionally configured Honcho for this host.
             _explicitly_configured = bool(host_block) or raw.get("enabled") is True
    ```

    ```diff
    @@ -811,7 +828,10 @@ def get_honcho_client(config: HonchoClientConfig | None = None) -> Honcho:
             or "::1" in resolved_base_url
         )
         if _is_local:
    -        # Check if the host block has its own apiKey (explicit local auth)
    +        # Check if the host block has its own apiKey (explicit local auth).
    +        # Auth-skipping is loopback-only: a stored key is likely a cloud key
    +        # that would break a no-auth local server, so we substitute the SDK's
    +        # required-non-empty placeholder unless the host block opts in.
             _raw = config.raw or {}
             _host_block = (_raw.get("hosts") or {}).get(config.host, {})
             _host_has_key = bool(_host_block.get("apiKey"))
    ```

    ```diff
    @@ -819,6 +839,18 @@ def get_honcho_client(config: HonchoClientConfig | None = None) -> Honcho:
         else:
             effective_api_key = [redacted:secret-assignment]
     
    +    # The Honcho SDK's route builders (e.g. routes.workspaces()) already
    +    # include the version prefix (e.g. "/v3/workspaces").  When a user-supplied
    +    # base_url already ends in a version segment (e.g.
    +    # "http://localhost:38000/v3", "https://honcho.my.ts.net/v3"), concatenating
    +    # the two produces "/v3/v3/workspaces" → 404 on every call.  This is a pure
    +    # routing concern independent of host, so strip a trailing version segment
    +    # from ANY base_url — loopback, LAN, custom domain, or cloud alike.  The
    +    # SDK then appends its own versioned paths correctly.
    +    if resolved_base_url:
    +        import re as _re
    +        resolved_base_url = _re.sub(r"/v\d+/*$", "", resolved_base_url).rstrip("/")
    +
         kwargs: dict = {
             "workspace_id": config.workspace_id,
             "api_key": effective_api_key,
    ```
  - `plugins/memory/mem0/__init__.py`

    ```diff
    @@ -155,7 +155,8 @@ def save_config(self, values, hermes_home):
                 except Exception:
                     pass
             existing.update(values)
    -        config_path.write_text(json.dumps(existing, indent=2))
    +        from utils import atomic_json_write
    +        atomic_json_write(config_path, existing, mode=0o600)
     
         def get_config_schema(self):
             return [
    ```
  - `plugins/memory/supermemory/__init__.py`

    ```diff
    @@ -152,7 +152,8 @@ def _save_supermemory_config(values: dict, hermes_home: str) -> None:
             except Exception:
                 existing = {}
         existing.update(values)
    -    config_path.write_text(json.dumps(existing, indent=2, sort_keys=True) + "\n", encoding="utf-8")
    +    from utils import atomic_json_write
    +    atomic_json_write(config_path, existing, mode=0o600, sort_keys=True)
     
     
     def _detect_category(text: str) -> str:
    ```
  - `tests/gateway/test_agent_cache.py`

    ```diff
    @@ -276,6 +276,111 @@ def test_extract_includes_live_tool_registry_generation(self, monkeypatch):
     
             assert out["tools.registry_generation"] == 12345
     
    +
    +    def test_skips_honcho_config_read_when_provider_is_not_honcho(self, monkeypatch):
    +        """Non-Honcho gateways must not read/parse honcho.json on every message."""
    +        from gateway.run import GatewayRunner
    +
    +        called = False
    +
    +        def _boom():
    +            nonlocal called
    +            called = True
    +            raise AssertionError("should not read Honcho config")
    +
    +        monkeypatch.setattr(GatewayRunner, "_extract_honcho_cache_busting_config", _boom)
    +
    +        out = GatewayRunner._extract_cache_busting_config({"memory": {"provider": "mem0"}})
    +
    +        assert called is False
    +        assert out["honcho.peer_name"] is None
    +        assert out["honcho.user_peer_aliases"] is None
    +
    +    def test_reads_honcho_config_only_when_provider_is_honcho(self, monkeypatch):
    +        from gateway.run import GatewayRunner
    +
    +        calls = []
    +
    +        def _fake():
    +            calls.append(True)
    +            return {
    +                "honcho.peer_name": "eri",
    +                "honcho.ai_peer": "hermes",
    +                "honcho.pin_peer_name": True,
    +                "honcho.runtime_peer_prefix": "tg_",
    +                "honcho.user_peer_aliases": [("123", "eri")],
    +            }
    +
    +        monkeypatch.setattr(GatewayRunner, "_extract_honcho_cache_busting_config", _fake)
    +
    +        out = GatewayRunner._extract_cache_busting_config({"memory": {"provider": "honcho"}})
    +
    +        assert calls == [True]
    +        assert out["honcho.peer_name"] == "eri"
    +        assert out["honcho.user_peer_aliases"] == [("123", "eri")]
    +
    +    def test_memory_provider_change_busts_signature(self, monkeypatch):
    +        """Switching memory.provider must itself change the cache-busting
    +        signature, so the agent is rebuilt when a user swaps providers
    +        mid-gateway (independent of the honcho.json identity keys)."""
    +        from gateway.run import GatewayRunner
    +
    +        # Neutralize honcho.json reads so the only varying input is the
    +        # provider value itself.
    +        monkeypatch.setattr(
    +            GatewayRunner,
    +            "_extract_honcho_cache_busting_config",
    +            classmethod(lambda cls: cls._empty_honcho_cache_busting_config()),
    +        )
    +
    +        sig_honcho = GatewayRunner._extract_cache_busting_config({"memory": {"provider": "honcho"}})
    +        sig_mem0 = GatewayRunner._extract_cache_busting_config({"memory": {"provider": "mem0"}})
    +
    +        assert sig_honcho["memory.provider"] == "honcho"
    +        assert sig_mem0["memory.provider"] == "mem0"
    +        assert sig_honcho != sig_mem0
    +
    +    def test_honcho_cache_busting_config_memoized_by_mtime(self, monkeypatch, tmp_path):
    +        """Repeated Honcho extraction for unchanged honcho.json should reuse parse result."""
    +        from types import SimpleNamespace
    +        from gateway.run import GatewayRunner
    +
    +        config_path = tmp_path / "honcho.json"
    +        config_path.write_text("{}")
    +        parse_calls = []
    +
    +        class FakeConfig:
    +            peer_name = "eri"
    +            ai_peer = "hermes"
    +            pin_peer_name = False
    +            runtime_peer_prefix = "tg_"
    +            user_peer_aliases = {"123": "eri"}
    +
    +            @classmethod
    +            def from_global_config(cls, config_path=None):
    +                parse_calls.append(config_path)
    +                return cls()
    +
    +        fake_client = SimpleNamespace(
    +            HonchoClientConfig=FakeConfig,
    +            resolve_config_path=lambda: config_path,
    +        )
    +        monkeypatch.setitem(__import__("sys").modules, "plugins.memory.honcho.client", fake_client)
    +        monkeypatch.setattr(GatewayRunner, "_HONCHO_CACHE_BUSTING_MEMO", {})
    +
    +        first = GatewayRunner._extract_honcho_cache_busting_config()
    +        second = GatewayRunner._extract_honcho_cache_busting_config()
    +
    +        assert first == second
    +        assert first["honcho.user_peer_aliases"] == [("123", "eri")]
    +        assert parse_calls == [config_path]
    +
    +        config_path.write_text("{\n  \"changed\": true\n}")
    +        third = GatewayRunner._extract_honcho_cache_busting_config()
    +
    +        assert third == first
    +        assert parse_calls == [config_path, config_path]
    +
         def test_full_round_trip_busts_cache_on_real_edit(self):
             """End-to-end: simulate a config edit on main and verify the
             extracted cache_keys change produces a new signature."""
    ```
  - `tests/hermes_cli/test_memory_setup_provider_arg.py`

    ```diff
    @@ -0,0 +1,50 @@
    +"""Tests for `hermes memory setup [provider]` routing.
    +
    +The `memory setup` subcommand accepts an optional positional ``provider`` so a
    +fresh install can configure a specific provider directly (e.g.
    +``hermes memory setup honcho``) without the interactive picker — which matters
    +because the per-provider ``hermes <provider>`` subcommand is only registered
    +once that provider is active.
    +"""
    +
    +from types import SimpleNamespace
    +from unittest.mock import patch
    +
    +from hermes_cli import memory_setup
    +
    +
    +class TestMemorySetupProviderRouting:
    +    def test_setup_with_provider_arg_skips_picker(self):
    +        """`memory setup honcho` routes straight to cmd_setup_provider."""
    +        args = SimpleNamespace(memory_command="setup", provider="honcho")
    +        with patch.object(memory_setup, "cmd_setup_provider") as direct, \
    +             patch.object(memory_setup, "cmd_setup") as picker:
    +            memory_setup.memory_command(args)
    +        direct.assert_called_once_with("honcho")
    +        picker.assert_not_called()
    +
    +    def test_setup_without_provider_runs_picker(self):
    +        """`memory setup` (no provider) runs the interactive picker."""
    +        args = SimpleNamespace(memory_command="setup", provider=None)
    +        with patch.object(memory_setup, "cmd_setup_provider") as direct, \
    +             patch.object(memory_setup, "cmd_setup") as picker:
    +            memory_setup.memory_command(args)
    +        picker.assert_called_once_with(args)
    +        direct.assert_not_called()
    +
    +    def test_setup_with_missing_provider_attr_runs_picker(self):
    +        """A SimpleNamespace lacking `provider` must not crash — fall back to picker."""
    +        args = SimpleNamespace(memory_command="setup")
    +        with patch.object(memory_setup, "cmd_setup_provider") as direct, \
    +             patch.object(memory_setup, "cmd_setup") as picker:
    +            memory_setup.memory_command(args)
    +        picker.assert_called_once_with(args)
    +        direct.assert_not_called()
    +
    +    def test_unknown_provider_reports_and_returns_early(self, capsys):
    +        """An unknown provider name surfaces a helpful message and returns
    +        before any config load/save (the not-found guard precedes those imports)."""
    +        memory_setup.cmd_setup_provider("notaprovider")
    +        out = capsys.readouterr().out
    +        assert "not found" in out
    +        assert "hermes memory setup" in out
    ```
  - `tests/hermes_cli/test_profiles.py`

    ```diff
    @@ -754,8 +754,8 @@ def test_renames_root_honcho_host_without_changing_ai_peer(self, profile_env):
     
             cfg = json.loads(honcho_path.read_text())
             assert "hermes.ssi_health" not in cfg["hosts"]
    -        assert cfg["hosts"]["hermes.heimdall"]["aiPeer"] == "ssi_health"
    -        assert cfg["hosts"]["hermes.heimdall"]["peerName"] == "user-peer"
    +        assert cfg["hosts"]["hermes_heimdall"]["aiPeer"] == "ssi_health"
    +        assert cfg["hosts"]["hermes_heimdall"]["peerName"] == "user-peer"
     
         def test_pins_ai_peer_when_absent_on_honcho_host_rename(self, profile_env):
             tmp_path = profile_env
    ```

    ```diff
    @@ -772,8 +772,8 @@ def test_pins_ai_peer_when_absent_on_honcho_host_rename(self, profile_env):
     
             cfg = json.loads(honcho_path.read_text())
             assert "hermes.ssi_health" not in cfg["hosts"]
    -        assert cfg["hosts"]["hermes.heimdall"]["aiPeer"] == "ssi_health"
    -        assert cfg["hosts"]["hermes.heimdall"]["workspace"] == "hermes"
    +        assert cfg["hosts"]["hermes_heimdall"]["aiPeer"] == "ssi_health"
    +        assert cfg["hosts"]["hermes_heimdall"]["workspace"] == "hermes"
     
         def test_does_not_overwrite_existing_honcho_host_on_rename(self, profile_env):
             tmp_path = profile_env
    ```

    ```diff
    @@ -782,7 +782,7 @@ def test_does_not_overwrite_existing_honcho_host_on_rename(self, profile_env):
             honcho_path.write_text(json.dumps({
                 "hosts": {
                     "hermes.ssi_health": {"aiPeer": "ssi_health"},
    -                "hermes.heimdall": {"aiPeer": "heimdall"},
    +                "hermes_heimdall": {"aiPeer": "heimdall"},
                 }
             }))
     
    ```

    ```diff
    @@ -791,7 +791,7 @@ def test_does_not_overwrite_existing_honcho_host_on_rename(self, profile_env):
     
             cfg = json.loads(honcho_path.read_text())
             assert cfg["hosts"]["hermes.ssi_health"]["aiPeer"] == "ssi_health"
    -        assert cfg["hosts"]["hermes.heimdall"]["aiPeer"] == "heimdall"
    +        assert cfg["hosts"]["hermes_heimdall"]["aiPeer"] == "heimdall"
     
         def test_default_raises_value_error(self, profile_env):
             with pytest.raises(ValueError, match="default"):
    ```
  - `tests/honcho_plugin/test_cli.py`

    ```diff
    @@ -1,6 +1,7 @@
     """Tests for plugins/memory/honcho/cli.py."""
     
     from types import SimpleNamespace
    +import json
     
     
     class TestResolveApiKey:
    ```

    ```diff
    @@ -100,6 +101,84 @@ def test_accepts_legacy_schemeless_host(self, monkeypatch):
                     f"expected local sentinel for legacy schemeless {legacy!r}"
     
     
    +class TestCmdSetupLocalJwt:
    +    """Local-deployment setup must allow configuring a JWT for AUTH_JWT_SECRET-backed Honcho servers."""
    +
    +    def _run_setup(self, monkeypatch, tmp_path, initial_cfg, prompt_answers):
    +        import plugins.memory.honcho.cli as honcho_cli
    +
    +        # Avoid touching real config / SDK / filesystem.
    +        cfg_path = tmp_path / "honcho.json"
    +        monkeypatch.setattr(honcho_cli, "_read_config", lambda: dict(initial_cfg))
    +        monkeypatch.setattr(honcho_cli, "_local_config_path", lambda: cfg_path)
    +        monkeypatch.setattr(honcho_cli, "_config_path", lambda: cfg_path)
    +        monkeypatch.setattr(honcho_cli, "_host_key", lambda: "hermes")
    +        monkeypatch.setattr(honcho_cli, "_ensure_sdk_installed", lambda: True)
    +
    +        written = {}
    +
    +        def _capture_write(cfg, path=None):
    +            written["cfg"] = cfg
    +            written["path"] = path
    +
    +        monkeypatch.setattr(honcho_cli, "_write_config", _capture_write)
    +
    +        # Feed scripted prompt answers in order.
    +        answers = list(prompt_answers)
    +
    +        def _fake_prompt(label, default=None, secret=[redacted:secret-assignment]
    +            if not answers:
    +                # Default-through any remaining prompts to keep the wizard moving.
    +                return default or ""
    +            return answers.pop(0)
    +
    +        monkeypatch.setattr(honcho_cli, "_prompt", _fake_prompt)
    +
    +        honcho_cli.cmd_setup(SimpleNamespace())
    +        return written.get("cfg")
    +
    +    def test_local_setup_stores_jwt_under_host_block(self, monkeypatch, tmp_path):
    +        """Self-hosted users supplying a JWT must have it written under hosts.<host>.apiKey,
    +        not as the top-level cloud apiKey, so cloud/hybrid switching is preserved and
    +        get_honcho_client treats it as an explicit local auth opt-in."""
    +        cfg = self._run_setup(
    +            monkeypatch,
    +            tmp_path,
    +            initial_cfg={},
    +            prompt_answers=[
    +                "local",                       # deployment
    +                "http://localhost:8000",       # base URL
    +                "my-local-jwt-token",          # local JWT
    +            ],
    +        )
    +        assert cfg is not None
    +        assert cfg.get("baseUrl") == "http://localhost:8000"
    +        # Top-level apiKey must remain unset (cloud field).
    +        assert not cfg.get("apiKey")
    +        # The new local JWT belongs under the host block.
    +        host_block = (cfg.get("hosts") or {}).get("hermes") or {}
    +        assert host_block.get("apiKey") == "my-local-jwt-token"
    +
    +    def test_local_setup_blank_jwt_keeps_local_no_auth(self, monkeypatch, tmp_path):
    +        """Blank JWT prompt response on a fresh local config must not introduce an apiKey
    +        anywhere (local no-auth Honcho deployments must still work out of the box)."""
    +        cfg = self._run_setup(
    +            monkeypatch,
    +            tmp_path,
    +            initial_cfg={},
    +            prompt_answers=[
    +                "local",
    +                "http://localhost:8000",
    +                "",  # blank JWT
    +            ],
    +        )
    +        assert cfg is not None
    +        assert cfg.get("baseUrl") == "http://localhost:8000"
    +        assert not cfg.get("apiKey")
    +        host_block = (cfg.get("hosts") or {}).get("hermes") or {}
    +        assert not host_block.get("apiKey")
    +
    +
     class TestCmdStatus:
         def test_reports_connection_failure_when_session_setup_fails(self, monkeypatch, capsys, tmp_path):
             import plugins.memory.honcho.cli as honcho_cli
    ```

    ```diff
    @@ -192,7 +271,7 @@ def test_user_peer_aliases_carry_into_cloned_profile(self, monkeypatch, tmp_path
             honcho_cli, written = self._setup_clone_env(monkeypatch, tmp_path, cfg)
             ok = honcho_cli.clone_honcho_for_profile("coder")
             assert ok is True
    -        new_block = written["cfg"]["hosts"]["hermes.coder"]
    +        new_block = written["cfg"]["hosts"]["hermes_coder"]
             assert new_block["userPeerAliases"] == {"86701400": "eri", "discord-491827364": "eri"}
     
         def test_runtime_peer_prefix_carries_into_cloned_profile(self, monkeypatch, tmp_path):
    ```

    ```diff
    @@ -208,7 +287,7 @@ def test_runtime_peer_prefix_carries_into_cloned_profile(self, monkeypatch, tmp_
             honcho_cli, written = self._setup_clone_env(monkeypatch, tmp_path, cfg)
             ok = honcho_cli.clone_honcho_for_profile("coder")
             assert ok is True
    -        new_block = written["cfg"]["hosts"]["hermes.coder"]
    +        new_block = written["cfg"]["hosts"]["hermes_coder"]
             assert new_block["runtimePeerPrefix"] == "telegram_"
     
         def test_pin_peer_name_carries_into_cloned_profile(self, monkeypatch, tmp_path):
    ```

    ```diff
    @@ -224,7 +303,7 @@ def test_pin_peer_name_carries_into_cloned_profile(self, monkeypatch, tmp_path):
             honcho_cli, written = self._setup_clone_env(monkeypatch, tmp_path, cfg)
             ok = honcho_cli.clone_honcho_for_profile("coder")
             assert ok is True
    -        new_block = written["cfg"]["hosts"]["hermes.coder"]
    +        new_block = written["cfg"]["hosts"]["hermes_coder"]
             assert new_block["pinPeerName"] is True
     
         def test_unset_identity_keys_do_not_appear_in_cloned_profile(self, monkeypatch, tmp_path):
    ```

    ```diff
    @@ -235,7 +314,7 @@ def test_unset_identity_keys_do_not_appear_in_cloned_profile(self, monkeypatch,
             honcho_cli, written = self._setup_clone_env(monkeypatch, tmp_path, cfg)
             ok = honcho_cli.clone_honcho_for_profile("coder")
             assert ok is True
    -        new_block = written["cfg"]["hosts"]["hermes.coder"]
    +        new_block = written["cfg"]["hosts"]["hermes_coder"]
             assert "userPeerAliases" not in new_block
             assert "runtimePeerPrefix" not in new_block
             assert "pinPeerName" not in new_block
    ```

    ```diff
    @@ -572,5 +651,5 @@ def test_clone_inherits_host_pin_user_peer(self, monkeypatch, tmp_path):
     
             ok = honcho_cli.clone_honcho_for_profile("partner")
             assert ok is True
    -        new_block = written["cfg"]["hosts"]["hermes.partner"]
    +        new_block = written["cfg"]["hosts"]["hermes_partner"]
             assert new_block["pinUserPeer"] is True
    ```
  - `tests/honcho_plugin/test_client.py`

    ```diff
    @@ -13,6 +13,7 @@
     from plugins.memory.honcho.client import (
         HonchoClientConfig,
         get_honcho_client,
    +    profile_host_key,
         reset_honcho_client,
         resolve_active_host,
         resolve_config_path,
    ```

    ```diff
    @@ -430,6 +431,10 @@ def test_from_global_config_uses_local_path(self, tmp_path):
     
     
     class TestResolveActiveHost:
    +    def test_profile_host_key_uses_honcho_safe_separator(self):
    +        assert profile_host_key("coder") == "hermes_coder"
    +        assert profile_host_key("default") == "hermes"
    +
         def test_default_returns_hermes(self):
             with patch.dict(os.environ, {}, clear=True):
                 os.environ.pop("HERMES_HONCHO_HOST", None)
    ```

    ```diff
    @@ -444,7 +449,7 @@ def test_profile_name_derives_host(self):
             with patch.dict(os.environ, {}, clear=False):
                 os.environ.pop("HERMES_HONCHO_HOST", None)
                 with patch("hermes_cli.profiles.get_active_profile_name", return_value="coder"):
    -                assert resolve_active_host() == "hermes.coder"
    +                assert resolve_active_host() == "hermes_coder"
     
         def test_default_profile_returns_hermes(self):
             with patch.dict(os.environ, {}, clear=False):
    ```

    ```diff
    @@ -477,10 +482,10 @@ def test_profiles_import_failure_falls_back(self):
     class TestProfileScopedConfig:
         def test_from_env_uses_profile_host(self):
             with patch.dict(os.environ, {"HONCHO_API_KEY": "key"}):
    -            config = HonchoClientConfig.from_env(host="hermes.coder")
    -        assert config.host == "hermes.coder"
    +            config = HonchoClientConfig.from_env(host="hermes_coder")
    +        assert config.host == "hermes_coder"
             assert config.workspace_id == "hermes"  # shared workspace
    -        assert config.ai_peer == "hermes.coder"
    +        assert config.ai_peer == "hermes_coder"
     
         def test_from_env_default_workspace_preserved_for_default_host(self):
             with patch.dict(os.environ, {"HONCHO_API_KEY": "key"}):
    ```

    ```diff
    @@ -494,19 +499,19 @@ def test_from_global_config_reads_profile_host_block(self, tmp_path):
                 "apiKey": "shared-key",
                 "hosts": {
                     "hermes": {"aiPeer": "hermes", "peerName": "alice"},
    -                "hermes.coder": {
    -                    "aiPeer": "hermes.coder",
    +                "hermes_coder": {
    +                    "aiPeer": "hermes_coder",
                         "peerName": "alice-coder",
                         "workspace": "coder-ws",
                     },
                 },
             }))
             config = HonchoClientConfig.from_global_config(
    -            host="hermes.coder", config_path=config_file,
    +            host="hermes_coder", config_path=config_file,
             )
    -        assert config.host == "hermes.coder"
    +        assert config.host == "hermes_coder"
             assert config.workspace_id == "coder-ws"
    -        assert config.ai_peer == "hermes.coder"
    +        assert config.ai_peer == "hermes_coder"
             assert config.peer_name == "alice-coder"
     
         def test_from_global_config_auto_resolves_host(self, tmp_path):
    ```

    ```diff
    @@ -514,14 +519,30 @@ def test_from_global_config_auto_resolves_host(self, tmp_path):
             config_file.write_text(json.dumps({
                 "apiKey": "key",
                 "hosts": {
    -                "hermes.dreamer": {"peerName": "dreamer-user"},
    +                "hermes_dreamer": {"peerName": "dreamer-user"},
                 },
             }))
    -        with patch("plugins.memory.honcho.client.resolve_active_host", return_value="hermes.dreamer"):
    +        with patch("plugins.memory.honcho.client.resolve_active_host", return_value="hermes_dreamer"):
                 config = HonchoClientConfig.from_global_config(config_path=config_file)
    -        assert config.host == "hermes.dreamer"
    +        assert config.host == "hermes_dreamer"
             assert config.peer_name == "dreamer-user"
     
    +    def test_from_global_config_reads_legacy_dot_profile_host_block(self, tmp_path):
    +        config_file = tmp_path / "config.json"
    +        config_file.write_text(json.dumps({
    +            "apiKey": "key",
    +            "hosts": {
    +                "hermes.dreamer": {"peerName": "dreamer-user"},
    +            },
    +        }))
    +        config = HonchoClientConfig.from_global_config(
    +            host="hermes_dreamer",
    +            config_path=config_file,
    +        )
    +        assert config.host == "hermes_dreamer"
    +        assert config.peer_name == "dreamer-user"
    +        assert config.workspace_id == "hermes_dreamer"
    +
     
     class TestObservationModeMigration:
         """Existing configs without explicit observationMode keep 'unified' default."""
    ```

    ```diff
    @@ -890,3 +911,176 @@ def test_depth_levels_invalid_values_default_to_low(self, tmp_path):
             }))
             config = HonchoClientConfig.from_global_config(config_path=config_file)
             assert config.dialectic_depth_levels == ["low", "high"]
    +
    +
    +class TestGetHonchoClientBaseUrlDoublePrefixFix:
    +    """Regression tests for #20688 — Honcho SDK double-prefixing of /v3 for
    +    self-hosted instances where base_url already contains a version path."""
    +
    +    def teardown_method(self):
    +        reset_honcho_client()
    +
    +    @pytest.mark.skipif(
    +        not importlib.util.find_spec("honcho"),
    +        reason="honcho SDK not installed"
    +    )
    +    def test_local_base_url_with_v3_suffix_stripped(self):
    +        """base_url 'http://localhost:38000/v3' must become 'http://localhost:38000'
    +        before passing to the Honcho SDK to avoid double '/v3/v3' prefixing."""
    +        fake_honcho = MagicMock(name="Honcho")
    +        cfg = HonchoClientConfig(
    +            api_key=None,
    +            base_url="http://localhost:38000/v3",
    +            workspace_id="hermes",
    +            environment="production",
    +        )
    +
    +        with patch("honcho.Honcho", return_value=fake_honcho) as mock_honcho, \
    +             patch("hermes_cli.config.load_config", return_value={}):
    +            get_honcho_client(cfg)
    +
    +        mock_honcho.assert_called_once()
    +        passed_base_url = mock_honcho.call_args.kwargs.get("base_url")
    +        assert passed_base_url == "http://localhost:38000", (
    +            f"Expected 'http://localhost:38000', got {passed_base_url!r}"
    +        )
    +
    +    @pytest.mark.skipif(
    +        not importlib.util.find_spec("honcho"),
    +        reason="honcho SDK not installed"
    +    )
    +    def test_local_base_url_without_version_unchanged(self):
    +        """base_url 'http://localhost:38000' (no version) must be passed unchanged."""
    +        fake_honcho = MagicMock(name="Honcho")
    +        cfg = HonchoClientConfig(
    +            api_key=None,
    +            base_url="http://localhost:38000",
    +            workspace_id="hermes",
    +            environment="production",
    +        )
    +
    +        with patch("honcho.Honcho", return_value=fake_honcho) as mock_honcho, \
    +             patch("hermes_cli.config.load_config", return_value={}):
    +            get_honcho_client(cfg)
    +
    +        mock_honcho.assert_called_once()
    +        passed_base_url = mock_honcho.call_args.kwargs.get("base_url")
    +        assert passed_base_url == "http://localhost:38000", (
    +            f"Expected 'http://localhost:38000', got {passed_base_url!r}"
    +        )
    +
    +    @pytest.mark.skipif(
    +        not importlib.util.find_spec("honcho"),
    +        reason="honcho SDK not installed"
    +    )
    +    def test_cloud_base_url_without_version_unchanged(self):
    +        """A cloud base_url with no version segment must pass through untouched."""
    +        fake_honcho = MagicMock(name="Honcho")
    +        cfg = HonchoClientConfig(
    +            api_key="[redacted:secret-assignment]",
    +            base_url="https://api.honcho.dev",
    +            workspace_id="hermes",
    +            environment="production",
    +        )
    +
    +        with patch("honcho.Honcho", return_value=fake_honcho) as mock_honcho, \
    +             patch("hermes_cli.config.load_config", return_value={}):
    +            get_honcho_client(cfg)
    +
    +        mock_honcho.assert_called_once()
    +        passed_base_url = mock_honcho.call_args.kwargs.get("base_url")
    +        assert passed_base_url == "https://api.honcho.dev", (
    +            f"Expected 'https://api.honcho.dev', got {passed_base_url!r}"
    +        )
    +
    +    @pytest.mark.skipif(
    +        not importlib.util.find_spec("honcho"),
    +        reason="honcho SDK not installed"
    +    )
    +    def test_cloud_base_url_with_version_stripped(self):
    +        """A version segment double-prefixes regardless of host, so a cloud
    +        base_url that ends in '/v3' must also be stripped (the SDK re-adds it)."""
    +        fake_honcho = MagicMock(name="Honcho")
    +        cfg = HonchoClientConfig(
    +            api_key="[redacted:secret-assignment]",
    +            base_url="https://api.honcho.dev/v3",
    +            workspace_id="hermes",
    +            environment="production",
    +        )
    +
    +        with patch("honcho.Honcho", return_value=fake_honcho) as mock_honcho, \
    +             patch("hermes_cli.config.load_config", return_value={}):
    +            get_honcho_client(cfg)
    +
    +        mock_honcho.assert_called_once()
    +        passed_base_url = mock_honcho.call_args.kwargs.get("base_url")
    +        assert passed_base_url == "https://api.honcho.dev", (
    +            f"Expected 'https://api.honcho.dev', got {passed_base_url!r}"
    +        )
    +
    +    @pytest.mark.skipif(
    +        not importlib.util.find_spec("honcho"),
    +        reason="honcho SDK not installed"
    +    )
    +    @pytest.mark.parametrize(
    +        "raw_url, expected",
    +        [
    +            # LAN IP self-host
    +            ("http://10.0.0.5:8000/v3", "http://10.0.0.5:8000"),
    +            ("http://192.168.1.20:38000/v3/", "http://192.168.1.20:38000"),
    +            # Tailscale / custom-domain self-host
    +            ("https://honcho.my.ts.net/v3", "https://honcho.my.ts.net"),
    +            ("https://honcho.lab.internal/v3", "https://honcho.lab.internal"),
    +            ("https://honcho.fly.dev/v3", "https://honcho.fly.dev"),
    +            # higher version segments are also stripped
    +            ("https://honcho.lab.internal/v12", "https://honcho.lab.internal"),
    +            # self-host without a version segment is left unchanged
    +            ("https://honcho.my.ts.net", "https://honcho.my.ts.net"),
    +            ("http://10.0.0.5:8000", "http://10.0.0.5:8000"),
    +        ],
    +    )
    +    def test_self_hosted_base_url_version_stripped(self, raw_url, expected):
    +        """Non-loopback self-hosted instances (LAN IPs, Tailscale, custom
    +        domains) must get the same version-segment stripping as localhost.
    +        Regression for #20688 recurring on any non-loopback self-host."""
    +        fake_honcho = MagicMock(name="Honcho")
    +        cfg = HonchoClientConfig(
    +            api_key="[redacted:secret-assignment]",
    +            base_url=raw_url,
    +            workspace_id="hermes",
    +            environment="production",
    +        )
    +
    +        with patch("honcho.Honcho", return_value=fake_honcho) as mock_honcho, \
    +             patch("hermes_cli.config.load_config", return_value={}):
    +            get_honcho_client(cfg)
    +
    +        mock_honcho.assert_called_once()
    +        passed_base_url = mock_honcho.call_args.kwargs.get("base_url")
    +        assert passed_base_url == expected, (
    +            f"Expected {expected!r}, got {passed_base_url!r}"
    +        )
    +
    +    @pytest.mark.skipif(
    +        not importlib.util.find_spec("honcho"),
    +        reason="honcho SDK not installed"
    +    )
    +    def test_local_base_url_with_trailing_slash_stripped(self):
    +        """base_url 'http://127.0.0.1:38000/v3/' must also be cleaned up."""
    +        fake_honcho = MagicMock(name="Honcho")
    +        cfg = HonchoClientConfig(
    +            api_key=None,
    +            base_url="http://127.0.0.1:38000/v3/",
    +            workspace_id="hermes",
    +            environment="production",
    +        )
    +
    +        with patch("honcho.Honcho", return_value=fake_honcho) as mock_honcho, \
    +             patch("hermes_cli.config.load_config", return_value={}):
    +            get_honcho_client(cfg)
    +
    +        mock_honcho.assert_called_once()
    +        passed_base_url = mock_honcho.call_args.kwargs.get("base_url")
    +        assert passed_base_url == "http://127.0.0.1:38000", (
    +            f"Expected 'http://127.0.0.1:38000', got {passed_base_url!r}"
    +        )
    ```
  - `tests/honcho_plugin/test_pin_peer_name.py`

    ```diff
    @@ -745,10 +745,10 @@ def test_cache_busting_signature_reflects_pin_peer_name(self, tmp_path, monkeypa
             monkeypatch.setenv("HERMES_HOME", str(tmp_path))
     
             cfg_path.write_text(json.dumps({"apiKey": "k", "peerName": "Igor", "pinPeerName": True}))
    -        sig_pinned = GatewayRunner._extract_cache_busting_config({})
    +        sig_pinned = GatewayRunner._extract_cache_busting_config({"memory": {"provider": "honcho"}})
     
             cfg_path.write_text(json.dumps({"apiKey": "k", "peerName": "Igor", "pinPeerName": False}))
    -        sig_unpinned = GatewayRunner._extract_cache_busting_config({})
    +        sig_unpinned = GatewayRunner._extract_cache_busting_config({"memory": {"provider": "honcho"}})
     
             assert sig_pinned["honcho.pin_peer_name"] != sig_unpinned["honcho.pin_peer_name"]
     
    ```

    ```diff
    @@ -759,14 +759,14 @@ def test_cache_busting_signature_reflects_user_peer_aliases(self, tmp_path, monk
             monkeypatch.setenv("HERMES_HOME", str(tmp_path))
     
             cfg_path.write_text(json.dumps({"apiKey": "k", "peerName": "Igor"}))
    -        sig_no_aliases = GatewayRunner._extract_cache_busting_config({})
    +        sig_no_aliases = GatewayRunner._extract_cache_busting_config({"memory": {"provider": "honcho"}})
     
             cfg_path.write_text(json.dumps({
                 "apiKey": "k",
                 "peerName": "Igor",
                 "userPeerAliases": {"86701400": "Igor"},
             }))
    -        sig_with_aliases = GatewayRunner._extract_cache_busting_config({})
    +        sig_with_aliases = GatewayRunner._extract_cache_busting_config({"memory": {"provider": "honcho"}})
     
             assert sig_no_aliases["honcho.user_peer_aliases"] != sig_with_aliases["honcho.user_peer_aliases"]
     
    ```

    ```diff
    @@ -777,14 +777,14 @@ def test_cache_busting_signature_reflects_runtime_peer_prefix(self, tmp_path, mo
             monkeypatch.setenv("HERMES_HOME", str(tmp_path))
     
             cfg_path.write_text(json.dumps({"apiKey": "k", "peerName": "Igor"}))
    -        sig_no_prefix = GatewayRunner._extract_cache_busting_config({})
    +        sig_no_prefix = GatewayRunner._extract_cache_busting_config({"memory": {"provider": "honcho"}})
     
             cfg_path.write_text(json.dumps({
                 "apiKey": "k",
                 "peerName": "Igor",
                 "runtimePeerPrefix": "telegram_",
             }))
    -        sig_with_prefix = GatewayRunner._extract_cache_busting_config({})
    +        sig_with_prefix = GatewayRunner._extract_cache_busting_config({"memory": {"provider": "honcho"}})
     
             assert sig_no_prefix["honcho.runtime_peer_prefix"] != sig_with_prefix["honcho.runtime_peer_prefix"]
     
    ```

    ```diff
    @@ -805,14 +805,14 @@ def test_cache_busting_signature_reflects_ai_peer(self, tmp_path, monkeypatch):
                 "peerName": "Igor",
                 "aiPeer": "hermes",
             }))
    -        sig_before = GatewayRunner._extract_cache_busting_config({})
    +        sig_before = GatewayRunner._extract_cache_busting_config({"memory": {"provider": "honcho"}})
     
             cfg_path.write_text(json.dumps({
                 "apiKey": "k",
                 "peerName": "Igor",
                 "aiPeer": "hermetika",
             }))
    -        sig_after = GatewayRunner._extract_cache_busting_config({})
    +        sig_after = GatewayRunner._extract_cache_busting_config({"memory": {"provider": "honcho"}})
     
             assert sig_before["honcho.ai_peer"] != sig_after["honcho.ai_peer"]
     
    ```
  - `tests/plugins/memory/test_hindsight_provider.py`

    ```diff
    @@ -6,7 +6,9 @@
     """
     
     import json
    +import os
     import re
    +import stat
     import sys
     from types import SimpleNamespace
     from unittest.mock import AsyncMock, MagicMock
    ```

    ```diff
    @@ -1570,3 +1572,13 @@ def test_local_embedded_shutdown_closes_inner_async_client_on_shared_loop(self,
             assert embedded._client is None
             assert provider._client is None
     
    +
    [redacted:email](os.name == "nt", reason="POSIX mode bits not enforced on Windows")
    +def test_save_config_sets_owner_only_permissions(tmp_path):
    +    """hindsight/config.json must be written with 0o600 so API key is not world-readable."""
    +    provider = HindsightMemoryProvider()
    +    provider.save_config({"api_key": "hd-test-key"}, str(tmp_path))
    +    config_file = tmp_path / "hindsight" / "config.json"
    +    assert config_file.exists()
    +    mode = stat.S_IMODE(config_file.stat().st_mode)
    +    assert mode == 0o600, f"Expected 0o600 (owner-only), got {oct(mode)}"
    ```
  - `tests/plugins/memory/test_mem0_v2.py`

    ```diff
    @@ -4,6 +4,10 @@
     """
     
     import json
    +import os
    +import stat
    +
    +import pytest
     
     from plugins.memory.mem0 import Mem0MemoryProvider
     
    ```

    ```diff
    @@ -202,6 +206,17 @@ def test_prefetch_dict_response(self, monkeypatch):
     # ---------------------------------------------------------------------------
     
     
    [redacted:email](os.name == "nt", reason="POSIX mode bits not enforced on Windows")
    +def test_save_config_sets_owner_only_permissions(tmp_path):
    +    """mem0.json must be written with 0o600 so API key is not world-readable."""
    +    provider = Mem0MemoryProvider()
    +    provider.save_config({"api_key": "m0-test-key"}, str(tmp_path))
    +    config_file = tmp_path / "mem0.json"
    +    assert config_file.exists()
    +    mode = stat.S_IMODE(config_file.stat().st_mode)
    +    assert mode == 0o600, f"Expected 0o600 (owner-only), got {oct(mode)}"
    +
    +
     class TestMem0Defaults:
         """Ensure we don't break existing users' defaults."""
     
    ```
  - `tests/plugins/memory/test_supermemory_provider.py`

    ```diff
    @@ -1,4 +1,6 @@
     import json
    +import os
    +import stat
     import threading
     
     import pytest
    ```

    ```diff
    @@ -409,3 +411,13 @@ def test_get_config_schema_minimal():
         assert len(schema) == 1
         assert schema[0]["key"] == "api_key"
         assert schema[0]["secret"] is True
    +
    +
    [redacted:email](os.name == "nt", reason="POSIX mode bits not enforced on Windows")
    +def test_save_config_sets_owner_only_permissions(tmp_path):
    +    """supermemory.json must be written with 0o600 so API key is not world-readable."""
    +    _save_supermemory_config({"api_key": "sm-test-key"}, str(tmp_path))
    +    config_file = tmp_path / "supermemory.json"
    +    assert config_file.exists()
    +    mode = stat.S_IMODE(config_file.stat().st_mode)
    +    assert mode == 0o600, f"Expected 0o600 (owner-only), got {oct(mode)}"
    ```
  - `tests/test_honcho_client_config.py`

    ```diff
    @@ -2,9 +2,13 @@
     
     import json
     import os
    +import stat
    +from pathlib import Path
     
    +import pytest
     
     from plugins.memory.honcho.client import HonchoClientConfig
    +from plugins.memory.honcho import HonchoMemoryProvider
     
     
     class TestHonchoClientConfigAutoEnable:
    ```

    ```diff
    @@ -100,3 +104,24 @@ def test_falls_back_to_env_when_no_config_file(self, tmp_path, monkeypatch):
     
             assert cfg.api_key == "fallback-key"
             assert cfg.enabled is True  # from_env() sets enabled=True
    +
    +
    [redacted:email](os.name == "nt", reason="POSIX mode bits not enforced on Windows")
    +def test_save_config_sets_owner_only_permissions(tmp_path, monkeypatch):
    +    """honcho.json is created atomically with 0o600, not chmod-after-write."""
    +    import utils
    +    calls = []
    +    real_atomic = utils.atomic_json_write
    +
    +    def spy(path, data, **kwargs):
    +        calls.append(kwargs.get("mode"))
    +        return real_atomic(path, data, **kwargs)
    +
    +    monkeypatch.setattr(utils, "atomic_json_write", spy)
    +    provider = HonchoMemoryProvider()
    +    provider.save_config({"api_key": "hc-test-key"}, str(tmp_path))
    +    assert calls == [0o600]
    +    config_file = tmp_path / "honcho.json"
    +    assert config_file.exists()
    +    mode = stat.S_IMODE(config_file.stat().st_mode)
    +    assert mode == 0o600, f"Expected 0o600 (owner-only), got {oct(mode)}"
    ```
  - `utils.py`

    ```diff
    @@ -87,6 +87,7 @@ def atomic_json_write(
         data: Any,
         *,
         indent: int = 2,
    +    mode: int | None = None,
         **dump_kwargs: Any,
     ) -> None:
         """Write JSON data to a file atomically.
    ```

    ```diff
    @@ -99,13 +100,16 @@ def atomic_json_write(
             path: Target file path (will be created or overwritten).
             data: JSON-serializable data to write.
             indent: JSON indentation (default 2).
    +        mode: Optional final permission mode. When set, the temp file is
    +            created and replaced with this mode, avoiding chmod-after-write
    +            TOCTOU exposure for secret-bearing files.
             **dump_kwargs: Additional keyword args forwarded to json.dump(), such
                 as default=str for non-native types.
         """
         path = Path(path)
         path.parent.mkdir(parents=True, exist_ok=True)
     
    -    original_mode = _preserve_file_mode(path)
    +    original_mode = None if mode is not None else _preserve_file_mode(path)
     
         fd, tmp_path = tempfile.mkstemp(
             dir=str(path.parent),
    ```

    ```diff
    @@ -113,6 +117,8 @@ def atomic_json_write(
             suffix=".tmp",
         )
         try:
    +        if mode is not None:
    +            os.fchmod(fd, mode)
             with os.fdopen(fd, "w", encoding="utf-8") as f:
                 json.dump(
                     data,
    ```

    ```diff
    @@ -125,7 +131,13 @@ def atomic_json_write(
                 os.fsync(f.fileno())
             # Preserve symlinks — swap in-place on the real file (GitHub #16743).
             real_path = atomic_replace(tmp_path, path)
    -        _restore_file_mode(real_path, original_mode)
    +        if mode is not None:
    +            try:
    +                os.chmod(real_path, mode)
    +            except OSError:
    +                pass
    +        else:
    +            _restore_file_mode(Path(real_path), original_mode)
         except BaseException:
             # Intentionally catch BaseException so temp-file cleanup still runs for
             # KeyboardInterrupt/SystemExit before re-raising the original signal.
    ```
  - `website/docs/user-guide/features/honcho.md`

    ```diff
    @@ -106,6 +106,10 @@ The auto-injected dialectic scales `dialecticReasoningLevel` by query length: +1
     
     Honcho is configured in `~/.honcho/config.json` (global) or `$HERMES_HOME/honcho.json` (profile-local). The setup wizard handles this for you.
     
    +### Self-Hosted Honcho with Authentication
    +
    +When pointing Hermes at a self-hosted Honcho server, `hermes honcho setup` (and `hermes memory setup`) ask for a **local JWT / bearer token** after the base URL. Paste a JWT signed with the server's `AUTH_JWT_SECRET` (the Honcho compose env var) to enable authenticated access; leave it blank for servers running with `AUTH_USE_AUTH=false`. The local token is stored under the host block (`hosts.<host>.apiKey` in `honcho.json`), separate from any cloud `apiKey`, so you can flip the `Cloud or local?` prompt back to `cloud` later without losing either credential.
    +
     ### Full Config Reference
     
     | Key | Default | Description |
    ```

    ```diff
    @@ -199,11 +203,12 @@ When Honcho is active as the memory provider, five tools become available:
     
     ## CLI Commands
     
    -The `hermes honcho` subcommand is **only registered when Honcho is the active memory provider** (`memory.provider: honcho` in `config.yaml`). Run `hermes memory setup` and pick Honcho first; the subcommand appears on the next invocation.
    +The `hermes honcho` subcommand is **only registered when Honcho is the active memory provider** (`memory.provider: honcho` in `config.yaml`). On a fresh install, configure Honcho directly with `hermes memory setup honcho` (or run `hermes memory setup` and pick it from the list); the `hermes honcho` subcommand then appears on the next invocation.
     
     ```bash
    +hermes memory setup honcho    # Configure Honcho directly (works before activation)
     hermes honcho status          # Connection status, config, and key settings
    -hermes honcho setup           # Redirects to `hermes memory setup`
    +hermes honcho setup           # Redirects to `hermes memory setup` (post-activation alias)
     hermes honcho strategy        # Show or set session strategy (per-session/per-directory/per-repo/global)
     hermes honcho peer            # Show or update peer names + dialectic reasoning level
     hermes honcho mode            # Show or set recall mode (hybrid/context/tools)
    ```
  - `website/docs/user-guide/features/memory-providers.md`

    ```diff
    @@ -66,7 +66,7 @@ AI-native cross-session user modeling with dialectic reasoning, session-scoped c
     hermes memory setup        # select "honcho" — runs the Honcho-specific post-setup
     ```
     
    -The legacy `hermes honcho setup` command still works (it now redirects to `hermes memory setup`), but is only registered after Honcho is selected as the active memory provider.
    +On a fresh install, configure Honcho directly with `hermes memory setup honcho`. The legacy `hermes honcho setup` command still works (it now redirects to `hermes memory setup`), but is only registered after Honcho is selected as the active memory provider.
     
     **Config:** `$HERMES_HOME/honcho.json` (profile-local) or `~/.honcho/config.json` (global). Resolution order: `$HERMES_HOME/honcho.json` > `~/.hermes/honcho.json` > `~/.honcho/config.json`. See the [config reference](https://github.com/NousResearch/hermes-agent/blob/main/plugins/memory/honcho/README.md) and the [Honcho integration guide](https://docs.honcho.dev/v3/guides/integrations/hermes).
     
    ```
- **#107547: feat(honcho): each turn is written under its author's peer; bot DMs get their own session (salvage #103890)**. merged to main · fix tier `verified`
  - maintainer direction: category `merged`; implication `superseded`
  - files: `agent/turn_author.py`, `gateway/run.py`, `gateway/run_agent_cache.py`, `plugins/memory/honcho/README.md`, `plugins/memory/honcho/__init__.py`, `plugins/memory/honcho/client.py`, `plugins/memory/honcho/config_schema.py`, `plugins/memory/honcho/session.py`, `plugins/memory/honcho/session_peers.py`, `tests/honcho_plugin/test_a2a_sessions.py`, `tests/honcho_plugin/test_identity_signature.py`, `tests/honcho_plugin/test_pin_peer_name.py`, `tests/honcho_plugin/test_turn_author_peers.py`, `tests/plugins/memory/test_honcho_config_schema.py`, `tests/test_honcho_startup_fail_open.py`, `website/docs/user-guide/features/honcho.md`
- **#33954: fix(honcho): harden self-hosted setup paths**. closed without merge
  - maintainer direction: category `merged_via`; implication `superseded`; refs `827ce602d`, `#35170`, `#34851`
- **#50260: refactor(memory): move Honcho cache signature into provider**. closed without merge
  - maintainer direction: category `implemented_elsewhere`; implication `superseded`; refs `#33381`

## Fix status

- Merged to main in PR #30077 (cited above). No release data is tracked. Verify downstream.
- Fix last verified (merged) `2026-09-10`.
- Subsystem: `agent`
