spike(#446): herdr × mycelium wake path — doc + PoC - #540
Draft
juliarvalenti wants to merge 7 commits into
Draft
Conversation
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.
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
|
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.
Contributor
Author
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.

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 becomesherdr agent prompt <pane>: the agent wakes in place with full context and replies through the room. Strictly better than the removedclaude -pcold-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:
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 + durablehandle→paneregistry (~/.mycelium/herdr/registry.json).mycelium herdrgroup —map / unmap / ls(live liveness column)/ status / wake.herdr.autowakewired intoagent invoke(config-gated, fail-soft; degrades to the cursor).[herdr]section; fixessave()global-section whitelist that dropped it.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:lineanchors:fastapi-backend/app/routes/participate.py:158): a handle's cursor inits to current transcript end on first poll, soinvoke → wake → first-awaitmisses the just-posted message. Correct for a looping resident, wrong for cold-wake. A second invoke (cursor established) delivered fine.commands/agent.py:174): post-wake,_is_residentreads 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
await/respond— herdr is an optional wake, not a requirement.Closes the exploration in #446.
Known gaps
only into the human message path (
routes/messages.pyPOST /messages, i.e.the UI /
room send). Agent replies go out viamycelium respond→routes/participate.pyPOST /reply, which does not call the_enqueue_herdr_wakeshook — so an agent@-mentioning another agent enqueuesno 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
/replypath (and ideallyfactor it into one shared post-message hook so no future write path can miss it).
herdr infers
workingfrom the terminal-title spinner (osc_title_working,prio 1100 >
live_prompt_box, 950), which can stick after an interrupted turn —a pane reads
workingwhile idle and its held wake never releases. Reliablestate needs
herdr integration install claude.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.
POST /sessions/herdr-presenceandGET /sessions/herdr-wakeshave no authz, and awake ultimately runs
herdr agent promptagainst 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.