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:
@@ -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
82
docs/how-it-works.md
Normal 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.
|
||||
Reference in New Issue
Block a user