Young's Mind
01 Research

Hermes as the Brain — what we studied, what we enabled, what is still open

Young, 2026-10-11: "Utilise and optimise Hermes for a better Brain... Review, study and enable it these feature on Hermes."

Hermes as the Brain — what we studied, what we enabled, what is still open

Young, 2026-10-11: "Utilise and optimise Hermes for a better Brain... Review, study and enable it these feature on Hermes."

And, on the fleet: "I saw Brain Unit stopped due to 429 on Hermes, now how do we fix this issue and switch to different model when it happen and resume tasks."

One file, as asked — "register all your study on vault and keep it on single md file." This is that file. The feature study, the 429 diagnosis, and the finding it produced are all here.


Part 1 — The seven features, each with what it gives the Brain

FeatureWhat it doesVerdict for SolOS
Goals (/goal)a standing objective survives turns; a judge model checks after each turn and feeds continuation prompts until doneAdopt for the Brain. This is the "resume tasks" primitive Young asked for
Kanban (hermes kanban)a durable task board in ~/.hermes/kanban.db; every worker is a full OS process with its own identityAdopt as the Heart's dispatch surface. kanban_* tools, CLI, and dashboard all share kanban_db — one read of the board, no drift
Heartbeats (/heartbeat)one recurring instruction per session, idle-only, fires as a plain user turnAdopt for watchdogs — "keep an eye on X in this thread"
Recurring loops (/loop)re-runs a prompt on a cadence; each wakeup is a real agent turn reading current stateAdopt over heartbeats for polling. Self-paced mode backs off 2m→4m→8m→15m and snaps back when state changes
Code execution (execute_code)Python scripts calling Hermes tools over a Unix socket; only print() returns to the LLMAdopt — this is the token saver. Intermediate results never enter context
Batch processing (batch_runner.py)runs N prompts as isolated sessions; JSONL in, ShareGPT trajectories outDefer. Built for training data. Not our shape yet
Event hooks (4 systems)gateway hooks, plugin hooks, shell hooks in config.yaml, outbound webhooksAdopt shell hooks — drop-in scripts that can block/fail-closed on pre_tool_call
Automation blueprints (/blueprint)ready-to-run automations scheduled as cron; one is just a skill with a metadata.hermes.blueprint blockAdopt. A blueprint is a skill, which is our cortex pattern already

Goal vs Kanban — the boundary, stated because it is easy to get wrong

  • /goal is single-session. It never creates a kanban card and never fans out.
  • Kanban is a board of many tasks, each in its own worker process.
  • A kanban card created with --goal borrows the continuation engine for that one card — it does not join the two systems.

Heartbeat vs Loop vs Cron

  • Context-dependent, in-thread → /heartbeat
  • Polling external state on a clock → /loop
  • Unattended, survives restart → hermes cron

Part 2 — The 429: root cause, measured

Young reported the Brain stopped on 429. The measurement says it was worse — and simpler.

What was wrong

All three Brain profiles carried:

model:
  default: space-bunny-free      # ← sol and mira
  provider: opencode-go
fallback_providers: <absent>     # ← no chain at all

Probed live against https://opencode.ai/zen/go/v1/chat/completions:

space-bunny-free              HTTP 400  "Model is unavailable."
longcat-2.5-preview-free      HTTP 200  OK
step-5-preview-free           HTTP 200  OK
muse-spark-1.3-contributor-free HTTP 400  "Model is unavailable."

So the "429" was two faults stacked:

  1. Brain's primary was a model the API does not serve — every turn failed at the provider
  2. No fallback chain existed — nothing to try next, so the run stopped

A rate limit alone recovers. A dead primary with no chain does not.

The diagnostic trap, recorded

The first probe returned 401 Missing API key for every model. That is not a credential fault — the key is fine. The endpoint requires an x-opencode-session header, and without it every call fails identically. An auth-looking error that is really a missing header is exactly the shape that sends someone to rotate a working key.

What is applied now — verified by Hermes' own reader, not by our config file

