Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 50 additions & 20 deletions gateway/run.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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

Expand Down
8 changes: 7 additions & 1 deletion hermes_cli/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
6 changes: 5 additions & 1 deletion hermes_cli/memory_setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
21 changes: 14 additions & 7 deletions hermes_cli/profiles.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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")
Expand All @@ -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:
Expand Down
4 changes: 2 additions & 2 deletions optional-skills/autonomous-ai-agents/honcho/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
```

Expand Down
3 changes: 2 additions & 1 deletion plugins/memory/hindsight/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."""
Expand Down
21 changes: 13 additions & 8 deletions plugins/memory/honcho/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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
Expand Down Expand Up @@ -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**.

Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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"
Expand All @@ -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

Expand Down Expand Up @@ -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 |
Expand Down Expand Up @@ -344,7 +349,7 @@ Presets:
"dialecticMaxChars": 600,
"saveMessages": true
},
"hermes.coder": {
"hermes_coder": {
"enabled": true,
"aiPeer": "coder",
"sessionStrategy": "per-repo",
Expand Down
4 changes: 3 additions & 1 deletion plugins/memory/honcho/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {}
Expand All @@ -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 [
Expand Down
Loading
Loading