new GET /api/agent-config/{name} returns the contents of
/var/lib/hyperhive/applied/<name>/agent.nix — the file the
container actually builds against. validated against the live
container list to avoid arbitrary filesystem reads.
frontend mirrors the journald viewer: collapsed <details> on each
container row, lazy-fetches on expand, refresh button re-fetches.
restore-keyed (agent-config:<name>) so it survives the dashboard
heartbeat refresh.
read-only — mutating the applied config goes through the existing
request_apply_commit + operator approval flow.
generalises the focus-preservation pattern to expanded details
sections (journald viewer was collapsing on every 5s refresh; same
issue for approval diff blocks). before re-render we snapshot
which <details data-restore-key=...> are open; after render we
re-apply. setting .open = true programmatically also fires the
toggle event, so journald's lazy-fetch listener re-runs cleanly.
tagged: journal:<container>, approval-diff:<id>. anything else
that should survive a refresh just needs a stable data-restore-key
attribute.
bug: all notifications used tag='hyperhive', so each new fire
replaced the previous — operator only ever saw one at a time and
might miss the fact that a second arrived. now per-event tags
(hyperhive:approval:<id>, hyperhive❓<id>,
hyperhive:msg:<at>:<rand>) so distinct events stack in the OS
notification center.
dropped the bogus icon (was pointing at dashboard.css) — some
browsers refuse to display a notification with an invalid icon.
added console.debug at every block point (not supported, permission
not granted, muted) and a 'shown' log on success, so the operator
can see in the browser console exactly why a notification didn't
fire.
note for the operator: most browsers also suppress notifications
while the originating tab is FOCUSED. that's a browser-level
decision, not ours.
every refreshState tick does root.innerHTML = '' across the managed
sections, which destroys any focused input. detect the case before
re-rendering: if document.activeElement is an INPUT / TEXTAREA /
SELECT inside one of the managed sections, skip this tick and try
again in 2s. eventually the operator blurs and the refresh lands.
managed section ids: containers / tombstones / questions / inbox /
approvals. msgflow + message-flow SSE rows don't have inputs so
they're not affected.
three signals fire OS notifications:
- new approval lands in the queue (per id, via /api/state delta)
- new ask_operator question queued (per id)
- broker message sent to operator (live via SSE)
first /api/state render after page load seeds the 'seen' sets
without firing — only items that arrive while the page is open
count. controls in a row under the banner: 🔔 enable
notifications (calls requestPermission, hides on grant), 🔕 mute /
🔔 unmute toggle (localStorage-backed so operator can silence
without revoking the permission), inline status text when blocked
or unsupported.
notification tag='hyperhive' collapses rapid bursts; onclick
focuses the dashboard tab. requires secure context (HTTPS or
localhost) — on other origins the API is unavailable and the
controls hide themselves.
todo: entry dropped.
new GET /api/journal/{name}?unit=&lines= shells out journalctl -M
<container> -b --no-pager --output=short-iso --lines=<N> (cap 5000).
optional unit filter, restricted to hive-ag3nt.service /
hive-m1nd.service so the shell-out can't be coerced into reading
unrelated units. validates the container name against the live list
before invoking journalctl.
frontend renders a collapsed '↳ logs · <container>' details block
on each container row. expanding triggers a lazy fetch; refresh
button re-fetches; unit dropdown switches between the harness
service (default) and the full machine journal. output sits in a
24em-tall monospace pre, auto-scrolled to the bottom on fresh
fetch.
hive-c0re's systemd unit already runs as root, so journalctl has
the access it needs.
manager can pass ttl_seconds to ask_operator. on submit, host
stores deadline_at = now + ttl in operator_questions (new column,
migrated via existing pragma_table_info pattern), spawns a tokio
task that sleeps until the deadline then resolves the question with
answer '[expired]' and fires the same OperatorAnswered helper event.
already-resolved races no-op silently.
dashboard renders a '⏳ MM:SS' chip on the question row when
deadline_at is set. format collapses seconds → s, < 1h → m s, ≥ 1h
→ h m. heartbeat refresh (5s) keeps the chip current; the operator
sees it tick down.
manager prompt + mcp tool description updated. journald viewer per
container queued in todo (separate task).
new POST /cancel-question/{id} resolves a pending operator question
with the sentinel answer '[cancelled]' and fires the usual
HelperEvent::OperatorAnswered so the manager sees a terminal state
and can fall back. uses the same OperatorQuestions::answer path —
no special handling, the manager already has to deal with arbitrary
answer strings.
dashboard renders the cancel as a separate <form> below the main
qform so the answer-merge submit handler on the main form doesn't
inadvertently fire when the operator clicks cancel. confirm dialog
spells out what the manager will see.
ttl-based auto-cancel is still on the todo (would spawn a tokio task
per submitted question).
new Coordinator::kick_agent(name, reason) drops a system message
into the agent's inbox so the next turn picks it up with a 'you
were just (re)started, check /state/ for notes, --continue session
is intact' hint. wakes the turn loop without any harness-side
handling needed — it's just another inbox message with sender =
'system'.
wired from:
- dashboard /start /restart /rebuild handlers (via lifecycle_action's
on-success tail)
- manager mcp_hyperhive_start / restart
dashboard: pending approvals + tombstones + questions now refresh on
a 5s heartbeat when nothing else is happening. previously refresh
only fired on async-form submit or on broker traffic addressed to
operator — manager-queued approvals went through neither, so the
operator had to reload to see them. 5s is the slow-path; 2s
remains for in-flight transients.
submitting R3QU3ST SP4WN immediately queues an approval that lands
in the very next list. the form belonged with that list, not at the
top of containers — the agent doesn't exist yet at form time anyway.
docs: claude.md grows operator_questions.rs / events.rs sqlite /
broker vacuum to the file map; web-ui shape lists the actual current
endpoint set (per-agent cancel/compact/history, dashboard tombstone
purge/answer/spawn); live-view section now describes the state
badge, sticky-bottom scroll, history backfill, and the terminal-
embedded prompt with its slash commands; dashboard-action-surface
rewritten around the new six-section page (containers / kept-state /
questions / inbox / approvals / message-flow) and the two-line
container row. new 'persistence + retention' section documenting both
sqlite databases and their vacuum cadences. readme picks up the new
mgr mcp surface (start/restart/ask_operator) + operator-side
features list + ask_operator answer flow.
todo trimmed of shipped items (bigger terminal / sticky scroll /
cancel button / /compact trigger / /cancel command). new entry for
the two-step spawn-with-preconfig flow.
new section between containers and questions: lists every name with a
state dir under /var/lib/hyperhive/agents/ that doesn't correspond to
a live container. shows state size + last-modified age + whether
claude creds are kept. two actions per row:
- R3V1V3 — queues a spawn approval with the same name (operator
approves to recreate; spawn flow reuses prior config + claude
creds, no re-login needed)
- PURG3 — wipes the agent's state + applied dirs (post /purge-tombstone/
endpoint; refuses if a live container with that name still exists)
dashboard also opens agent links in new tabs now (target=_blank +
rel=noopener) so the operator's overview tab stays put when they
dive into an agent.
ask_operator now accepts a multi: bool. when true and options is
non-empty, the dashboard renders the choices as checkboxes — operator
picks any subset, answer comes back as a ', '-joined string. when
false (default), options are radio buttons.
independent of multi, a free-text input ('or type your own…') is
always rendered alongside options so the operator is never trapped
by an incomplete list. submit merges checked options + free text into
the single 'answer' field.
schema migration: operator_questions grows a multi INTEGER column
with a one-shot ALTER TABLE on open. backward compatible — old rows
default to 0 (not multi).
prompt + mcp tool description updated; existing dashboard css for
.qform was rewritten around the new vertical layout.
backend:
- TransientKind grows Starting / Stopping / Restarting / Rebuilding /
Destroying alongside the existing Spawning. each dashboard handler
(start/restart/kill/rebuild/destroy) wraps the lifecycle call with
set_transient + clear_transient so the dashboard knows what's in
flight. transient kind is surfaced inline on ContainerView.pending
(existing-container actions) — only Spawning (pre-creation) lands
in the separate transients list.
frontend:
- container row is now two lines: identity + meta on top, action
buttons below. less cluttered, leaves room for the pending state
pill. pending rows dim their actions and surface a pulsing
'◐ spawning… / starting… / stopping… / restarting… / rebuilding…
/ destroying…' indicator next to the name.
- 'needs login' / 'needs update' chips moved into a unified .badge
styling for consistency.
- auto-refresh kicks in not only on transient spawn but on any
container with a pending action.
new --purge flag on the destroy verb (cli + admin socket + dashboard).
default destroy still keeps /var/lib/hyperhive/{agents,applied}/<name>/
so recreating with the same name reuses prior config + creds.
with --purge, both dirs go too (config history, claude creds, /state/
notes). no undo. dashboard adds a separate PURG3 button with an
explicit confirmation copy; the existing DESTR0Y button keeps the
soft semantics.
claude.md dashboard-action-surface section updated; todo entry
dropped.
agent page restructure:
- send form moves into the terminal panel as a prompt-style row beneath
the live tail (status line stays above so it still reads as a header).
- live panel + prompt share a single bordered 'terminal-wrap' box.
- harness-alive / login-state status lines drop their decorative ascii
bookends; just a leading dot/glyph remains.
- banner gradient is now a real css gradient with a shimmer animation
toggled by an .active class. turn_start adds it, turn_end removes it.
dashboard side mirrors this: each broker sse event nudges a 4s
shimmer window.
- dashboard container rows drop their static ▓█▓▒░ / ▒░▒░░ glyph
prefixes; the role chips already disambiguate m1nd vs ag3nt.
- empty-state placeholders drop the ▓ bookends.
terminal pre-fill: hive-ag3nt::events::Bus grows a 500-event ring
buffer; new GET /events/history endpoint returns it. The agent JS
fetches history before opening the SSE stream so opening the page mid-
turn shows the last N events instead of a blank panel. The replay
walks turn_start/turn_end pairs to seed the banner-active state
correctly if a turn was still open.
new mcp tool on the manager surface that queues a question on the
dashboard and returns the question id immediately. operator submits an
answer via /answer-question/<id>; the dashboard fires
HelperEvent::OperatorAnswered { id, question, answer } into the manager
inbox so the next turn picks it up.
also: fix async-form button stuck on spinner after successful submit
(refreshState skipped re-rendering, so the button was never re-enabled).