Skip to content

spike(#446): herdr × mycelium wake path — doc + PoC - #540

Draft
juliarvalenti wants to merge 7 commits into
mainfrom
herdr-integration-poc-446
Draft

spike(#446): herdr × mycelium wake path — doc + PoC#540
juliarvalenti wants to merge 7 commits into
mainfrom
herdr-integration-poc-446

Conversation

@juliarvalenti

@juliarvalenti juliarvalenti commented Aug 15, 2026

Copy link
Copy Markdown
Contributor

Spike for #446 — does herdr solve mycelium's persistent-runtime gap, and what's the seam. Exploration / de-risking, not a commitment to build. Two deliverables: a "what's possible" doc and a working PoC of Shape 1.

The gap it closes

A non-resident mycelium agent invoke <handle> queues on the durable cursor with nothing to wake it — it "goes to the aether." herdr keeps agent sessions alive + addressable, so the wake becomes herdr agent prompt <pane>: the agent wakes in place with full context and replies through the room. Strictly better than the removed claude -p cold-spawn (which discarded context every turn), without the coupling that rotted openclaw/hermes.

Proven live (end-to-end)

Against a running backend + a real persistent Claude Opus session in a herdr pane:

invoke poc-reviewer → not resident → herdr agent prompt w2:pV "<wake prompt>"
  → live session woke IN PLACE, ran `mycelium await` then `mycelium respond`
  → reply landed: poc-reviewer [broadcast]: "Confirmed — the herdr wake path delivered this…"

What's here

  • docs/design/herdr-integration.md — the "what's possible" deliverable: surfaces herdr exposes, the 4 shapes re-scoped after a live 0.8.0/protocol-19 probe, user stories, open questions.
  • integrations/herdr/ — fail-soft CLI bridge + durable handle→pane registry (~/.mycelium/herdr/registry.json).
  • mycelium herdr group — map / unmap / ls (live liveness column) / status / wake.
  • opt-in herdr.autowake wired into agent invoke (config-gated, fail-soft; degrades to the cursor).
  • config — new [herdr] section; fixes save() global-section whitelist that dropped it.
  • tests — 18 mocked-herdr unit tests (registry, parsing, wake, invoke gate). Full CLI gate green (ruff/format/ty + 314 tests).

Two backend-await findings the live run surfaced (NOT herdr bugs)

These are the spike's real payoff — the issue's "wake semantics" / "lifecycle ownership" open questions, now concrete with file:line anchors:

  1. Cold-wake races the first-await cursor init (fastapi-backend/app/routes/participate.py:158): a handle's cursor inits to current transcript end on first poll, so invoke → wake → first-await misses the just-posted message. Correct for a looping resident, wrong for cold-wake. A second invoke (cursor established) delivered fine.
  2. Presence lease outlives actual awaiting (commands/agent.py:174): post-wake, _is_resident reads True from a stale lease even though the agent settled and isn't looping — so the next invoke skips the wake.

Both are backend fixes, deliberately out of scope for this CLI-side PoC.

Non-goals

  • Not replacing await/respond — herdr is an optional wake, not a requirement.
  • Not spawning panes/agents from mycelium — the PoC only drives agents the user created.
  • Not committing to build — this is to decide whether it's worth it and what the seam is.

Closes the exploration in #446.

Known gaps

  • Agent-to-agent mentions don't wake (structural). Wake-on-mention is hooked
    only into the human message path (routes/messages.py POST /messages, i.e.
    the UI / room send). Agent replies go out via mycelium respond
    routes/participate.py POST /reply, which does not call the
    _enqueue_herdr_wakes hook — so an agent @-mentioning another agent enqueues
    no wake and the tag falls into the aether. Human→agent works; agent→agent
    doesn't. Fix is to run the same mention→enqueue on the /reply path (and ideally
    factor it into one shared post-message hook so no future write path can miss it).
  • herdr state is title-scraped (flaky). Without the herdr claude state-hook,
    herdr infers working from the terminal-title spinner (osc_title_working,
    prio 1100 > live_prompt_box, 950), which can stick after an interrupted turn —
    a pane reads working while idle and its held wake never releases. Reliable
    state needs herdr integration install claude.
  • Backend restart blanks presence for ~45s. The sync bridge only re-pushes on
    change/heartbeat, so after a backend bounce the badges/wakes go dark until the
    next heartbeat (or a bridge restart). Bridge should force a push per tick or the
    backend should request a resync.
  • Security: the two new endpoints are unauthenticated. POST /sessions/herdr-presence and GET /sessions/herdr-wakes have no authz, and a
    wake ultimately runs herdr agent prompt against a local coding-agent pane —
    room-post content can steer a local agent. Single-user-local PoC only; must be
    architected around the D1 JWT/SPIRE identity work before anything hosted/multi-user.

Closes the persistent-runtime "aether" gap: a non-resident `agent invoke`
now wakes the handle's mapped herdr pane in place (full context) instead of
only queuing on the durable cursor — the strictly-better replacement for the
removed cold-spawn daemon.

- docs/design/herdr-integration.md: the "what's possible" deliverable, grounded
  in a live herdr 0.8.0/protocol 19 probe + a proven live end-to-end, incl. two
  backend-await seam findings the run surfaced (first-await cursor race; presence
  lease outliving actual awaiting).
- integrations/herdr/: fail-soft CLI bridge + durable handle→pane registry.
- `mycelium herdr` group: map/unmap/ls (with liveness)/status/wake.
- opt-in `herdr.autowake` wired into `agent invoke` (config-gated, fail-soft).
- config: add [herdr] section; fix save() global-section whitelist dropping it.
- tests: 18 mocked-herdr unit tests (registry, parsing, wake, invoke gate).
Reconcile the backend's presence lease against herdr availability — the fix for
the PoC's Finding 2 (a containerized, herdr-blind backend can report a stale
lease as "resident"). The CLI is the one layer that reaches both the backend
/members API and the local herdr socket, so reconciliation lives here.

