feat: runtime monkey-patch instead of file patching — zero fork required

The plugin now monkey-patches HonchoMemoryProvider._do_session_init at
plugin load time instead of requiring changes to any file in the Hermes
repo. This means:

- No modifications to plugins/memory/honcho/__init__.py or any other file
- No fork needed — pure plugin approach
- Survives Hermes upgrades without merge conflicts
- Removed docs/honcho-injector.md (replaced by docs/how-it-works.md)
- Updated README with new architecture description

Config file at /opt/data/identity-config.json remains the single source
of truth for mappings.
This commit is contained in:
2026-05-24 16:07:02 -04:00
parent 172fa90b25
commit 514706bcce
4 changed files with 238 additions and 190 deletions

View File

@@ -3,51 +3,60 @@
Maps platform-specific user IDs (Discord snowflake, Telegram UID, terminal Maps platform-specific user IDs (Discord snowflake, Telegram UID, terminal
session) and kanban boards to stable Honcho peer names. Eliminates session) and kanban boards to stable Honcho peer names. Eliminates
`user-default-t_*` peers from kanban workers and unifies a user's identity `user-default-t_*` peers from kanban workers and unifies a user's identity
across platforms (Discord + Telegram → same Honcho peer). across platforms.
**Zero modifications to the Hermes repo.** No fork required. The plugin
uses runtime monkey-patching at plugin load time to wrap Honcho's session
initialization — no files are changed on disk.
## Architecture ## Architecture
``` ```
┌──────────────────────┐ ──────────────────────────┐
│ identity-config.json │ │ identity-config.json │
│ /opt/data/ │ │ /opt/data/ │
└──────┬───────────────┘ └──────┬───────────────────
│ read on init │ reads on every resolve
┌──────────────────┼──────────────────┐ ┌──────────────────┼───────────────────────
│ │ │ │ │ │
▼ ▼ ▼ ▼ ▼ ▼
┌──────────────┐ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ ┌──────────────────────
│ pre_gateway │ │ on_session │ │ Honcho │ pre_gateway │ │ /identity │ │ Honcho monkey-patch
│ dispatch │ │ start hook │ │ injector │ dispatch │ │ slash cmd │ │ (runtime, no files)
│ hook (log) │ │ (log/track) (session.py)│ │ hook (log) │ │ (manage) └──────────┬───────────┘
└──────────────┘ └──────────────┘ └──────┬───────┘ └──────────────┘ └──────────────┘
injects peer_name
resolves peer_name into kwargs["user_id"]
before Honcho init BEFORE Honcho session init
``` ```
Two components work together: ## How it works
1. **This plugin** (user-installed) — provides `/identity` slash command for 1. **Plugin loads**`register()` is called → monkey-patches
config management, `pre_gateway_dispatch` logging, and `on_session_start` `HonchoMemoryProvider._do_session_init` with a wrapper.
tracking. Installed via `hermes plugins install`.
2. **Honcho injector** (~6 lines in `plugins/memory/honcho/session.py`) — reads 2. **Kanban worker starts** → Honcho tries to init → no `user_id` from
the config file and `HERMES_KANBAN_TASK` env var to inject the correct gateway → the wrapper calls `get_peer_name()` → reads the task body
`runtime_user_peer_name` into Honcho's session initialization. See from `HERMES_KANBAN_DB` → extracts `context_peer: <name>` → injects
[`docs/honcho-injector.md`](docs/honcho-injector.md). it as `user_id` → Honcho creates the session with the correct peer.
3. **Gateway session starts** (Discord/Telegram) → normal flow with
platform user ID → monkey-patch is bypassed (user_id already set).
4. **Missing config or env** → returns None → Honcho behaves exactly as
before (creates `user-default-*` peers — safe fallback).
## Installation ## Installation
```bash ```bash
# 1. Install the plugin # 1. Install the plugin from Gitea
hermes plugins install gitea:code.lazyworkhorse.net/Hermes/hermes-identity-plugin hermes plugins install ssh://git@code.lazyworkhorse.net:2222/Hermes/hermes-identity-plugin.git
# 2. Apply the Honcho injector patch (see docs/honcho-injector.md) # 2. Create the config file (persistent volume)
# 3. Create the config file
cp config.sample.json /opt/data/identity-config.json cp config.sample.json /opt/data/identity-config.json
# 4. Edit /opt/data/identity-config.json with your mappings
# 5. Restart gateways # 3. Edit /opt/data/identity-config.json with your mappings
# 4. Restart gateways to pick up the plugin
``` ```
## Config file: `/opt/data/identity-config.json` ## Config file: `/opt/data/identity-config.json`
@@ -56,7 +65,9 @@ cp config.sample.json /opt/data/identity-config.json
{ {
"mappings": [ "mappings": [
{"platform": "discord", "id": "479136126737711105", "peer": "thierry"}, {"platform": "discord", "id": "479136126737711105", "peer": "thierry"},
{"platform": "telegram", "id": "123456789", "peer": "thierry"} {"platform": "telegram", "id": "123456789", "peer": "thierry"},
{"platform": "matrix", "id": "@thierry:example.org", "peer": "thierry"},
{"platform": "terminal", "id": "default", "peer": "thierry"}
], ],
"boards": { "boards": {
"default": "thierry", "default": "thierry",
@@ -69,7 +80,7 @@ cp config.sample.json /opt/data/identity-config.json
## Task body convention ## Task body convention
Every kanban task MUST include a `context_peer` in its body: Every kanban task SHOULD include a `context_peer` in its body:
```markdown ```markdown
Do research on Postgres migration costs. Do research on Postgres migration costs.
@@ -79,23 +90,25 @@ context_peer: thierry
``` ```
``` ```
Profiles (Claire, Ashley, Finn, Matt) MUST include this when calling Profiles (Claire, Ashley, Finn, Matt) set this when calling `kanban_create`.
`kanban_create`. The `enforce_context_peer` config flag controls whether
missing `context_peer` causes an error. **If `context_peer` is missing**, the plugin falls back to the board
config (e.g., `boards.default` → `thierry`). If no board config either,
it uses `fallback_peer`. If that's also unset → `user-default-*` (safe).
## Commands ## Commands
- `/identity status` — show current config and resolved peer - `/identity status` — show current config and resolved peer
- `/identity list` — list all mappings - `/identity list` — list all mappings
- `/identity add discord <id> <peer>` — add a new mapping - `/identity add discord <id> <peer>` — add a new mapping
- `/identity rm <index>` — remove mapping - `/identity rm <index>` — remove mapping by index
- `/identity board <slug> <peer>` — set board peer - `/identity board <slug> <peer>` — set board's default peer
- `/identity help` — full help - `/identity help` — full help
## Enforcing the convention ## Safety
Add this to the kanban-worker skill or the profile's system prompt: - The monkey-patch is applied ONLY at plugin load time, before any session.
- If the plugin is uninstalled or fails to load, Honcho is untouched.
> When creating a kanban task with `kanban_create`, you MUST include - If the config file is missing, all resolution returns None → Honcho
> `context_peer: <peer_name>` in the task body as a metadata block. creates `user-default-*` as today.
> Use the user's Honcho peer name — not your own profile name. - No data loss risk. No changes to any file in the Hermes repo.

View File

@@ -2,11 +2,10 @@
hermes-identity-plugin (identity) hermes-identity-plugin (identity)
Resolves platform-specific user IDs and kanban workers to stable Honcho Resolves platform-specific user IDs and kanban workers to stable Honcho
peer names. The plugin provides hooks + slash command. The actual peer name peer names. NO modifications to the Hermes repo required — the plugin
injection into Honcho requires a ~6-line change in the bundled Honcho plugin monkey-patches Honcho's session initialization at plugin load time.
(plugins/memory/honcho/session.py) — see docs/honcho-injector.md.
Config: /opt/data/identity-config.json (persistent across container rebuilds) Config: /opt/data/identity-config.json (persistent volume)
""" """
from __future__ import annotations from __future__ import annotations
@@ -16,8 +15,9 @@ import logging
import os import os
import re import re
import sqlite3 import sqlite3
from functools import wraps
from pathlib import Path from pathlib import Path
from typing import Any from typing import Any, Callable
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -51,11 +51,11 @@ def save_config(cfg: dict[str, Any]) -> None:
logger.info("identity: config saved to %s", CONFIG_PATH) logger.info("identity: config saved to %s", CONFIG_PATH)
# ── Resolution ────────────────────────────────────────────────────────────── # ── Peer resolution ──────────────────────────────────────────────────────────
def resolve_peer(platform: str, platform_id: str) -> str | None: def resolve_peer(platform: str, platform_id: str) -> str | None:
"""Resolve a platform+ID to a canonical peer name.""" """Resolve a platform+ID to a canonical peer name from config."""
cfg = load_config() cfg = load_config()
for m in cfg.get("mappings", []): for m in cfg.get("mappings", []):
if m.get("platform") == platform and str(m.get("id", "")) == str(platform_id): if m.get("platform") == platform and str(m.get("id", "")) == str(platform_id):
@@ -75,8 +75,9 @@ def get_peer_name() -> str | None:
"""Resolve peer name for the current process context. """Resolve peer name for the current process context.
Priority: Priority:
1. HERMES_HONCHO_PEER_NAME env var (set by kanban dispatcher or manually) 1. HERMES_HONCHO_PEER_NAME env var (explicit override or manual)
2. Kanban worker: task body context_peer → board config → fallback 2. Kanban worker: task body context_peer → board config → fallback
3. None (Honcho behaves as before)
""" """
explicit = os.environ.get("HERMES_HONCHO_PEER_NAME") explicit = os.environ.get("HERMES_HONCHO_PEER_NAME")
if explicit: if explicit:
@@ -88,7 +89,8 @@ def get_peer_name() -> str | None:
if task_id and db_path: if task_id and db_path:
return _resolve_kanban_peer(task_id, board, db_path) return _resolve_kanban_peer(task_id, board, db_path)
return None cfg = load_config()
return cfg.get("fallback_peer")
def _resolve_kanban_peer(task_id: str, board: str | None, db_path: str) -> str | None: def _resolve_kanban_peer(task_id: str, board: str | None, db_path: str) -> str | None:
@@ -104,7 +106,7 @@ def _resolve_kanban_peer(task_id: str, board: str | None, db_path: str) -> str |
"SELECT body FROM tasks WHERE id = ?", (task_id,) "SELECT body FROM tasks WHERE id = ?", (task_id,)
).fetchone() ).fetchone()
if not row: if not row:
return None return resolve_board_peer(board)
body = row["body"] or "" body = row["body"] or ""
peer = _extract_context_peer(body) peer = _extract_context_peer(body)
if peer: if peer:
@@ -131,14 +133,8 @@ def _extract_context_peer(body: str) -> str | None:
return None return None
# ── Enforce context_peer on kanban task creation ────────────────────────────
# Profiles (Claire, Ashley, etc.) MUST include ``context_peer: <name>`` in
# every kanban task body. The kanban-worker skill references this convention.
# The Honcho injector (see docs/honcho-injector.md) reads it at worker start.
def enforce_context_peer(body: str | None) -> str | None: def enforce_context_peer(body: str | None) -> str | None:
"""Return error message if body lacks context_peer, None if OK.""" """Return error string if body lacks context_peer, None if OK."""
cfg = load_config() cfg = load_config()
if not cfg.get("enforce_context_peer", True): if not cfg.get("enforce_context_peer", True):
return None return None
@@ -147,14 +143,81 @@ def enforce_context_peer(body: str | None) -> str | None:
"Missing required `context_peer: <name>` in task body. " "Missing required `context_peer: <name>` in task body. "
"Add a metadata block:\n" "Add a metadata block:\n"
"```metadata\ncontext_peer: thierry\n```\n" "```metadata\ncontext_peer: thierry\n```\n"
"Replace `thierry` with the intended user's peer name."
) )
peer = _extract_context_peer(body) peer = _extract_context_peer(body)
if not peer: if not peer:
return "context_peer: found in body but could not be parsed. Use format: context_peer: <name>" return "context_peer: found in body but could not be parsed."
return None return None
# ── Runtime monkey-patch of Honcho session init ─────────────────────────────
# This is the KEY mechanism: no files modified, no fork needed.
# The plugin wraps HonchoMemoryProvider._do_session_init at plugin load time
# to inject our resolved peer name before Honcho creates the session.
_original_init: Callable | None = None
def _patched_do_session_init(self, cfg, session_id: str, **kwargs):
"""Wrapper around Honcho's _do_session_init.
If no user_id was provided by the gateway (kanban worker / CLI context),
resolve one from the identity config and inject it.
"""
if not kwargs.get("user_id"):
resolved = get_peer_name()
if resolved:
logger.info(
"identity: injecting peer '%s' into Honcho session (task=%s, board=%s)",
resolved,
os.environ.get("HERMES_KANBAN_TASK", "-"),
os.environ.get("HERMES_KANBAN_BOARD", "-"),
)
kwargs["user_id"] = resolved
return _original_init(self, cfg, session_id, **kwargs) # type: ignore[misc]
def _apply_honcho_patch():
"""Apply monkey-patch to Honcho's session initialization.
Must be called after Honcho is loaded but before any session starts.
Safe to call multiple times - only patches on first call.
"""
global _original_init
if _original_init is not None:
return # already patched
try:
import plugins.memory.honcho # type: ignore[import-untyped]
# Use string-based patching to avoid import-time issues
honcho_mod = __import__(
"plugins.memory.honcho",
fromlist=["HonchoMemoryProvider"],
)
provider_cls = getattr(honcho_mod, "HonchoMemoryProvider", None)
if provider_cls is None:
logger.warning("identity: HonchoMemoryProvider not found, cannot patch")
return
orig = getattr(provider_cls, "_do_session_init", None)
if orig is None:
logger.warning("identity: HonchoMemoryProvider._do_session_init not found")
return
# Store original and replace with wrapper
_original_init = orig
@wraps(orig)
def wrapper(self, cfg, session_id: str, **kwargs):
return _patched_do_session_init(self, cfg, session_id, **kwargs)
setattr(provider_cls, "_do_session_init", wrapper)
logger.info("identity: patched HonchoMemoryProvider._do_session_init ✓")
except Exception as exc:
logger.warning("identity: failed to patch Honcho init: %s", exc)
# ── Hooks ─────────────────────────────────────────────────────────────────── # ── Hooks ───────────────────────────────────────────────────────────────────
@@ -168,7 +231,7 @@ def _pre_gateway_dispatch(event: Any, gateway: Any, session_store: Any, **kw) ->
logger.debug("identity: gateway platform=%s user_id=%s → peer=%s", platform, user_id, resolved) logger.debug("identity: gateway platform=%s user_id=%s → peer=%s", platform, user_id, resolved)
else: else:
logger.info("identity: unmapped gateway user platform=%s user_id=%s", platform, user_id) logger.info("identity: unmapped gateway user platform=%s user_id=%s", platform, user_id)
return None # allow normal dispatch return None
def _on_session_start(**kw: Any) -> None: def _on_session_start(**kw: Any) -> None:
@@ -179,7 +242,7 @@ def _on_session_start(**kw: Any) -> None:
if peer: if peer:
logger.info("identity: kanban worker task=%s peer=%s", task_id, peer) logger.info("identity: kanban worker task=%s peer=%s", task_id, peer)
else: else:
logger.info("identity: kanban worker task=%s no peer → user-default", task_id) logger.info("identity: kanban worker task=%s → user-default (no mapping)", task_id)
elif peer: elif peer:
logger.debug("identity: session peer=%s", peer) logger.debug("identity: session peer=%s", peer)
@@ -199,15 +262,13 @@ def _cmd_identity(raw_args: str) -> str:
elif cmd == "list": elif cmd == "list":
return _list_mappings() return _list_mappings()
elif cmd in ("add", "set") and len(args) >= 4: elif cmd in ("add", "set") and len(args) >= 4:
# /identity add discord 479136126737711105 thierry
platform, pid, peer_name = args[1], args[2], args[3] platform, pid, peer_name = args[1], args[2], args[3]
return _add_mapping(platform, pid, peer_name) return _add_mapping(platform, pid, peer_name)
elif cmd == "rm" and len(args) >= 2: elif cmd == "rm" and len(args) >= 2:
idx = int(args[1]) if args[1].isdigit() else -1 idx = int(args[1]) if args[1].isdigit() else -1
return _remove_mapping(idx) return _remove_mapping(idx)
elif cmd == "board" and len(args) >= 3: elif cmd == "board" and len(args) >= 3:
slug, peer_name = args[1], args[2] return _set_board(args[1], args[2])
return _set_board(slug, peer_name)
elif cmd == "help": elif cmd == "help":
return _help_text() return _help_text()
return _help_text() return _help_text()
@@ -221,13 +282,13 @@ def _show_status() -> str:
f"Mappings: {len(cfg.get('mappings', []))}", f"Mappings: {len(cfg.get('mappings', []))}",
f"Boards: {len(cfg.get('boards', {}))}", f"Boards: {len(cfg.get('boards', {}))}",
f"Enforce context_peer: {cfg.get('enforce_context_peer', True)}", f"Enforce context_peer: {cfg.get('enforce_context_peer', True)}",
f"Fallback peer: {cfg.get('fallback_peer', 'user')}", f"Fallback: {cfg.get('fallback_peer', 'user')}",
f"Honcho patch: {'' if _original_init else '✗ not applied'}",
"", "",
"Current context:", "Current context:",
] ]
peer = get_peer_name() peer = get_peer_name()
if peer: lines.append(f" Resolved peer: **{peer or '(none — will use user-default)'}**")
lines.append(f" Resolved peer: **{peer}**")
task = os.environ.get("HERMES_KANBAN_TASK") task = os.environ.get("HERMES_KANBAN_TASK")
if task: if task:
lines.append(f" Kanban task: `{task}`") lines.append(f" Kanban task: `{task}`")
@@ -258,7 +319,7 @@ def _add_mapping(platform: str, pid: str, peer_name: str) -> str:
cfg = load_config() cfg = load_config()
cfg.setdefault("mappings", []).append({"platform": platform, "id": pid, "peer": peer_name}) cfg.setdefault("mappings", []).append({"platform": platform, "id": pid, "peer": peer_name})
save_config(cfg) save_config(cfg)
return f"Added mapping: `{platform}` `{pid}` → **{peer_name}**" return f"Added: `{platform}` `{pid}` → **{peer_name}**"
def _remove_mapping(idx: int) -> str: def _remove_mapping(idx: int) -> str:
@@ -294,13 +355,21 @@ Commands:
def register(ctx: Any) -> None: def register(ctx: Any) -> None:
"""Register identity plugin hooks and slash command.""" """Register identity plugin hooks and apply Honcho session patch."""
# Apply the runtime monkey-patch to Honcho's session init
# NO files modified, NO fork needed — pure Python at import time
_apply_honcho_patch()
# Register hooks
ctx.register_hook("pre_gateway_dispatch", _pre_gateway_dispatch) ctx.register_hook("pre_gateway_dispatch", _pre_gateway_dispatch)
ctx.register_hook("on_session_start", _on_session_start) ctx.register_hook("on_session_start", _on_session_start)
# Register slash command
ctx.register_command( ctx.register_command(
name="identity", name="identity",
handler=_cmd_identity, handler=_cmd_identity,
description="Manage identity peer mappings", description="Manage identity peer mappings",
args_hint="status|list|add|rm|board|help", args_hint="status|list|add|rm|board|help",
) )
logger.info("identity-plugin: registered hooks (pre_gateway_dispatch, on_session_start) and /identity command")
logger.info("identity-plugin: registered ✓ (honcho patch=%s)", "applied" if _original_init else "not-applied")

