Skip to content

feat(acp): Hermes runtime discovery and durable session load#2633

Open
joelbrilliant wants to merge 5 commits into
block:mainfrom
joelbrilliant:feat/hermes-native-profile
Open

feat(acp): Hermes runtime discovery and durable session load#2633
joelbrilliant wants to merge 5 commits into
block:mainfrom
joelbrilliant:feat/hermes-native-profile

Conversation

@joelbrilliant

@joelbrilliant joelbrilliant commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

Summary

Clean, mergeable Hermes attachment path for Buzz Desktop + buzz-acp:

  • Register Hermes Agent as a known ACP runtime (Desktop discovery + arg normalisation for hermes / hermes-acp)
  • Prefer official hermes acp; keep direct hermes-acp fallback
  • Command-specific readiness: hermes acp --check / hermes-acp --checkAdapterMissing when ACP deps are absent (not a fake "logged out")
  • Exact python / python3 marker-owned process recognition (Hermes console scripts under a venv)
  • Persist channel→ACP session bindings and restore with session/load when the agent advertises loadSession
  • Onboarding: Hermes appears in first-run harness picker as a CLI-native ready path
  • Owner Rocky dogfood guide: docs/hermes-agent-rocky.md
  • buzz-acp README Hermes section

Hermes keeps its own tools/skills/memory/config. Buzz does not inject Buzz MCP for Hermes by default.

Live dogfood (2026-07-24)

Owner-only Rocky DM on Desktop worked end-to-end after:

  1. Hermes home: acp.tool_policy: profile (companion PR below)
  2. Managed agent: hermes, empty args, HERMES_HOME, BUZZ_ACP_NO_MEMORY=true (must be the string true, not 1)
  3. Instance parallelism=1 (higher values spawn N ACP processes against one profile home and break cold-start turns)
  4. Full Hermes tools in session; multi-turn DM replies; session bind across harness restarts

Relationship to other PRs

