Add CapabilitiesChanged and ToolGroupsChanged DashboardEvent variants
so the P3RM1SS10NS tab reflects perm changes without the operator
navigating away and back.
Backend:
- DashboardEvent::CapabilitiesChanged { seq, caps, descriptions,
assignments } — same payload shape as GET /api/capabilities
- DashboardEvent::ToolGroupsChanged { seq, groups, descriptions,
assignments } — same payload shape as GET /api/tool-groups
- Coordinator::emit_capabilities_snapshot() and
emit_tool_groups_snapshot() — read from the JSON files and broadcast
- rebuild_queue.rs PermChange worker: emit after each successful
commit_capabilities / commit_tool_groups call
Frontend:
- applyCapabilitiesChanged(ev): calls renderCapabilities(root, ev)
- applyToolGroupsChanged(ev): calls renderToolGroups(root, ev)
- Both registered in MUTATION_HANDLERS
- activateTab comment updated (SSE now covers perm changes)
Docs: dashboard.md and CLAUDE.md updated.
This completes SSE coverage for all dashboard sections: SW4RM,
Y3R C4LL, SYST3M, SCH3DUL3S/reminders, and P3RM1SS10NS all
derive live updates from /dashboard/stream.
- activateTab('schedules') now calls both refreshSchedules() and
refreshReminders() since both sections live on the same tab.
(The previous SYST3M/system target was wrong.)
- Update index.html comment to reflect schedules_changed SSE coverage
- Update index.html reminders comment to mention reminders_changed SSE
- Update tabs.js reminders section comment to reflect SSE coverage
Add RemindersChanged SSE event so the pending-reminders list in the
SYST3M tab updates live without polling.
Backend emission sites (every path that mutates the reminders table):
- agent_server: store_remind (remind MCP call)
- dashboard.rs: post_cancel_reminder, post_retry_reminder
- questions.rs: cancel_loose_end Reminder kind
- reminder_scheduler: after each delivery batch (any_delivered)
Coordinator gets emit_reminders_snapshot() mirroring the existing
emit_schedules_snapshot() pattern: lists PendingReminder rows from the
broker and emits DashboardEvent::RemindersChanged.
Frontend: applyRemindersChanged(ev) calls renderReminders(ev.reminders)
and is registered as reminders_changed in MUTATION_HANDLERS.
Docs: dashboard.md reminders_changed entry; CLAUDE.md file map updated.
Agent-triggered schedule mutations (cancel_schedule, fire_schedule_now,
edit_schedule MCP tools) go through manager_server.rs, not the HTTP API
handlers. Approval-resolved SchedulePrompt inserts go through actions.rs.
Neither was emitting SchedulesChanged.
- manager_server.rs: add emit_schedules_snapshot() on Ok in
handle_cancel_schedule, handle_fire_schedule_now, handle_edit_schedule
- actions.rs: emit_schedules_snapshot() after successful
run_approval_schedule_prompt (covers request_schedule_prompt approval
resolving)
Coverage is now complete: every path that writes a scheduled_prompts row
emits the SSE snapshot.
Add `SchedulesChanged` to the dashboard event channel so the
operator's schedule list updates in real time without requiring a
tab-activation or form-submit refresh.
Backend:
- `dashboard_events.rs`: new `SchedulesChanged { seq, schedules }`
variant carrying a full `Vec<WireSchedule>` snapshot (same
snapshot-over-diff rationale as `RebuildQueueChanged`).
- `coordinator.rs`: `emit_schedules_snapshot()` helper — queries the
scheduled_prompts list, converts to wire shape, broadcasts the event.
- `dashboard.rs`: call `emit_schedules_snapshot()` at the end of each
operator API handler that mutates a schedule:
`post_schedule_new`, `post_schedule_fire_now`,
`patch_schedule`, `post_schedule_cancel`.
- `scheduled_prompts_worker.rs`: call `emit_schedules_snapshot()`
after each tick that fires schedules, so `last_fired_at_unix`,
`next_fire_at_unix`, and reaped one-shots surface live.
Frontend:
- `tabs.js`: add `applySchedulesChanged(ev)` — replaces
`schedulesState` from the snapshot and calls `renderSchedulesList()`.
Registered in `MUTATION_HANDLERS` as `schedules_changed`.
Tab-activation re-fetch kept as safety net for approval-path
inserts and disconnect windows; comment updated to reflect this.
Docs:
- `docs/web-ui/dashboard.md`: document `schedules_changed` event.
- `CLAUDE.md`: add `SchedulesChanged` to the file-map entry.
hivectl's DEFAULT_HOST_SOCKET was /run/hive/host.sock, but hive-c0re binds
/run/hyperhive/host.sock (main.rs default + startup log). The doc comment
even claimed it matched hive-c0re's default — it didn't. So 'hivectl agents
restart' / 'restart-all' failed with 'connect to /run/hive/host.sock: No such
file or directory' unless the operator passed --socket explicitly.
Point the default at /run/hyperhive/host.sock.
Per operator review on the PR: the prose marker list ('new password is:',
' to:', 'changed to:', etc.) was speculative — built from a misread
screenshot, not a real reply. The conduit admin bot always code-spans the
password, so the backtick anchor is the verified, complete format. Removing
the markers leaves a ~15-line function that's honest about what it parses.
If a non-code-span format ever appears, extract_new_password returns None and
the diagnostic logging in admin_room_send_and_poll records the raw body, so a
real format change is visible — far better than a speculative marker silently
mis-parsing it. Tests now cover the live format, symbol passwords, the
code-spanned-user-id error guard, and the no-codespan / non-password cases.
The live conduit admin-room reply (observed directly in #admins) is:
Successfully reset the password for user @x:server: `<password>`
The delimiter is ': ' after the user id, NOT ' to:' — so the ' to: ' marker
added earlier never matched the real format, and extract_new_password returned
None for every reply, timing out auto-recovery for every agent.
conduit always renders the password as a backtick code span, so anchor on
that directly: take the content of the first backtick pair when the message
is a password-reset success. Guard against a code-spanned matrix user id in
an error message (a real password has no whitespace and isn't @localpart:server).
Prose markers stay as a fallback for hypothetical non-code-span builds.
Tests cover the exact live format and the code-spanned-user-id error case.
The backend already fetches ReminderStats from the broker RPC and
includes it in the /api/stats snapshot, but the frontend was not
rendering it. Add three chips to the summary row when the field is
present: reminders scheduled / delivered / pending.
mara observed the tuwunel reply renders the new password as a code span,
so the plain message body carries literal backticks:
Successfully reset password for @user:server to: `<password>`
The previous extraction stopped at the first whitespace, capturing the
surrounding backticks ("`<password>`") and producing a login string that
doesn't match the password the bot actually set — recovery would still
fail after parsing.
Strip a leading backtick after the marker and stop the token at the first
whitespace OR closing backtick. Generated passwords contain neither, so a
real password is never truncated mid-token. Two regression tests cover the
backtick-wrapped form, including trailing prose after the closing backtick.
extract_new_password had markers for 'is:', 'changed to:', 'reset to:',
'set to:' etc. but none match tuwunel's actual reset-password reply:
Successfully reset password for @user:server to: <password>
Here the verb ('reset') is not adjacent to 'to:', so every marker missed
and the function returned None for every polled event. The admin-room poll
then ran its full 15s loop without a match and the auto-recovery timed out
on every agent — even though the bot replied correctly and the backward
poll found the message. This is why matrix password auto-recovery kept
looping despite the poll-strategy fix.
Add a generic ' to: ' marker (with surrounding spaces) placed after the
specific verb markers and before the bare 'password:' last resort. Matrix
user ids and server names can't contain ' to: ', so it only ever anchors
on the prose delimiter. Two regression tests cover the exact production
wording.
The c0re-proxy endpoint was removed in the dashboard-state refactor.
Links are now served by agent_links() via two agent-local endpoints:
StateSnapshot.links (GET /api/state, per-agent page) and
DashboardState.links (GET /api/dashboard-state, dashboard card).
Update docs/web-ui/agent.md and the StateSnapshot.links doc comment
to reflect the current source of truth.
The hive-ci-prefetch oneshot has RemainAfterExit=true and is wired to the
container only via wantedBy + before. Once it runs successfully it stays
'active (exited)' indefinitely, so systemd skips it on subsequent container
restarts. The runner-token file it wrote is never refreshed.
This breaks the runner after its first registration: the token written on
the first successful boot is either a placeholder (forge-core-token wasn't
ready yet) or a registration token that has since been consumed/rotated.
On the next container restart prefetch does not re-run, the stale token
persists, and the in-container register service fails with
'invalid_argument: runner registration token not found' — exactly the
symptom in the field (worked briefly, two orphan runners registered, then
permanently offline).
Add partOf = [ nixos-container@hive-ci.service ] so a container stop/restart
propagates to the prefetch unit, forcing it to re-run and fetch a fresh
registration token before the container comes back up. before= still orders
it ahead of the container start within the same transaction.
ensure_claude_dir creates the dir as 0755 but cannot re-chmod after
hive-agent-user-migrate chowns it to the agent user (EPERM — non-owner).
The activation script runs as root and can always chmod it. Add an explicit
'chmod 755 $homeDir/.claude' after the existing chown so existing 0700
dirs from pre-fix containers are corrected on the next container boot.
Without this, all agents with pre-existing .claude dirs show 'needs login'
in the dashboard even with working sessions, because hive-core cannot list
the 0700 dir owned by a different user.
Replace the stale GET /api/agent/{name}/links reference with
GET /api/dashboard-state (merged in #1292). The new endpoint is
served by the agent backend and returns DashboardState carrying
links, status_text, rate_limited, ctx_tokens, and
context_window_tokens — all agent-owned fields that were previously
(mis)read by hive-c0re from disk.
Also update the ctx_tokens description: it's now sourced from
DashboardState rather than ContainerView (which no longer carries
ctx_tokens/context_window_tokens).
The forward-pagination approach (dir=b anchor → dir=f poll) fails in
production: commands sent as @hive time out consistently even though
tuwunel responds. Root cause is likely a pagination-token direction
incompatibility in some tuwunel builds where 'end' from dir=b cannot
be used as 'from' for dir=f.
New strategy: send the command, capture the event_id from the PUT
response, then poll dir=b&limit=20 each tick. Events come back
newest-first; walk until we hit our own event_id, then stop — anything
before that marker arrived after our command. Simpler, avoids stored
tokens entirely.
Also:
- check formatted_body in addition to body (some admin bots put content
only in HTML formatted_body)
- add more extract_new_password patterns: 'changed to:', 'reset to:',
'set to:', 'new password:', 'password:' to handle different tuwunel
version response formats
- add unit tests for new patterns
Fixes#1283.
The old measurement used tabbar.clientWidth which includes horizontal
padding (~2em / ~30px total), causing an over-allocation of the same
amount. Flex gap between tabs (0.2em) was also not counted in the
accumulated offsetWidth, adding another ~20px of error. Together these
caused the algorithm to leave too many tabs visible, overflowing the
bar and pushing the ⋮ button off the right edge of the screen.
Replace the clientWidth + cumulative-offsetWidth approach with
getBoundingClientRect: the cutoff is the tabbar's actual right edge
minus its right padding, minus a reserve for the overflow wrapper.
Individual tabs are overflowed once their right edge exceeds the
cutoff; subsequent tabs are overflowed unconditionally to keep the
visible set contiguous and left-anchored.
Two cooperating fixes:
1. overflow-anchor: none on .live — disables browser scroll anchoring
so the manual scrollTop compensation in loadMore() is the sole
mechanism. Without this the browser auto-adjusts scrollTop when
rows are prepended above the viewport, then our += delta doubles
the compensation, causing the erratic jump.
2. updateLoadMoreBtn() moved before the beforeH baseline capture —
if histHasMore becomes false the load-more button is removed here
rather than after the delta is applied, so the button height is
already baked into beforeH and doesn't shift the viewport post-
compensation.
common.css sets `li { padding: 0.5em 0 }` globally. The .agent-menu-sep
li inherits this, bloating the 1px separator into a ~16px thick bar.
Add `padding: 0` to .agent-menu-sep to reset it.
Now that the dashboard fetches agent-owned state directly from
GET /api/dashboard-state (via gateway), hive-c0re no longer needs
to read those fields from disk on the agent's behalf.
Removed from ContainerView:
ctx_tokens, context_window_tokens, rate_limited,
extra_links, status_text, status_set_at
Removed from container_view.rs:
DashboardLink struct, build_nav_links, read_dashboard_links,
read_status, is_rate_limited, read_last_turn, resolve_ctx_window
(and the resolve_ctx_window unit tests)
Removed from dashboard.rs:
GET /api/agent/{name}/links route + get_agent_links handler
dashboard JS (tabs.js):
Merged rate_limited, ctx-window badge, and status-text rendering
into the async dashboard-state fetch block. c0re still provides
needs_login (auth sentinel on host), needs_update, pending_reminders,
running, deployed_sha, parent — all genuinely host-side fields.
hive-ag3nt now exposes a lean DashboardState endpoint that returns the
agent-owned fields the dashboard card needs:
- status_text / status_set_at (from hyperhive-status on disk)
- ctx_tokens / context_window_tokens (from Bus)
- rate_limited (from Bus)
- links (from agent_links — includes screen link for GUI agents,
which c0re's disk-based fallback cannot determine)
The dashboard fetches `${containerBase}/api/dashboard-state` instead of
the previous c0re proxy `/api/agent/{name}/links`. For gateway deployments
this is a same-origin call to the agent via the gateway's unix-socket
upstream; for direct TCP it hits the agent port directly. Both paths fail
gracefully (empty strip) when the agent is starting up.
The main behaviour fix: GUI agents now show the screen link in the dashboard
nav strip. c0re's build_nav_links reads /etc/hyperhive/gui.json from outside
the container (not possible), so it always omitted the screen link.
Add a '❓ N' badge to each container row showing the count of pending
questions where the agent is the asker (awaiting an answer) or the
target (owes a reply). Derived live from questionsState — no backend
field needed.
Details:
- New .badge-loose-ends CSS class (purple, matching Q33R1ES section colour)
- Badge tooltip breaks down the count: 'N asked, N to answer'
- renderContainersFromState() called on QuestionAdded/QuestionResolved
so the badge updates instantly when question state changes
- Existing ⏰ reminders badge kept separate (different signal)
The catch-all renderer was dumping raw JSON (in loud orange) for
claude's system events like api_retry — producing noise like:
! {"attempt":1,"error":"unknown","subtype":"api_retry",...}
Replace the single 'init' filter with a full system-event block:
- api_retry → muted note: '⚠ api retry 1/10 · unknown · 502ms'
- api_error → amber note: '✗ api error · <message>'
- other subtypes (context_window_exceeded, etc.) → '⚙ <subtype>'
The loud orange sys catch-all is now reserved for truly unrecognised
top-level event types, not routine operational noise.
The pending-label derivation had no case for op.kind === 'restart', so
it fell through to the rebuild/rebuild-queued else branch — showing a
misleading 'rebuild pending' badge when the operator issued a restart.
Add explicit 'restarting' / 'restart queued' cases alongside the
existing meta_update and destroy cases.
Also add restart / startup_sweep / perm_change to QUEUE_KIND_GLYPH so
they render with a meaningful glyph (↺ / ⚡ / 🔑) instead of '?' in
the rebuild-queue panel.
Add clarification for the agent parameter: omit to list own threads,
pass agent name for direct children (always accessible), or query_agent_state
capability for non-children. Note that hive-wide '*' query unavailable
on agent socket.