View File

@@ -1,116 +0,0 @@
# Honcho Injector Patch
The identity plugin cannot directly inject `runtime_user_peer_name` into
Honcho from a user-installed hook (the hook system only allows message
rewrite/skip, not user_id modification). Instead, we add ~6 lines in the
bundled Honcho plugin's session initialization.
## File
`plugins/memory/honcho/__init__.py` — line ~362
## The change
### Before
```python
runtime_user_peer_name=kwargs.get("user_id") or None,
```
### After
```python
runtime_user_peer_name=kwargs.get("user_id") or _resolve_identity_peer(),
```
### Add this function in the same file (or import from the identity plugin)
```python
def _resolve_identity_peer() -> str | None:
"""Resolve peer name from env vars and identity config.
Priority: HERMES_HONCHO_PEER_NAME → kanban task body context_peer
→ kanban board config → None (falls through to Honcho default).
"""
import json, os, sqlite3, re
from pathlib import Path
# 1. Explicit env override
explicit = os.environ.get("HERMES_HONCHO_PEER_NAME")
if explicit:
return explicit
# 2. Kanban worker: read task body for context_peer
task_id = os.environ.get("HERMES_KANBAN_TASK")
db_path = os.environ.get("HERMES_KANBAN_DB")
if task_id and db_path:
db = Path(db_path)
if db.exists():
try:
conn = sqlite3.connect(str(db))
conn.row_factory = sqlite3.Row
try:
row = conn.execute(
"SELECT body FROM tasks WHERE id = ?", (task_id,)
).fetchone()
if row and row["body"]:
m = re.search(r"context_peer:\s*(\S+)", row["body"])
if m:
return m.group(1)
finally:
conn.close()
except Exception:
pass
# 3. Identity config file
cfg_path = Path("/opt/data/identity-config.json")
if cfg_path.exists():
try:
cfg = json.loads(cfg_path.read_text())
# Check kanban board config
board = os.environ.get("HERMES_KANBAN_BOARD")
if board and board in cfg.get("boards", {}):
return cfg["boards"][board]
# Use fallback
return cfg.get("fallback_peer")
except Exception:
pass
return None
```
## How it works
1. When a **gateway session** starts, `kwargs["user_id"]` carries the platform
ID (Discord snowflake, Telegram UID). The identity config file maps these
to canonical peer names. The injector is bypassed — normal flow.
2. When a **kanban worker** starts, `kwargs["user_id"]` is None (no gateway).
The injector kicks in:
a. Checks `HERMES_HONCHO_PEER_NAME` env var (set by a future dispatcher
enhancement or manually).
b. Reads the kanban task body from the SQLite database, extracts
`context_peer: <name>` from the body.
c. Falls back to the board-level config from identity-config.json.
d. If nothing resolves → returns None → Honcho creates `user-default-*`
as today (safe fallback).
## Why not modify the kanban dispatcher?
The kanban dispatcher (`hermes_cli/kanban_db.py:_default_spawn`) is core
Hermes code. We avoid touching it. Instead, the Honcho injector reads the
task directly from the kanban DB using env vars that are already set
(`HERMES_KANBAN_TASK`, `HERMES_KANBAN_DB`, `HERMES_KANBAN_BOARD`).
This adds ~10 microseconds to worker startup — negligible.
## Compatibility
- If the identity plugin is removed, the injector function returns None and
Honcho behaves exactly as before.
- If the config file is missing, same safe fallback.
- No data loss risk.

