ADR 0001 — Unify the Hermes runtime into the mac monorepo¶
- Status: Superseded (vendoring premise ended 2026-08-17)
- Date: 2026-05-30
- Decision owner: Jordan Hubbard
- Context: validating the premise that the three-month "circling the drain"
is caused by
mac,hermes-agent, andtokenhubbeing separate systems with separate event loops and opaque boundaries — and that folding a mature snapshot ofhermes-agentintomacwould make the fleet easier to observe, coordinate, and keep interactive.
Amendment — 2026-08-17¶
ADR 0001 accepted vendoring a pruned Hermes runtime into src/mac/_hermes/
and running it in-process. That premise ended: Hermes is now installed
externally (upstream's own installer) and is the only human interface and
chat gateway; nothing in src/mac/ runs it in-process.
PR #377 removed the inactive snapshot (~444k lines), hermes_vendor.py,
hermes_gateway.py, deploy/hermes/ (including SNAPSHOT.md and the
re-vendor tooling), the hermes-revendor CI job, and the sandbox .pth
injection. Hermes can still be fetched and patched on demand; full git
history retains every byte. See docs/hermes-vendor-fate.md for the
pre-deletion checks (a)–(d) and the post-removal inventory.
Residual (out of scope for the fate decision): a legacy Hermes gateway
install path in deploy/fleet-node-install.sh still references
python -m mac.hermes_gateway (module absent). It is inert and belongs in a
separate deploy cleanup.
The body below is the original accepted decision and remains historical context for why the tree was once carried in-tree.
TL;DR verdict¶
The premise is two-thirds right, and the right two-thirds matter most.
| Boundary | Is it a root cause? | Decision |
|---|---|---|
| mac ↔ hermes-agent | Yes. | Merge. Own a pinned, pruned, in-tree snapshot of the Hermes runtime; run it in one process under mac's event loop; delete the runtime string-surgery shims; fold the out-of-tree patches in-tree. |
| mac ↔ tokenhub | No (not in the way the premise implies). | Keep separate. It is mature Go infra (Thompson-sampling router, encrypted vault, Temporal). The "dark spot" is an observability gap, not a boundary problem. Consume tokenhub's existing decision feed instead of rewriting it. |
| mac's internal pain (inert memory tier, 3.1 GB observability table, runaway loops) | No. | These are mac-internal bugs/ops gaps. Merging Hermes does not fix them; they have their own tickets and partial fixes already landed. |
"Single coherent codebase" is the correct instinct for the agent runtime. It is the wrong instinct for tokenhub, and it is irrelevant to mac's own internal bugs. Conflating all three is how the last two efforts (including ACC) became "too complicated."
What is actually true today (ground truth, not assumptions)¶
1. mac does not own Hermes — it patches a pristine, fast-moving upstream clone¶
deploy/deploy-mac-fleet.sh:3995 runs:
It then mutates that clone three ways:
- Three out-of-tree git patches in
deploy/hermes/: disable-shutdown-chat-notices.patch(15 lines →gateway/run.py)mac-runtime-context-prompt.patch(63 lines →agent/prompt_builder.py)multi-slack-mvp.patch(1,372 lines →gateway/platforms/slack.py,gateway/session.py, tests). This is a substantial feature fork of Hermes carried as a patch file.- Runtime string-surgery monkeypatching (
src/mac/hermes_startup.py):_apply_gateway_runtime_shim()readsgateway/run.pyas text and does exact-string.replace()on needles like" runtime_kwargs = _resolve_runtime_agent_kwargs()\n"and" model = _resolve_gateway_model(user_config)\n". When upstream changes those lines, the needle silently fails to match and the function records"...upstream gateway/run.py changed". - Separate process, separate venv, separate event loop at runtime:
deploy-mac-fleet.sh:4219runs the gateway asexec .../hermes-agent/.venv/bin/python .../hermes gateway run --replace, whilemacruns as its ownmac.service.
Upstream Hermes had a commit on the same day this ADR was written
(b1a25404b, PR #35532). So mac's patches and string-surgery shims sit on
top of a target that moves daily.
2. The "inexplicable provider behavior" dark spot is mechanically explained¶
The per-agent model/provider override (the mechanism that prevents an
"agent monoculture" by putting different agents on different model families)
is injected by item (2) above — string surgery against gateway/run.py.
When upstream drifts and the needle misses:
- the override silently does not apply,
- the agent runs on whatever the unpatched gateway defaults to,
- and nothing in the agent's own runtime can explain why, because the decision was supposed to be made by code that was never actually inserted.
mac can only observe Hermes as filesystem metadata — hermes_startup.py
inventories SOUL.md, state.db, gateway/run.py, etc. by path / size /
mtime (_file_ref), never by live state, because it is a different process
it shares no memory with. "Observe, coordinate, keep interactive" is
structurally capped at what you can see across a process boundary plus a
text-diff of someone else's source tree.
3. The runtime surface is large, and Hermes is opinionated¶
| Hermes runtime dir | py files | py LOC |
|---|---|---|
hermes_cli/ |
119 | ~111k |
gateway/ |
60 | ~82k |
tools/ |
99 | ~72k |
agent/ |
108 | ~68k |
skills/ |
43 | ~13k |
≈ 350k LOC of runtime, versus mac's ≈ 43k. Hermes is ~8× mac. It exact-pins every dependency (a deliberate supply-chain stance after the Mini Shai-Hulud worm) and requires Python ≥ 3.11; mac uses loose ranges and claims ≥ 3.9. "Merge Hermes into mac" is really "mac becomes the control plane of a forked Hermes."
4. tokenhub is mature, sophisticated infrastructure¶
Per tokenhub/README.md: a multi-objective routing engine, Thompson-sampling
contextual bandit model selection, AES-256-GCM/Argon2id encrypted vault,
optional Temporal durable workflows with circuit-breaker fallback,
health tracking, an SSE event bus that already broadcasts routing
decisions (success/error/escalation/health/model-selection), and a live
admin decision feed. 111 Go files.
This is exactly the class of mature, separately-languaged, working infrastructure that should not be rewritten in Python to satisfy a "single codebase" aesthetic. That move is the ACC failure mode.
The decision, and why this is not failure #3¶
ACC (714 Rust files) was abandoned for being "too complicated." The reflex worry is that absorbing 350k LOC of Hermes is more complexity, not less. The resolution:
- The complexity that kills these projects is boundary complexity, not line count. Two venvs + two event loops + a 1,372-line out-of-tree patch + runtime string surgery against a daily-moving upstream is a system no single human can hold in their head. That is the actual failure mode. Merging the runtime into one process, one event loop, one dependency set, one repo, reduces that complexity even though it raises the line count.
- mac is already a fork of Hermes (the 1,372-line patch). It currently pays the full cost of forking (no clean upstream tracking) while getting none of the benefit (no in-process observability, no coherent single codebase). This is the worst quadrant. Owning a snapshot moves us to the good quadrant.
- The discipline that keeps it from becoming ACC:
- Snapshot-and-prune. Vendor only the runtime surface mac actually runs
(
agent/,gateway/,providers/, thehermes_cliruntime modules, theskills/toolsactually invoked). Dropwebsite/(731 files),ui-tui/(344),web/(97),infographic/,datagen-config-examples/,locales/, datagen/training scaffolding. - Freeze, don't chase. "Mature" means pinned. We stop tracking the commit-of-the-day. Upstream pulls become deliberate, reviewed snapshot bumps — not a daily moving target under our patches.
- Delete the string surgery. The override the shim injects becomes first-class, owned, tested mac code (see ADR keystone below). No code path rewrites another file's source at runtime.
- Do not absorb tokenhub. Keep the Go service. Close the dark spot by consuming its decision feed into mac observability.
Target architecture¶
┌──────────────────────────────────────────────┐
│ mac monorepo (one Python pkg, one venv, │
│ one asyncio loop) │
│ │
│ mac control plane in-tree vendored │
│ (tasks, leases, ───▶ hermes runtime │
│ ledger, review) ◀── (agent loop, gateway, │
│ ▲ │ providers) — IMPORTED, │
│ │ │ not subprocessed │
│ agent_provider.py (owned provider decision + │
│ │ │ rationale, observable) │
└────────┼───┼───────────────────────────────────┘
│ │ OpenAI-compatible HTTP + decision-feed SSE
│ ▼
┌────────┴──────────┐
│ tokenhub (Go) │ unchanged; its SSE decision feed is
│ routing + vault │ consumed by mac so provider behavior
└───────────────────┘ is legible to the agent
Boundaries that disappear: mac↔hermes process/venv/event-loop split; the clone-and-patch deploy step; the runtime string-surgery shims. Boundary that stays (but becomes transparent): mac↔tokenhub.
Staged migration (tracked as mac tasks, not done in one commit)¶
A 350k-LOC merge landed atomically is itself the ACC mistake. Stages:
- Keystone (this ADR's commit): own the provider decision.
agent_provider.pyreproduces the shim's env-precedence resolution as pure, tested mac code that emits a legible rationale, and surface it in the/startup/hermesreport so the intended provider/model is visible even when the brittle shim misses. - Vendor the pruned runtime snapshot into
src/mac/_hermes/behind a pin (deploy/hermes/SNAPSHOT.md+scripts/vendor-hermes-snapshot.sh); fold the three patches in permanently; merge dependency manifests (adopt Hermes' exact-pin discipline). - Import, don't subprocess. Replace
hermes gateway run(separate venv) with an in-process gateway entrypoint; gateway provider resolution callsagent_providerdirectly. Delete_apply_gateway_runtime_shimand the slack-activation string surgery. - Single-venv deploy.
deploy-mac-fleet.shstops cloning upstream and patching; it installs the monorepo. RetireMAC_HERMES_AGENT_DIR. - tokenhub legibility. mac subscribes to tokenhub's SSE decision feed and attributes each routing decision to the agent/turn that caused it, so "inexplicable provider behavior" becomes an observable, attributable event.
- Memory tier + observability bloat proceed on their own tickets; they are not blocked by, and do not block, the merge.
Consequences¶
- Positive: one event loop; mac can import and introspect the live agent; the monoculture-prevention model override is owned/tested/observable; deploy loses its most fragile step; the dark spot is closeable.
- Negative / accepted cost: mac now owns ~350k LOC of vendored runtime and must do deliberate snapshot bumps instead of free upstream tracking. This cost is already largely paid via the 1,372-line patch; we are making it honest.
- Explicitly rejected: rewriting tokenhub in Python; an atomic big-bang merge; continuing to clone-and-patch a daily-moving upstream.