- agent invoke: `_resolve_presence` makes herdr authoritative for a mapped
  handle — live+idle → wake in place (over a stale lease), working/blocked →
  resident, dead pane → stale (queued + warn); unmapped → lease behavior.
  Replaces `_try_herdr_wake`.
- `mycelium herdr ls`: now a 3-way reconciliation (registry × backend presence
  slim/lease × live herdr state) with `stale lease` / `herdr-only` / `in sync`
  verdicts.
- tests: herdr-grounded residence (idle wins over stale lease, stale mapping)
  + reconcile-verdict table. 320 pass.
@juliarvalenti

Copy link
Copy Markdown
Contributor Author

Follow-up (pushed): residence grounded in herdr's live agent list.

Per review, the set of lease-holding agents is now reconciled against herdr availability — the fix for Finding 2 (the containerized, herdr-blind backend reporting a stale lease as "resident"). The CLI is the only layer that reaches both the backend /members API and the local herdr socket, so it lives here.

  • agent invoke_resolve_presence makes herdr authoritative for a mapped handle: live+idle → wake in place (over a stale lease), working/blocked → resident, dead pane → stale (queued + warned); unmapped handles keep lease behavior.
  • mycelium herdr ls — now a 3-way reconciliation (registry × backend presence slim/lease × live herdr state) with stale lease / herdr-only / in sync verdicts. Verified live:
    handle          mycelium   herdr   verdict
    @poc-reviewer   absent     done    herdr-only (not joined)
    
    (the agent is alive in herdr but its lease has lapsed — the inverse of a stale lease).
  • +6 tests (idle-wins-over-stale-lease, stale-mapping, reconcile verdicts). 320 pass, gate green.

Answers "which herdr agents go in which room": a herdr workspace is the natural
unit for a coordination space (design Shape 2).

- `mycelium herdr enroll --workspace w2 --room X` registers a claude_code
  manifest for every live agent in the workspace, binds handle↔pane, and (with
  --wake) wakes each to join. Idempotent; --dry-run to preview.
- Handles derive from the **tab name** by default (--name-from tab|pane),
  sanitized to the manifest handle rule and disambiguated by pane suffix on
  collision — so w2 enrolls as @PR-Review / @test-area / @herdr, not opaque ids.
- bridge: `tab_labels()` (tab_id → label). doc: "Determining which herdr agents
  belong to which room" (binding vs enrollment, the two layers).
- tests: sanitize/derive/dedup handle logic. 323 pass, gate green.
A host-side sync bridge mirrors herdr's live agent states into the backend
presence surface so the frontend can show idle/working/blocked per agent —
the honest answer after the cold-spawn daemon's removal (backend is
containerized and herdr-blind; the CLI is the only layer that sees both).

- backend: MemberPresence gains `status`; a herdr presence overlay (TTL'd) in
  room_channels feeds presence() but NOT members() — visible, not a roster
  participant. New POST /sessions/herdr-presence; /members returns kind=herdr.
- cli: `mycelium herdr sync [--watch]` polls herdr, maps registry panes→status
  per room, pushes on change + heartbeat; Ctrl-C clears the overlay.
- frontend: PresenceMember.status; Monogram badge colored by herdr state
  (working amber/pulse, blocked red/pulse, idle/done green); agents-panel label
  "herdr · <state> · not joined".
- tests: backend presence overlay (surfaces without joining roster, overlays a
  lease, TTL expiry); CLI _collect_presence. 324 CLI + 39 FE + backend green.
… doorbell

Makes tagging a herdr agent from the room/UI actually wake it. The backend is
herdr-blind, so it enqueues wake "doorbells" the host-side `herdr sync` bridge
drains and turns into `herdr agent prompt`. Presence up + wakes down = the
bridge is bidirectional.

- backend: on an @-mention of a herdr-present handle, enqueue a wake
  (routes/messages.py). Queue is hold-until-idle — a tag for a working/blocked
  agent is held and released only once herdr shows it idle (TTL'd against a
  never-idle agent). GET /sessions/herdr-wakes drains it. herdr state is
  authoritative over a stale lease.
- cli: `herdr sync --watch` also drains wakes each tick and prompts the pane.
  The wake is a terse *doorbell* (no payload) — the agent reads the room itself,
  which dissolves accumulate/dedup/first-await-cursor issues and preserves agency.
- docs: wake-on-mention + hold + the OSC-title state-scrape reliability finding
  (install the herdr claude hook for authoritative state) + the JWT/SPIRE
  security boundary (both new endpoints are unauthenticated; PoC-local only).
- tests: backend hold/release/expiry/dedupe (17 room_channels). CLI 324 green.

Single-user-local PoC. The presence/wake endpoints are unauthenticated and a
wake steers a local coding agent, so this is gated behind D1 JWT/SPIRE identity
before anything hosted/multi-user.
…e UI

The herdr lifecycle *is* the mycelium lifecycle — one command, one seam.

sync is now the single lifecycle bridge: `herdr sync --workspace <id>
--room <room>` persists a durable workspace→room binding and every run (or
--watch tick) reconciles membership (auto-enroll live agents as members,
retire members whose pane closes), pushes liveness, and drains wakes. The
separate `enroll` command is removed — mapping and membership were the whole
UX confusion. Teardown is provenance-scoped (`HerdrPaneMapping.managed`) so a
hand-run `herdr map` is never auto-retired; `agent._delete_manifest` is
extracted so the bridge reuses the exact unregister path.

Agent→agent mentions now wake: the mention→enqueue hook is factored into one
shared `room_channels.enqueue_herdr_wakes_for_mentions` called from both the
human `POST /messages` path and the agent `/reply` path (it was only on the
former, so agent tags fell into the aether). Self-mentions excluded.

Presence surface carries more: `wake_pending` (held-until-idle doorbell) and
`title` (herdr terminal title = the agent's current task) flow bridge →
/members → UI.

Roster redesign (agents-panel + monogram):
- state as a colored halo ring that breathes via box-shadow (opaque, so the
  row line never bleeds through); idle drops the ring entirely, keeps a quiet
  dot; mail badge overlays a queued wake
- the ram mark replaces the "herdr … not joined" jargon; the freed subtext
  line shows the agent's current task, state-tinted
- rich per-row hover tooltips (Base UI — no shadcn/radix) with full detail

Tests: backend 439, CLI 331 (+ ty clean), frontend tsc + 39.
@juliarvalenti

juliarvalenti commented Aug 15, 2026

Copy link
Copy Markdown
Contributor Author
Screenshot 2026-08-15 at 3 30 16 PM

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.

1 participant