82
docs/how-it-works.md Normal file
View File

@@ -0,0 +1,82 @@
# Architecture: Why monkey-patch instead of patching files?
The identity plugin uses **runtime monkey-patching** — no files in the
Hermes repo are modified. This is the key design decision that eliminates
the need for a fork.
## The problem
Kanban workers start with NO gateway user identity. The spawn function
runs `hermes -p <profile> chat -q "work kanban task <id>"`. When Honcho
initializes, `kwargs["user_id"]` is None, so it creates `user-default-*`
peers.
There is no Hermes plugin hook that fires at agent initialization before
Honcho creates the session. The hooks available are:
- `pre_gateway_dispatch` — only fires for gateway sessions (Discord, etc.)
- `on_session_start` — fires AFTER Honcho has already initialized
- `pre_tool_call` / `post_tool_call` — fire during tool execution, too late
## The solution: runtime monkey-patch
Python allows replacing any method on any class at runtime. The identity
plugin does exactly this in its `register()` function:
1. At plugin load time, it imports `HonchoMemoryProvider`
2. Stores a reference to the original `_do_session_init` method
3. Replaces it with a wrapper that checks the identity config first
4. If the wrapper finds a peer name, it injects it via `kwargs["user_id"]`
5. Otherwise, it calls the original method unchanged
```python
# In the plugin's register() function:
provider_cls = plugins.memory.honcho.HonchoMemoryProvider
_original_init = provider_cls._do_session_init
@wraps(_original_init)
def wrapper(self, cfg, session_id, **kwargs):
if not kwargs.get("user_id"):
resolved = get_peer_name() # reads config + env vars
if resolved:
kwargs["user_id"] = resolved
return _original_init(self, cfg, session_id, **kwargs)
provider_cls._do_session_init = wrapper
```
## Advantages over file patching
| Aspect | File patching | Monkey-patching |
|--------|--------------|-----------------|
| Repo modifications | Yes (edits plugin file) | Zero |
| Fork needed | Yes (git diff to maintain) | No |
| Upgrades | Merge conflict on every pull | Unchanged |
| Uninstall | Need to revert file changes | Plugin removed → clean |
| Reliability | Works across restarts | Applied at every load |
| Complexity | Simple file edit | Wrapping pattern |
## When does the patch apply?
- At plugin discovery time (Hermes startup)
- Before any Honcho session is created
- Before any agent is initialized
- Before any kanban worker is spawned
The Hermes plugin loader calls `register()` during gateway/agent startup,
which means the patch is in place before any session can begin.
## Safety guarantees
1. **Non-destructive**: The original method is preserved via closure —
removing the plugin restores original behavior with zero cleanup.
2. **Idempotent**: `_apply_honcho_patch()` guards against double-patching
via the `_original_init` sentinel.
3. **Graceful degradation**: If the config file is missing, parsing fails,
or the kanban DB is unavailable, the wrapper returns None → Honcho
falls back to its default behavior (creating `user-default-*` peers).
4. **No data loss**: The injected `kwargs["user_id"]` only affects what
peer name Honcho assigns to the session's messages. It doesn't touch
any existing data.