PR Scope Status
This PR (#2633) Local managed Hermes + durable session load + onboarding Mergeable, focused
#2468 (nytemode) Hermes + large external-agent directory / kind 10100 / relay observer presentation Open, CONFLICTING with main
Nous #70326 acp.tool_policy: profile full tool surface Companion Hermes capability

Stolen from #2468 without the conflicting external-agent surface: readiness probe as AdapterMissing, python/python3 interpreter recognition, operator docs patterns.

Not in this PR (intentionally): external-agent directory cards, kind 10100 profile publishing, owner presentation overrides, relay observer ingestion. That can land as a follow-up once #2468 is rebased or split.

Deeper native Hermes product integration (first-class Nous surface beyond ACP bridge) is best done by the Nous Research team upstream.

Test plan

  • cargo test -p buzz-acp --lib (595 passed)
  • Hermes arg normalisation / readiness probe / process-name unit tests
  • Session store round-trip unit tests
  • Desktop hermes_* unit tests green
  • Owner DM dogfood with HERMES_HOME + session/load after restart (manual)
  • Reviewer: Desktop catalog shows Hermes when hermes is on PATH
  • Reviewer: hermes without ACP extra surfaces AdapterMissing (not ready)

DCO

All commits carry Signed-off-by: joelbrilliant <joelbrilliant1@gmail.com>.

@joelbrilliant
joelbrilliant requested a review from a team as a code owner July 23, 2026 22:00
Register Hermes as a known ACP runtime in Desktop and buzz-acp, with
command normalisation for hermes and hermes-acp entrypoints.

Persist channel→session bindings across harness restarts and restore via
session/load when the agent advertises loadSession (Hermes does). History
replay stays on the wire log path and is not re-published to Buzz.

Add owner dogfood guide for attaching Rocky with HERMES_HOME and
acp.tool_policy: profile.

Signed-off-by: joelbrilliant <joelbrilliant1@gmail.com>
Include Hermes Agent in the onboarding runtime picker alongside Claude
and Codex, treat it as a CLI-native ready path (no Buzz provider pick),
and lock provider selection the same way as other profile-owned runtimes.

Signed-off-by: joelbrilliant <joelbrilliant1@gmail.com>
Steal the strongest Hermes-specific pieces from block#2468 without the
conflicting external-agent directory work:

- command-specific hermes/hermes-acp --check readiness as AdapterMissing
- exact python/python3 marker-owned interpreter recognition
- dogfood guide fixes (BUZZ_ACP_NO_MEMORY=true, parallelism=1)
- buzz-acp README Hermes section

Signed-off-by: joelbrilliant <joelbrilliant1@gmail.com>
@joelbrilliant

Copy link
Copy Markdown
Contributor Author

Review request

Focused Hermes path for local managed agents (not the external-agent directory work in #2468).

What this adds

  • Desktop + buzz-acp Hermes discovery (hermes / hermes-acp)
  • Durable channel→session bind via session/load
  • Onboarding picker entry
  • Readiness: missing ACP deps → AdapterMissing (not fake logged-out)
  • Exact python/python3 marker-owned cleanup for Hermes venv scripts

Dogfood (2026-07-24)
Owner-only Rocky DM on Desktop: full Hermes tools after companion Nous acp.tool_policy: profile, instance parallelism=1, BUZZ_ACP_NO_MEMORY=true (string true, not 1), multi-turn replies + session restore path.

Checks

  • DCO / Semgrep / zizmor green
  • cargo test -p buzz-acp --lib 595 passed locally
  • Desktop hermes + interpreter unit tests green

Companion
NousResearch/hermes-agent#70326

Happy to split or drop anything that blocks merge. External-agent / kind 10100 / relay-observer stays out of this PR on purpose so it can land cleanly.

@rungmc357

Copy link
Copy Markdown

Hey Joel — I built on #2633 locally, preserving your three commits and authorship, and found one data-loss issue worth flagging before merge.

SessionStore is keyed by command/args but cached per process, so two buzz-acp processes using the same Hermes invocation can overwrite each other’s channel bindings. I fixed it with a stable sibling lockfile, a fresh read under lock for every get/put/remove, and atomic mutation writes. The regression opens two stores before either writes and proves interleaved writes and removals cannot clobber or resurrect bindings.

The full branch also includes optional Hermes profile selection (hermes --profile NAME acp) and canonical supports_profile metadata from my earlier implementation. Your #2633 work remains credited as the operational backbone; #2468 and @NYTEMODEONLY remain credited as prior Hermes groundwork.

Verification: focused session-store 3/3; lost-update regression passed 75 repeated runs; clippy, formatting, and diff checks green; buzz-acp 595/596 with the sole keepalive timing failure reproduced unchanged on the frozen base; broader Hermes discovery/runtime/profile/onboarding/frontend gates passed.

I didn’t open a competing PR. Happy to split the session-store fix or profile slice however is easiest for you to land.

SessionStore was process-local cache + whole-file rewrite, so two
buzz-acp processes sharing the same agent command/args identity could
clobber each other's channel bindings.

Reload under a sibling lockfile for every get/put/remove, keep atomic
writes, recover put from corrupt sidecars, and cover independent-open
interleaving in regression tests.

Reported and prototyped by @rungmc357 on PR block#2633.

Co-authored-by: Georgio Constantinou <210088133+rungmc357@users.noreply.github.com>
Co-authored-by: joelbrilliant <joelbrilliant1@gmail.com>
Signed-off-by: joelbrilliant <joelbrilliant1@gmail.com>
@joelbrilliant

joelbrilliant commented Jul 24, 2026

Copy link
Copy Markdown
Contributor Author

Thanks - good catch.

Agreed on the SessionStore lost-update: process-local cache + whole-file rewrite is unsafe when two buzz-acp processes share the same command/args identity. I've landed the session-store fix on this PR (lock + reload under lock + atomic write + independent-open regression, plus put recovery from a corrupt sidecar):

Verification here:

  • cargo test -p buzz-acp --lib session_store4/4
  • cargo test -p buzz-acp --lib596 passed / 1 failed, sole failure acp::tests::keepalive_resets_idle_past_deadline (timing flake; same class you saw on the frozen base)

I'd like to keep #2633 scoped to discovery + durable load + onboarding. Optional Hermes --profile / supports_profile is better as a follow-up PR so this one stays reviewable. Happy to review that split if you open it against main (or against this branch after CI settles).

Credits stay as noted for #2468 / @NYTEMODEONLY and your store fix. No need for a competing PR.

@rungmc357

Copy link
Copy Markdown

Thanks, that sounds right. I don’t want to open a competing PR. Happy to help get #2633 through, and I’ll hold the optional profile support for a separate follow-up after this lands.

@joelbrilliant

Copy link
Copy Markdown
Contributor Author

Appreciate it.

Store fix is on the branch (3025359), DCO / Semgrep / zizmor green, and the PR is mergeable pending maintainer review. Holding profile support for a follow-up after this lands is perfect.

If you want to help while we wait, the highest-leverage things are:

  1. A second pair of eyes on the dogfood notes in docs/hermes-agent-rocky.md (especially parallelism=1, BUZZ_ACP_NO_MEMORY=true as the string true, and acp.tool_policy: profile on the Hermes side)
  2. Flagging any residual multi-process session-store edge cases you still see against 3025359

Otherwise we can leave it for review as-is. Thanks again for catching the lost-update before merge.

@rungmc357

Copy link
Copy Markdown

I took a second pass against 3025359. The dogfood notes look right on parallelism=1, BUZZ_ACP_NO_MEMORY=true rather than 1, and acp.tool_policy: profile.

I found two residual session-binding edges:

  1. The restore path can still delete a newer binding. Process A reads session X, process B writes Y, then A’s failed load of X triggers a key-only remove that deletes Y. A remove_if_equals(X) under the existing lock, plus an interleaving regression, should close it.
  2. The multi-profile section presents a different HERMES_HOME as equivalent to unique -p <profile> acp args, but durable store identity excludes environment and managed-agent identity. Two managed agents in the same channel with identical command/args and different homes can therefore collide. The simplest short-term fix is to require unique profile args and remove the HERMES_HOME-only alternative until the store is namespaced.

I didn’t find anything else material.

Failed session/load used key-only remove, so process A reading X then
failing load could delete process B's newer Y for the same channel.
Clear bindings with remove_if_equals under the existing lock, and cover
the interleaving race in regression tests.

Durable store identity is command + args + channel, not HERMES_HOME.
Require unique -p <profile> acp args for multi-profile managed agents
and drop the HERMES_HOME-only alternative from dogfood docs until the
store is namespaced.

Also note long-lived ACP OAuth stay-alive ops in the Rocky dogfood guide.

Reported by @rungmc357 on PR block#2633.

Co-authored-by: Georgio Constantinou <210088133+rungmc357@users.noreply.github.com>
Co-authored-by: joelbrilliant <joelbrilliant1@gmail.com>
Signed-off-by: joelbrilliant <joelbrilliant1@gmail.com>
@joelbrilliant

joelbrilliant commented Jul 24, 2026

Copy link
Copy Markdown
Contributor Author

Second pass taken - both residual edges were real.

1. Failed load deleting a newer binding
Confirmed in try_load_persisted_session: key-only remove after a failed session/load of X could drop a concurrent Y.

Landed remove_if_equals(expected) under the existing lock, wired the restore path to use it, and added an interleaving regression (remove_if_equals_does_not_delete_newer_binding).

2. HERMES_HOME-only multi-profile collision
Agreed. Durable store identity is command + args + channel, not env / managed-agent id. Updated dogfood docs + buzz-acp README to require unique -p <profile> acp args for a second Hermes profile and removed the HERMES_HOME-only alternative until the store is namespaced.

Commit: 3c83c15

Verification:

  • cargo test -p buzz-acp --lib session_store6/6

Thanks again for the careful review.

@joelbrilliant

Copy link
Copy Markdown
Contributor Author

Repositioning this against #2773, now that #2656 is being closed in its favour.

The overlap I flagged on #2656 was runtime registration and model-selection UX, and BYOH supersedes that half properly - a generic seam beats two harness-specific PRs, so that's the right call. What it doesn't cover is durable sessions: I had a look through #2773 and there's no session_store, no pool binding, no session/load (the only persistence in there is composer draft state). So Buzz's channel-to-session map is still in-memory only after BYOH lands, and a restart still orphans live agent sessions - the agent has the conversation, Buzz has forgotten which session belonged to which channel, and the user quietly starts over.

That's the half this PR does, and it's independent of how a runtime gets registered. So the plan I proposed on #2656 still works with #2773 as the target instead: land BYOH, then I rebase this down to just session_store.rs and pool.rs on top of the new seam and drop anything that overlaps. I've already built that slice locally against current main - 651 lines, cargo test -p buzz-acp green at 604 including the six store tests - so it's a rebase rather than a rewrite whenever #2773 is in.

Also worth calling out that @rungmc357's review here found two real session-binding edges (a failed restore deleting a newer binding, and a multi-profile identity collision). Both are fixed in the current head with remove_if_equals under the existing lock and an interleaving regression, so the persistence layer has had a proper pass rather than going in cold.

@wpfleger96 no rush on this at all - happy to wait until BYOH is settled and then do the rebase against whatever the final seam looks like. Just sing out if you'd rather I close this and open a fresh persistence-only PR instead, either way suits.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants