Follow-up to the light-DOM custom-elements pilot (mara: 'that one landed
and works. i dont really like the css still being shared - id like that
to be split by component, with common stuff via @include').
<hive-dialog> and <hive-toast> now attach a shadow root and adopt a
component-scoped CSSStyleSheet built from a template-string constant in
modal.js (DIALOG_CSS / TOAST_CSS), plus a new shared/src/component-
styles.js sheet (currently just .btn) adopted alongside it via
adoptedStyleSheets -- the native equivalent of a Sass @include, no
preprocessor added. Theme vars keep resolving through the shadow
boundary since CSS custom properties inherit across it; only plain
class rules needed the explicit move.
Deleted the global shared/src/modal.css entirely and dropped its
@import from both dashboard/common.css and agent/agent.css -- nothing
outside modal.js renders the old .tc-* classes any more. The <hive-
dialog> element is now the backdrop itself (:host carries the fixed-
position/centering rules that used to be .tc-backdrop on a light-DOM
div); box/title/message/content/actions all render inside its shadow
root. <hive-toast> similarly styles :host directly instead of a light-
DOM div, with the message text placed straight into the shadow root
(no <slot> needed since there's no external light-DOM content to
project).
Public API (openDialog/themedConfirm/themedPrompt/themedToast)
unchanged -- no call-site changes needed anywhere in dashboard/agent.
Verified with a full frontend build; nix fmt clean.
`hive_host_sock::jobs::State` was a hand-maintained copy of
`hive_jobq::State` — five variants spelled the same in both, kept in sync
by whoever remembered. Adding `Skipped` last week meant adding it twice.
The wire crate now re-exports the scheduler's enum and `to_wire_state` is
gone.
Two states that were hidden now reach clients. `to_wire_state` renamed
`Pending` to `Queued` and folded `Finishing` into `Running`, so the
dashboard could not distinguish a node waiting on its dependencies from
one whose own work is done while its sub-nodes still run. Both are now
visible, and consumers say which they mean.
Every consumer had to move with it, and only the Rust ones said so: the
exhaustive matches in `hivectl` and `DagView::rollup_state` failed to
compile, while the dashboard's fourteen string comparisons would have
gone quietly wrong — a `finishing` node no longer counting as running,
a `pending` node no longer as queued.
The frontend also builds CSS class names out of the state string
(`rqe-` + state, `rqe-node-` + state) and keys its glyph map on it, all
lowercase. Those go through a `stateSlug` helper now; comparisons use the
wire spelling, presentation lowercases. Without that split every queue
entry and node chip would have silently lost its styling.
Dropping the `State as JobState` alias in hive-c0re falls out of this:
the alias only existed to tell two `State` types apart, and there is one
now.
Closes hyperhive#2788.
State::Skipped now rides the wire (per hive-host-sock's dag_view, no
longer filtered) — this was the last missing piece: the frontend had
no glyph for it, so a skipped node's per-node chip fell through to
QUEUE_STATE_GLYPH's '?' fallback.
Added a 'skipped' entry (·, same quiet glyph hivectl already uses for
the same state — no contract between them, just consistent taste) and
a .rqe-node-skipped CSS rule (dimmed only, no strikethrough —
deliberately distinct from .rqe-node-cancelled: a skipped node wasn't
dropped mid-flight, it was never going to run, so it should read as
expected/quiet rather than alarming). rollupState's defensive arm
(every(n => skipped || done) => done) already landed in #2799 and
needed no changes here.
buildNodeTree has no state-based filtering, so skipped nodes render
in the tree exactly like any other node kind — no other changes
needed. Verified with a full frontend build; nix fmt clean.
A node ruled out by its own dependency edges settles `Skipped` host-side,
but the wire folded it into `Cancelled` and `dag_view` filtered it out
entirely, so a client never saw which branch a run didn't take. Post-#2785
that is not a rare shape: every approval DAG has two not-taken tails and
every rebuild has one, on the happy path as much as on failure.
`State` gains `Skipped`, and it counts as terminal — the wait loops in
hivectl's progress display and the daemon's dag-settled check decide
"finished" with `all(is_terminal)`, so omitting it would hang them on
essentially every DAG.
`dag_view` now emits skipped nodes but no longer lets them keep a DAG
alive. Serialization and completion were the same expression: a DAG left
the snapshot because its nodes had all been filtered away. Keeping skipped
nodes on the wire under that rule would pin every finished deploy in the
queue view forever, so the completion test is now its own flag.
`rollupState` in the dashboard gains the matching arm. It has no `done`
case — `done` is inferred by falling off the wire — so its trailing
`return 'queued'` catches anything it doesn't recognise, and a green
deploy would have read as permanently queued the moment the backend
started emitting the new state. The Rust and JS roll-ups have silently
disagreed before; they are edited together here and say so.
Pilot for the components-split proposal (mara wants a look at using
custom elements now that we're recent-Firefox-only). Picked the
themed dialog system as the first candidate: most self-contained of
our existing de-facto reusable components (transient, imperative call
sites, no external render-tree coupling), and shared between the
dashboard and per-agent UI already.
<hive-dialog> replaces the manually-built tc-backdrop/tc-box tree in
openDialog — connectedCallback renders, the keydown listener and
click-outside-to-dismiss are owned by the element instead of a
closure, and the outcome is reported via a hive-dialog-close
CustomEvent rather than a hand-rolled resolve callback threaded
through the DOM tree.
<hive-toast> replaces the toast div themedToast built inline —
connectedCallback starts the auto-dismiss timer,
disconnectedCallback clears it (previously a closure-captured
setTimeout handle with no explicit cleanup on early removal).
Both are light DOM (no shadow root) — styling stays exactly where it
already lived, in modal.css's .tc-* classes, imported globally by
both packages' base stylesheets. This was the deliberate call for a
first pilot: shadow DOM would need every shared stylesheet
re-imported per instance (CSS custom properties pierce shadow
boundaries for theming, but plain class rules like .btn don't), which
is real migration cost. Light DOM validates the pattern (lifecycle
encapsulation, less manual event bookkeeping) without paying that
cost; shadow DOM is a drop-in upgrade to these same two classes if a
later pilot wants real style encapsulation.
Public API unchanged (openDialog/themedConfirm/themedPrompt/
themedToast) — every existing call site across dashboard + agent
keeps working with no changes. Verified with a full frontend build.
The dashboard has a themed modal/dialog system (modal.js: themedToast/
themedConfirm/themedPrompt) and a data-async form submit interceptor
(bindAsyncForms) that every dashboard action routes through. The
per-agent UI never adopted either — it had its own more primitive
data-async handler using native window.confirm()/alert() (8 call
sites) and a duplicated el() DOM helper.
- Moved el() out of dashboard/common.js into shared/src/dom.js.
- Moved modal.js + modal.css from dashboard/src/ to shared/src/,
updating its internal el import.
- Moved bindAsyncForms from dashboard/common.js into shared/forms.js,
alongside the asyncBtn primitive it's built on.
- Updated every dashboard file's imports to the new shared locations
(no re-export shims).
- agent.css now @imports shared/modal.css so the dialogs render
themed there too.
- agent/app.js: dropped its local el()/data-async duplicate, wired
bindAsyncForms(), and replaced all 8 window.confirm() sites with
themedConfirm (async, wrapped in a fire-and-forget IIFE where the
call site needs a synchronous boolean return, e.g. the slash-command
dispatcher).
Closes hyperhive#2791. Verified with a full frontend build
(npm run build) — both dashboard and agent bundles compile clean and
agent.css picks up the .tc-* dialog styles it previously lacked.
asyncBtn() (shared/src/forms.js) is used by both the dashboard and the
per-agent UI, but its .spinner class + @keyframes spin animation only
lived in dashboard.css. The agent UI's loading spinner rendered as a
static unstyled glyph instead of the animated amber spinner the
dashboard gets. Moved the rule to shared/base.css, which both
common.css (dashboard) and agent.css already @import.
Closes#2720 (partial — kept state + infra card layout).
core.html doesn't load dashboard.css so the .container-row styles from
the operator SPA weren't available. Add equivalent card rules directly in
core.css: .containers flex column, .container-row with bg-elev background
+ border + border-radius, .tombstone dashed variant, .head flex row with
badge + meta, .actions flex row for the action buttons. Matches the visual
weight of agent cards on the main dashboard.
Closes#2720 (partial — schedules + call history fixes).
schedules:
- Agent-name column headers: switch from -45° CSS transform (which clipped
names mid-glyph) to writing-mode:vertical-rl + rotate(180deg). Names now
read bottom-to-top without truncation in their 28px column.
- Shrink 'next' column from 8em → 5.5em and 'every' from 7em → 5em;
these only hold short duration strings so the wider widths wasted space.
call history:
- Approval history <li> items now get a lightweight card treatment
(bg-elev background, 1px border, 3px left accent) matching the visual
weight of the pending .approval-card items above them.
- Left border colour reflects outcome via :has(.glyph-*): green for
approved, red for denied, amber for failed.
Closes#2720 (partial — stats tab placement + contrast).
The time-window nav was in <main> below the page title, so it controlled
hash routing but wasn't visually part of the page chrome. Move it into
<header> (same row as ← home / ST4TS), fill the remaining header space,
and right-align the buttons — consistent with the /logs.html tabbar pattern.
Also set explicit color/border on inactive buttons so they remain readable
on lower-contrast operator colour schemes (var(--subtext1) fallback to
var(--muted)).
Closes#2720 (partial — logs scroll fix).
Make body.logs-shell a full-viewport flex column so the visible .logs-pane
fills the remaining height and .journal-output (already flex:1 overflow:auto)
scrolls its content. Also handle the AUDIT tab's <div> output the same way.
The tab bar and toolbar stay anchored at the top; only the log text scrolls.
Adds CPU/memory cap columns and an inline edit form to the container-load
table in /core.html, backed by a new POST /api/resource-limits/{name}
dashboard endpoint.
## Backend (hive-c0re)
lifecycle_ops.rs — new post_resource_limits handler:
- Parses ResourceLimitsForm { cpu_quota, memory_max } (both optional; empty
string = clear override, fall back to hive-wide default).
- Validates each non-empty value via resource_limits::validate_cpu_quota /
validate_memory_max — returns 422 UNPROCESSABLE_ENTITY with a human-
readable message on invalid input so the dashboard can surface it inline.
- Calls meta::commit_resource_limits (staged git write under META_LOCK, same
as hivectl set-limits).
- Re-applies the drop-in immediately via lifecycle::write_dropins so the new
ceilings take effect on the next container start without waiting for a
rebuild.
- Triggers rescan_containers_and_emit so ContainerView.cpu_quota/memory_max
update via SSE without waiting for the next periodic sweep.
dashboard/mod.rs — registers the route:
POST /api/resource-limits/{name}
## Frontend (core.js + system-sections.css)
core.js:
- containersState derived from /api/state snapshot alongside tombstonesState
— supplies configured cpu_quota/memory_max to the LOAD table.
- lastLoadRows stash lets SSE-triggered re-renders call renderContainerLoad
without waiting for the next 5s poll.
- renderContainerLoad: adds cpu cap / mem cap columns (muted; tooltip
'configured ceiling — takes effect on next start') sourced from
ContainerView, plus a per-row S3T toggle button that expands an inline
edit form with cpu_quota / memory_max text inputs and a S4V3 button.
The edit form shows a restart hint, surfaces validation errors inline, and
collapses on success.
- container_state_changed SSE handler: updates containersState in place and
re-renders the LOAD table so the cap columns flip immediately after a save.
system-sections.css:
- CSS for the new cap columns (.cload-cap-th, .cload-cap) and inline edit
form (.cload-edit-row, .cload-edit-form, .cload-edit-label, etc.).
- Remove dead .rqe-step rule (step sub-step label retired from the wire in
'job_queue: retire the now-off-wire step sub-step label').
logs.html/logs.js previously showed infra containers (hive-ci, hive-forge,
hive-gateway, hive-matrix) in an optgroup within the AGENT tab selector.
This was confusing because infra containers don't run the per-agent hive
daemons, making the unit filter meaningless for them.
Changes:
- Add INFRA tab (between AGENT and SYSTEM) with its own container selector
and full-machine-journal fetch (no unit filter).
- Remove the infra optgroup from the AGENT tab — it now shows agents only.
- loadContainerLists() replaces loadAgentList(): fetches /api/state once and
populates both selectors, avoiding a duplicate network request.
- Deep-link (?agent=hive-ci) now routes to the INFRA tab when the named
container is an infra container, falling back to AGENT otherwise.
- Remove syncUnitSelectForSelection() — no longer needed since the AGENT
tab no longer contains infra containers.
- Extend the 30s timestamp ticker to cover the INFRA tab fetch time.
No backend changes: /api/journal/{name} already supports infra container names.
When ContainerView.paused is true, show a clickable yellow `⏸ paused`
badge on the agent card that POSTs to the new /api/resume/{name} endpoint
to un-park the turn loop. The badge doubles as the resume button so the
state is self-documenting and one click to fix.
The agent action menu gains ⏸ P4US3 (when not paused) and ▶ R3SUM3
(when paused), orthogonal to the running/stopped start/stop actions.
On the backend, /api/pause/{name} and /api/resume/{name} POST routes
wire to Coordinator::set_paused and trigger an immediate rescan so the
badge flips via the existing SSE ContainerUpdate without polling.
Depends on the ContainerView.paused field and Coordinator::set_paused
added in the parent PR.
Replace the Unicode prefix string approach (└─ / ├─ / │ built up as text
in a single <span class="rqe-tree-indent">) with positioned DOM elements
that draw real lines:
- rqe-tree-guide: fixed-width ancestor column, optionally draws a full
vertical border-left when the ancestor has siblings below it
(.rqe-tree-guide-line).
- rqe-tree-connector: draws the L/T shape via ::before (vertical stem,
top→center for last child, full height for mid child) and ::after
(horizontal spur, center→right). .rqe-tree-connector-last vs
.rqe-tree-connector-mid controls stem length.
renderTreeNode() now takes ancestorLines: boolean[] instead of a prefix
string. Each entry is true when the ancestor at that depth was not the
last child (so a vertical guide is still needed through that column).
childAncestorLines propagates depth === 0 correctly (root nodes have no
guide columns, so their children start with an empty array).
Lines are drawn with var(--border) so they follow the theme and work at
any font size without alignment drift. Addresses the review note on PR 2686.
Add NodeView::parent to the wire (hive-sh4re + hive-c0re dag_view), then
render the recursive parent/child tree in the dashboard build queue instead
of the previous flat chain/fan-out layout.
Wire change (hive-sh4re, hive-c0re):
- NodeView gains parent: Option<NodeId> (skip_serializing_if = None)
- dag_view() projects node.parent, filtering out the Dag container id
(top-level work nodes become parent: None on the wire)
Frontend (builds.js):
- Replace nodeComponents + splitFanOut with buildNodeTree (uses parent
edges directly) + topoSort helper (orders siblings by deps)
- renderTreeNode walks the tree depth-first, rendering indented rows
with └─/├─ connectors and agent label per chip
- Flat chains and fan-out heuristics are gone; structure comes straight
from the scheduler's parent axis
Closes: none (parent issue tracked in forge)
Removes entryAgents() and the two places it rendered agent names:
- rqe-agent code element in the entry header
- rqe-node-agent-label prefix per component chain when multi-component
The DAG structure split (WCC + fan-out) already communicates subgraph
boundaries visually via the separate .rqe-nodes rows; the agent-name
labels on top of that caused layout breakage (#2666) and duplicate
information. Closes#2666.
The live build log header still labels liveNode.agent (a single specific
node, not the whole DAG) — that .rqe-agent rule is kept.
Also removes the now-unused .rqe-node-agent-label CSS rule and its
comment.
hive-c0re's broker reminder store and /api/reminders endpoint were
removed in PR #2644 (reminders migrated to in-container sqlite store).
The dashboard had a QU3U3D R3M1ND3RS section on the schedules tab backed
by that endpoint, and a per-agent badge driven by pending_reminders
(always 0 after the migration).
Remove both. Per-agent reminders now surface through get_loose_ends /
the todos pill on the agent page, consistent with the todos migration.
- frontend/packages/dashboard/src/schedules.js: drop refreshReminders,
renderReminders, applyRemindersChanged; drop appendLinkified import
(no longer used); update module comment.
- frontend/packages/dashboard/src/tabs.js: drop applyRemindersChanged
and refreshReminders imports; remove reminders-section from managed
list; remove refreshReminders call site; drop reminders_changed from
SSE dispatch; simplify countdown ticker (reminder-due gone).
- frontend/packages/dashboard/src/dashboard.html: remove QU3U3D
R3M1ND3RS section.
- frontend/packages/dashboard/src/dashboard.css: remove reminder list +
row CSS.
- frontend/packages/dashboard/src/common.css: remove .badge-reminder.
- frontend/packages/dashboard/src/swarm.js: remove pending_reminders
from fingerprint key and badge render.
Both old endpoints removed. refreshLooseEnds() call sites cleaned up;
lastLooseEnds stays as empty [] for reconcileAskBinds (no-op now that
the loose-ends source is gone).
- asyncBtn now returns fn().finally(...) so callers can await/chain it
- Move re-fetch calls inside try/catch in core.js and permissions.js so
network errors from fetchAndRenderStalePerms / fetchAndRender* are
caught instead of escaping as unhandled rejections
- clearStaleAgent returns the asyncBtn promise so the function is
properly awaitable when a button is present
- Update asyncBtn doc comment to reflect the return-value contract
Wrap the fetch() call in a try/catch so network errors (offline, DNS
failure, CORS) surface via themedToast instead of becoming unhandled
promise rejections. The asyncBtn finally() still restores the button
either way — the catch just adds the missing operator feedback.
add `asyncBtn(btn, fn)` to `@hive/shared/forms.js` as the single
reusable component for async button actions:
1. double-click guard: returns immediately if btn is already disabled
2. saves btn.innerHTML, replaces with spinner while in-flight
3. restores btn on resolve or reject via finally
wire it into all ad-hoc disable/spinner/restore patterns:
- common.js: bindAsyncForms uses asyncBtn internally
- core.js: 'clear perms' button
- permissions.js: clearStaleAgent
- schedules.js: saveSchedule submit, editSchedule submit
- app.js: buildAnswerForm, buildInboxMarkAllRow
fireScheduleNow in schedules.js is left with its existing childNode
save/restore because it shows a custom result flash on the button
content after a successful fire-now (the auto-restore of asyncBtn
would overwrite it); the surrounding themedConfirm dialog already
acts as a natural double-click barrier before the fetch.
saveAll in permissions.js is also left as-is: it uses a custom
'queued ✓' success label + a 900ms delay before re-fetch; the
btn.dataset.busy flag is its own double-submit guard.
The Reminder loose-end variant's due_at field is serialized as an ISO 8601
string (DateTime<Utc> on the wire), but the JS was doing:
const dueIn = (t.due_at || 0) - now;
A string minus a number is NaN in JS, so fmtAge(NaN) returned 'NaNd',
producing the 'due NaNd overdue' label seen in the screenshot.
Fix: parse the ISO string to unix seconds with new Date(...).getTime() / 1000
before the subtraction.
Adds a 'sessions' chip to the per-agent stats summary panel showing how
many fresh claude sessions started within the selected time window.
Backend (hive-agent/src/stats.rs):
- New optional field `session_count: Option<u64>` on `Snapshot`
(skip_serializing_if = None — inert-until-data, same pattern as
first_turn_ctx). Counts rows in the `sessions` table whose
started_at falls within the window; returns None when the table
doesn't exist on an older db.
- New `read_session_count(conn, from)` helper (rusqlite::Result so the
caller maps Err to None).
- Extracted per-row accumulation loop into `TurnAccum` struct +
`push()` method to keep `snapshot()` under the too_many_lines limit.
Frontend (frontend/packages/agent/src/stats.js):
- New 'sessions' chip added to renderSummary, guarded by
`typeof s.session_count === 'number'`, placed before the
existing first-turn-ctx chip.
The three message-bearing hyperhive tools (send, ask, answer) previously
had named JS branches in renderRichToolUse that each:
- computed a summary string (recipient / line count)
- rendered the body text via detailsOpenMd (marked + DOMPurify)
This commit moves the body text and summary string to the backend,
reducing the JS dispatch table to a single generic markdown path.
Backend (stream_enrich.rs):
- rich_tool_body: new 'markdown' body_type for send/ask/answer —
stamps _body with input.body / input.question / input.answer
- fmt_hyperhive_message_tool: new helper formats _summary as
'send* → to' / 'ask* → to' / 'answer* #id' with ' · NL' when
the body spans multiple lines; extracted out of fmt_hyperhive_tool
to keep it under the too_many_lines limit
- doc: updated rich_tool_body docstring to list the new 'markdown' type
Frontend (app.js):
- Remove the three named branches (send/ask/answer) from renderRichToolUse
- Extend the generic _body path: 'markdown' type calls detailsOpenMd
- The ask-form slot logic (operator inline-answer binding) is preserved
within the markdown branch, now reading the question from c._body
instead of input.question — DOM mounting remains client-side
- fmt_builtin_tool: Edit gets its own arm computing `-N +M` line counts
in _summary (was shared with Read/Write as bare file-path).
- is_rich_tool: drop Write (content is huge/one-sided; flat _summary row
is correct); Edit stays rich since it has an old/new diff.
- rich_tool_body: now returns Option<(String, &'static str)> where the
second field is the body type ('diff' or 'plain'). Edit arm builds the
'-'/'+ ' prefixed diff body; mcp__bash__run gets type 'plain'.
- enrich_tool_use_entry: stamps both _body and _body_type.
Frontend (app.js):
- Remove the Write/Edit branch from renderRichToolUse (~25 lines).
- Generic _body path now dispatches on _body_type: 'diff' ->
api.detailsDiff (colour-coded spans), default -> api.details.
No tool-specific JS remains for file diff rendering.
Backend now stamps `_body` (full `$ <cmd>`) alongside `_summary` (first
line) and `_category: "rich"` for mcp__bash__run tool_use entries. The
frontend drops the mcp__bash__run-specific branch in renderRichToolUse
and uses a generic `if (c._body)` path instead — api.details() with the
backend-computed summary and body, no JS knowledge of the tool name.
Phase 2 of hyperhive#2196. The backend now pre-computes enrichment fields
on every SSE event (stream_enrich.rs); the frontend reads them directly
instead of running its own dispatch logic.
Removed (~330 lines of JS):
- fmtArgsGeneric / TOOL_ICONS / toolIcon / fmtRoom / fmtUser / fmtToolUse
renderStream changes:
- system events: dispatch on v._category (drop/thinking_tok/note/details)
+ v._summary / v._body instead of per-subtype if-chains; status tick
still overrides label client-side when stateName === 'compacting' since
elapsed time is a wall-clock value the backend cannot know at emit time
- tool_use: use c._category === 'rich' for rich-renderer routing,
c._icon / c._summary for flat rows
renderRichToolUse: toolIcon(name) -> c._icon (from backend enrichment)
stream_enrich.rs: also stamp _category: 'drop' on top-level
type=result / type=rate_limit_event so the frontend can use a single
_category check instead of separate type-based early returns
During a compaction pass, bare `status` ticks from claude are the only
live signal (no dedicated progress event exists in the headless stream-json).
Previously these rendered as a coalescing `⚙ status` row — uninformative
while waiting for a long compact to finish.
The harness emits `TurnStateChanged { state: Compacting, since_unix }`
immediately before invoking `session.compact()` for both the turn-end and
idle-session manual-compact paths. The frontend already tracks this in
`stateName` / `stateSince` for the state badge (`📦 compacting · Xs`);
now the terminal's `status` tick renderer reads the same two variables so
consecutive ticks show `⚙ compact · Xs…` (elapsed, in-place updating via
the existing coalescer) instead of the unhelpful `⚙ status`.
Result: what used to look like
⚙ status
⚙ compact · manual · 37k→2k tokens · 40.1s
now looks like
⚙ compact · 3s… (updating in place)
⚙ compact · manual · 37k→2k tokens · 40.1s
Auto-compact (triggered by hive-claude's InfiniteSession policy internally,
no TurnState::Compacting set by the harness) continues to show `⚙ status`
as before.
Addresses #2276 (follow-up to status-tick coalescing in #2335).
Extends GET /api/journal/{name} to also accept the four hive infra
container names (hive-ci, hive-forge, hive-gateway, hive-matrix —
hive_priv_sock::InfraContainer is the allowlist), reusing the same
journalctl -M / hive-priv delegation path already used for agent
containers. Infra containers don't run the per-agent hive daemons, so
the unit filter is skipped for them — always the full machine journal.
Frontend: the AGENT tab's agent selector now lists infra containers
in a separate optgroup (sourced from /api/state's existing
infra_containers field), and disables the unit-filter select when one
is chosen.
inFlightOpsByAgent() read e.agent, a DAG-level field that no longer
exists (agent moved to per-node when DAGs became agent-per-node). So
the SW4RM tab's 'building...'/'meta-updating...' badges never matched
any real agent, and multi-agent DAGs (a startup sweep's MetaLock
cascade, a hive-wide restart) showed nothing at all on the per-agent
cards even while the rebuild queue clearly had them in flight.
Derive each agent's in-flight kind+state from its own node(s) within
the entry instead: a DAG can be 'running' overall while a given
agent's subgraph hasn't started (still queued behind an earlier node
in its chain), so per-node state is also more accurate than the old
per-DAG state for the badge, not just more available.
nodeComponents() split a DAG into weakly-connected components via deps
edges, but post #2476/#2450 every agent's rebuild subgraph hangs off a
shared MetaLock node via AfterOk, so the whole meta-update cascade is one
connected component and rendered as a single wall-of-chips line.
Add a second pass (splitFanOut) that further splits a component's
topo-ordered nodes on out-degree>1 points: a node with more than one
direct dependent renders as its own one-node line, and each dependent
becomes the root of an independent line. Purely deps-structure-driven,
same as the existing WCC split - no agent-field grouping involved. A
component with no fan-out (the common single-agent case) is unaffected.
Since the boot sweep (#2450) and meta-update cascade (#2476) became
single DAGs that grow subgraphs in-place, nothing constructs the old
fan-out anchors/parents anymore:
- NodeKind::Noop (the old boot_root grouping anchor) — no constructors.
- Template::StartupSweep / Source::StartupSweep (the old fan-out parent
template + cascade-child source) — replaced by Template::Boot and
Source::AutoUpdate/MetaUpdate respectively.
Drops the three variants + their as_str arms + the Noop executor arm, and
refreshes the stale fan-out/anchor doc comments (Boot/MetaUpdate/Source
docs, coordinator.md, dashboard.md). Frontend: the queue-kind glyph moves
from the dead startup_sweep to boot (which had none), and the dead
rqe-source-startup_sweep style is dropped.
No behaviour change — pure dead-variant removal.
Dashboard AGENT log tab sent unit=hive-ag3nt.service (the crate/dir
name) but the actual systemd unit is hive-agent.service, so every
fetch 400'd against the backend allow-list. Fixed the dropdown value
and, per the same issue's request, added the other per-agent daemons
(hive-mcp-http, hive-bash-daemon, hive-matrix-daemon) as selectable
units, plus hive-priv.service on the SYSTEM tab's host-daemon list.
Extended both backend allow-lists (post_journal / post_journal_host)
to match, and fixed a stale systemd.services.hive-ag3nt doc reference
in agent-hierarchy.md that had the same crate/unit-name confusion.
With the meta-update cascade (#2476) and startup sweep (#2450) folded
into single DAGs that grow per-agent subgraphs via append_subgraph,
nothing links parent/child DAGs anymore — parent_id is dead.
hive-c0re: drop parent_id from Dag/DagSpec (+ the DagView copy); delete
append_children and cancel_children (no callers); simplify trim_history
(no more terminal-parent-with-live-children guard — a one-big-DAG is
terminal only when its whole graph settles); drop the rebuild() parent_id
param; QueueDag returns just the polled DAG (no fan-out children to
gather). hive-sh4re: drop the DagView.parent_id wire field.
frontend: a multi-step op is one DAG now, so renderRebuildQueue drops the
childrenOf/orphans cross-DAG grouping and renders each entry flat; its
per-agent subgraphs render as nodes within the one row (split by deps).
Removed the dead rqe-child style + isChild plumbing.
Docs + the child-DAG queue tests updated/removed to match.
mara's review comment: the frontend shouldn't apply its own grouping
logic on top of the DAG — it should render the structure the backend
already provides. The actual structure is the nodes' deps graph, not
the incidental n.agent field.
Replace the group-by-agent heuristic with nodeComponents(): splits
entry.nodes into weakly-connected components via the deps edges
(undirected reachability), then topo-sorts each component (Kahn's
algorithm) so a chain renders in true dependency order. A DAG made of
independent per-agent subgraphs (no cross-agent deps) still comes back
as separate components — same visual result for today's templates —
but the split is now driven by what the backend actually encodes, and
naturally extends to any future non-agent-aligned branching. Agent
name is still shown as a per-component label, but purely as adjunct
info sourced from that component's own nodes, not the grouping key.
renderQueueEntry flattened entry.nodes into one arrow-joined chain
regardless of which agent each node belongs to. A multi-agent DAG
(e.g. hivectl restart --graceful with several agents) runs independent
per-agent subgraphs concurrently, no cross-agent deps, so joining them
all into one sequential-looking chain misrepresented the actual DAG
shape (mara's report: 'expected one dag that forks after the start
node into the per agent sub dags').
Group nodes by n.agent (stable, first-seen order) and render one
.rqe-nodes line per agent, with a small agent-label chip when the DAG
spans more than one. Single-agent DAGs (the common case) collapse back
to exactly the prior one-line render — no visible change there.
Restart now offers the same graceful-vs-hard choice stop already has:
single-agent menu item and the bulk-select action bar both grow a
'restart gracefully' checkbox that routes through the existing
submit::graceful_restart DAG (signal -> drain -> stop -> reconcile)
instead of a hard restart. Backend gains a ?graceful=true query param
on POST /api/restart/<name>, mirroring post_kill's shape (renamed
KillParams -> GracefulParams since it's now shared).
- builds.js NODE_KIND_LABEL: 'set wanted' (and the previously-missing
'noop') so the new head node renders with a label, not the raw kind.
- docs/coordinator.md + templates.rs module doc: the power-op DAG shapes
now show the head SetWanted node; SetWanted added to the lease-needing
list with the atomicity rationale.
Review (iris/argus): the dashboard rebuild live-log header labels one
specific node's log stream (liveNode, keyed by its build_log_id), so it
should show that node's own agent — entryAgents(running) listed every
agent in the DAG, which would mislabel a single agent's log once DAGs
span multiple. The other entryAgents() sites (row label, cancel-confirm,
fingerprint) are correct whole-DAG summaries and unchanged.
DagView no longer has a DAG-level agent, so consumers derive it from the
per-node agents:
- hivectl dag_progress.rs: dag_agents(d) helper (distinct node agents,
comma-joined) in place of d.agent.
- dashboard builds.js: entryAgents(entry) helper likewise for the
rebuild-queue card + live-log header + cancel confirms.
- docs/coordinator.md: lease prose (node-agent-keyed, global per agent),
wire shape (NodeView.agent, no DagView.agent), and dropped the removed
dedup section.