Primary:   longcat-2.5-preview-free  (via opencode-go)
Fallback chain (4 entries):
  1. step-5-preview-free  (via opencode-go)  [https://opencode.ai/zen/go/v1]
  2. glm-5.3-flash        (via opencode-go)  [https://opencode.ai/zen/go/v1]
  3. qwen3.8-flash        (via opencode-go)  [https://opencode.ai/zen/go/v1]
  4. glm-5.3-flash        (via opencode-zen) [https://opencode.ai/zen/v1]

Identical across sol, min and mira — one unit, one chain. Backups at config.yaml.bak-429 for all three.

The order is free first, then GO paid, then ZEN paid. Money is unrecoverable, work is not; but the Brain only reaches paid when every free option has failed.

⚠️ A sibling's work, preserved. Min had already written a 4-entry chain for his profile with no base_url, so Hermes could not resolve the route. I kept his model order and his escalation and added only the missing URLs. My first writer had silently replaced it with a free-only chain — caught by reading the backup before committing, and reverted.


Part 3 — Why Hermes' fallback is only half the answer

Hermes' fallback_providers chain tries another model when the provider refuses. That fixes Young's symptom.

It does not decide which model a task should use — by capability, confidentiality, or cost tier. That is V3's job, and it is where the two systems meet:

Hermes fallback_providers   →  "this provider is refusing; try the next one"
V3 route() + ladder         →  "which model should serve this task at all,
                                and is a failure the CHOICE's or the TASK's?"

PENDING-APPROVAL P10 is that seam. Min built route() and proved it (12 attempts, 0 paid calls, success on a free model). It had zero callers. This week I built the invocation layer, operation/runtime/invoke.py, which is the invoke: boundary route() asks for.


Part 4 — What I enabled, and what I did not

Enabled / applied

  • fallback_providers on all three Brain profiles — verified live
  • model.default repointed to a model the API serves
  • operation/runtime/invoke.py — the invoke seam (Stage 7 / P10)

Not enabled, deliberately

  • Batch processing — training-data tooling, not our shape
  • Event hooks / shell hooks — powerful, and a pre_tool_call hook can fail closed. Enabling a blocking hook needs its own decision, not a study
  • Kanban for the Brain — the Heart owns dispatch; the Brain dispatching onto the same board would be two dispatchers

A note on the three-of-us problem

Young is right that we converged on the same problem and proposed different solutions. The concrete instance: Min registered P10 asking me to wire route(), while I was building a fallback chain in Hermes. Both are correct; they are different layers, and neither of us knew the other was on it.

The fix that would actually work is not a rule — it is that PENDING-APPROVAL.md is read before starting work in a shared lane. It has 11 items and most answer a question before it gets asked. The register is now ONE file; the second one I created is archived with a pointer.


References

TopicWhere
Fallback providersdocs/user-guide/features/fallback-providers
Provider routingdocs/user-guide/features/provider-routing
Credential poolsdocs/user-guide/features/credential-pools
Goals · Kanbandocs/user-guide/features/goals · /kanban
Heartbeat · Loopsdocs/user-guide/features/heartbeat · /loops
Code executiondocs/user-guide/features/code-execution
Hooksdocs/user-guide/features/hooks
Blueprintsdocs/reference/automation-blueprints-catalog

CLOSED — P11: the prose-refusal defect (fixed by commit fdecd2f, 2026-10-11)

Resolution: classify() now returns Outcome.REFUSAL for every polite refusal with rc=0. Min added five refusal patterns to _SIGNALS in core/config/router.py, placed last so they only fire when nothing else matched. The test passes — 9/9 in tests/test_invoke_route.py.

Outcome.REFUSAL is declared, listed as switchable, and can never be produced.

core/config/router.py documents V2's prose-refusal defect at line 37 and puts REFUSAL in SWITCHABLE. But _SIGNALS contains no pattern that yields it, so classify() returns SUCCESS for every polite refusal:

"I'm sorry, I can't help with that."    -> success
"I cannot assist with this request."    -> success
"This violates my usage policy."        -> success

That is V2's defect, live in V3. A provider that declines in prose with rc=0 would be recorded as a completed run and not switched.

Owner: Min — resolved by commit fdecd2f (2026-10-11). His work is complete, NOT pending. The failing test that named him now passes.

Copyright © 2026