diff --git a/docs/web-ui/dashboard.md b/docs/web-ui/dashboard.md index 075401cd..b9a2c4a5 100644 --- a/docs/web-ui/dashboard.md +++ b/docs/web-ui/dashboard.md @@ -209,12 +209,8 @@ Three sub-tabs: **R3BU1LD QU3U3** (default), **M3T4 1NPUTS**, **BUILD L0GS**. Its own esbuild bundle (`builds.js`); cold-loads `/api/state` and subscribes to `/api/dashboard/stream` for `rebuild_queue_changed`, `meta_inputs_changed`, `meta_update_running`. -The SW4RM tab (`/dashboard.html`) does **not** read this endpoint (or -any job-queue state) at all — mara, on review: "swarm.js should not -need to pull in the jobq to do its job." Its per-agent pending badges -are transient-only, and its queue-summary banner is removed for now -(see Container row, below), pending a dedicated rollup endpoint it -can read directly instead of the full graph. +The dashboard tab keeps the rebuild-queue *state* for the SW4RM +card badges without rendering these panels. **R3BU1LD QU3U3** — pending, in-flight, and recently-settled container operations: rebuilds, meta-update cascades, and first-spawns. One @@ -226,11 +222,9 @@ listens for its `hive-jobq-graph-update` event to drive the two things below it that the generic view doesn't show. The component owns fetching, cold and live: `GET /api/jobq/graph` on mount, and `.refresh()` on every `rebuild_queue_changed` SSE tick (that event -still carries its own `Vec` payload — `DagView`-shaped, -also read by `hivectl`'s own wait/progress loop, a separate migration -— on the wire, but neither dashboard page reads it anymore; both -treat the tick as a pure refetch trigger against the generic -endpoint). +still carries its own `Vec` payload on the wire — the +SW4RM tab still consumes it for the badges, untouched — this page just +ignores the payload and treats the tick as a refetch trigger). Each row is one root graph node (`parent: null`); a multi-step op's per-agent subgraphs and sub-steps render as nodes within that one @@ -885,19 +879,22 @@ fetch entirely. independent badge rather than being collapsed into one label, matching the existing multi-badge convention this line already uses for `paused`/`needs_update`/model/ctx. - Any pending badge means the row is actually **running** something - right now — there is no separate queued-but-not-started row state - to visually distinguish it from (see **Pending-badge derivation** - below), so every row carrying ≥1 badge keeps the amber row tint AND - draws a **rotating amber ring** around the agent icon. + The row visual splits queued vs running: a **queued** entry (no + transient yet, see below) shows only the pending-state pill (no row + tint, so a long queue doesn't paint half the tab amber); a + **running** entry (at least one transient) keeps the amber row tint + AND draws a **rotating amber ring** around the agent icon, so it's + obvious at a glance which container is actually moving. - **Pending-badge derivation:** transients only (`transientsState`, - keyed `agent -> Map`) — a transient is **derived - from a job-queue node currently `Running`** against that agent, not - declared per request, so its label follows the operation as it - progresses (a rebuild reads `stop_for_update`, then `swap`, then - `reconcile` rather than one constant `rebuilding` for its whole - life). Two consequences for anything rendering it: + **Pending-badge derivation:** two separate stores, but no longer a + priority *order* between them — the second only ever applies when + the first has nothing to say. (1) **Transients** + (`transientsState`, keyed `agent -> Map`) — a + transient is **derived from a job-queue node currently `Running`** + against that agent, not declared per request, so its label follows + the operation as it progresses (a rebuild reads `stop_for_update`, + then `swap`, then `reconcile` rather than one constant `rebuilding` + for its whole life). Two consequences for anything rendering it: - The label vocabulary is **open** — it is the node's own wire tag (`NodeKind::as_str`, the same strings `NodeView.kind` carries), @@ -915,16 +912,20 @@ fetch entirely. their own label directly via `TransientSet`/`TransientCleared` events carrying no backing node at all. - **Queued (not-yet-started) work shows nothing on the card, and - `swarm.js` reads no job-queue state at all.** An earlier version of - this page had a job-queue-backed fallback badge for the `Pending` - case (`queuedOpsByAgent()`, reading a `GET /api/jobq/graph` fetch); - removed per mara, on review of hyperhive#3028's PR: *"swarm.js - should not need to pull in the jobq to do its job,"* followed by - *"remove the per agent pending stuff - only show what is running."* - The queue-summary banner below was, for the same reason, also pulled - rather than kept on that same client-side graph-fetch — see its own - entry below for why. + (2) The **rebuild-queue fallback** (`rebuildQueueState`) only + fires when an agent has **zero** transients — since (1) now covers + every `Running` node unconditionally, a `Running` rebuild-queue + entry can never usefully reach this fallback by the time it's + consulted; the fallback exists purely for the **`Pending` + (queued, not yet started)** case, which `running_transients()`'s + Running-only test cannot represent. `queuedOpsByAgent()` (swarm.js) + reflects this: it only ever looks at `Pending`-state queue nodes, + and (mara, on review) the badge shows the queue entry's raw `kind` + string as-is — no English-phrase lookup translating it first, same + opaque-string treatment a transient's own label already gets. + `opRunning` (driving the `pending-running` row class + spinner) is + simply "does this agent have at least one transient" — a queued-only + entry (no transient yet) leaves it false. An **active model badge** (`model · `, blue) appears when the container is running and the harness has persisted a model name (`harness/hyperhive-model`). Read by hive-c0re's `ContainerView` @@ -965,23 +966,13 @@ per-agent actions and navigation links. Contents: agent is stale. Banner pulses on each broker SSE event (`pulseBanner` with a 4s grace timer). -**Build-queue summary banner — currently removed, coming back on a -different data source.** Previously a compact amber banner above the -container list (`◐ build queue — N running · M queued — view -queue →`, linking to the BU1LDS page), derived client-side from `GET -/api/jobq/graph` (filter to `parent: null` group roots — each an -independent operation, not a raw node count — bucket by `state`). -Pulled per mara, on review of hyperhive#3028's PR: *"dont replace one -legacy thing with another. then we will have to either wait with -this pr or split it into what can and cannot be done now"* — a -client-side derivation over the generic graph was itself judged a -stopgap not worth landing, the same way the old `DagView` read it -replaced was. Comes back once a dedicated rollup endpoint (status × -count pairs, tracked separately) exists for `swarm.js` to read -directly instead of pulling the whole graph in for a summary. Until -then, the actual running work per agent is still visible on each -card via the transient pills; only the hive-wide at-a-glance count is -missing. +**Build-queue summary banner** — when the rebuild queue has any +`queued` / `running` entries, a compact amber banner sits above the +container list: `◐ build queue — N running · M queued — view queue →` +(the link goes to the BU1LDS page's R3BU1LD QU3U3). It replaces the +old per-transient spinner list; the actual running node for each +agent is already shown on its card (transient + in-flight-queue +badges), so the top of the tab only needs the at-a-glance summary. ### Themed dialogs diff --git a/frontend/packages/dashboard/src/dashboard.css b/frontend/packages/dashboard/src/dashboard.css index 843b8b42..8615b003 100644 --- a/frontend/packages/dashboard/src/dashboard.css +++ b/frontend/packages/dashboard/src/dashboard.css @@ -347,11 +347,8 @@ hive-agent-menu { .container-row:hover hive-agent-menu { --menu-btn-opacity: 1; } -/* Card actions dim while a row has any pending-state badge (transient- - driven only now — see swarm.js's pending-badge derivation comment; - there is no separate queued-but-not-running row state to tell apart - from this one anymore). */ -.container-row.pending-running .actions { opacity: 0.4; pointer-events: none; } +/* Pending state splits queued vs running. */ +.container-row.pending .actions { opacity: 0.4; pointer-events: none; } .container-row.pending-running { border-color: var(--amber); background: color-mix(in srgb, var(--amber) 5%, transparent); @@ -458,11 +455,7 @@ hive-agent-menu { /* Build-queue summary banner on the SW4RM tab: one compact line when the rebuild queue has active work, with a link to the full queue on - the C0R3 page. Amber to match the in-progress / "rebuilding" card tint. - Currently unused — swarm.js dropped the banner pending a dedicated - rollup endpoint (see swarm.js's transients-section comment) — kept - here rather than deleted-then-restored, since the markup/classes are - expected to come back unchanged once that endpoint exists. */ + the C0R3 page. Amber to match the in-progress / "rebuilding" card tint. */ .queue-summary { display: flex; align-items: center; diff --git a/frontend/packages/dashboard/src/swarm.js b/frontend/packages/dashboard/src/swarm.js index c86a358c..beff26ab 100644 --- a/frontend/packages/dashboard/src/swarm.js +++ b/frontend/packages/dashboard/src/swarm.js @@ -1,7 +1,7 @@ // SW4RM (containers) domain — extracted from tabs.js. // Agent topology, container-row rendering, selection bar, peer-hives block, -// and all live-update apply handlers for container-state and transient ops. -// See docs/web-ui.md::Container row for the rendering contract. +// and all live-update apply handlers for container-state, rebuild-queue, and +// transient ops. See docs/web-ui.md::Container row for the rendering contract. import { $, form, fmtAgeSecs, @@ -27,6 +27,8 @@ const CTX_CAUTION_TOKENS = 100_000; // fallback yellow threshold (~= 50% of 200k // ─── module-level state ───────────────────────────────────────────────────── +let rebuildQueueState = []; + // Keyed container row cache. Maps agent name -> { el:
  • , fingerprint }. // Allows renderContainers to skip rebuilding rows whose displayed state // hasn't changed — prevents full-wipe flicker + avoids redundant async @@ -50,20 +52,62 @@ const transientsState = new Map(); // tab-gated visibility). const selectionState = new Set(); -// ─── transients ───────────────────────────────────────────────────────────── +// ─── rebuild queue ────────────────────────────────────────────────────────── + +export function syncRebuildQueueFromSnapshot(s) { + rebuildQueueState = (s.rebuild_queue || []).slice(); +} +export function applyRebuildQueueChanged(ev) { + rebuildQueueState = (ev.queue || []).slice(); + // Re-render the SW4RM tab so newly-queued ops light up the right + // card with a " queued…" badge, and entries that drop out of + // Pending fall back to whatever transientsState (or nothing) says + // instead. Running work is *not* driven by this event — that's + // transient_set/transient_cleared's job, since every running node + // naming an agent already lights a pill by the time it gets here. + // See docs/web-ui.md::Container row for the badge taxonomy. + renderContainersFromState(); +} +// Map from agent name -> the queued op's raw `kind` string, backing +// the SW4RM card's fallback pending badge. Pending only — every +// *running* node naming an agent already lights a transient pill (any +// running node, not just a curated "worth it" subset — see +// docs/web-ui.md::Container row), so a Running entry here would +// always be redundant with `transientsState` by the time this is +// consulted. Queued (not yet started) work is the one state +// transients can't represent, since `running_transients()` on the +// backend is a Running-only test. // -// This is now the ONLY job-queue-derived state on this page — mara, on -// review of the DagView migration: "swarm.js should not need to pull in -// the jobq to do its job." An earlier version of this file also fetched -// GET /api/jobq/graph directly for two things now gone: a per-agent -// queued-but-not-running badge (`queuedOpsByAgent()`, removed per "remove -// the per agent pending stuff - only show what is running" — a card's -// pending badges are transients-only now, which already means "what is -// running") and the queue-summary banner (removed per "dont replace one -// legacy thing with another" — a client-side derivation over the generic -// graph was itself judged a stopgap not worth shipping; the banner comes -// back once the dedicated rollup endpoint exists, and reads that -// directly instead of pulling the graph in here at all). +// The returned `kind` is displayed as-is (mara, on review: "drop +// queuedLabelFor - just show what the backend sends") — same opaque- +// string treatment `pending`'s transient half already gets, no +// English-phrase lookup table translating it first. +// +// Agent is per-node, not per-DAG (a DAG can span agents — e.g. the +// startup sweep's MetaLock cascade, or a hive-wide restart), so this +// derives each agent's queued state from its own node(s) within the +// entry rather than the DAG's overall `state`/`kind`. +function queuedOpsByAgent() { + const out = new Map(); + for (const e of rebuildQueueState) { + if (e.state !== 'Pending') continue; + // spawn ops target an agent that doesn't exist yet as a + // container — the transient store already drives the + // pending row for that case. Skip here to avoid double- + // surfacing if the spawn op happens to land in the queue + // while the row exists transiently. + if (e.kind === 'spawn') continue; + for (const n of e.nodes || []) { + if (!n.agent || n.state !== 'Pending') continue; + // First entry found wins — with only one state to consider + // (Pending), there's no priority to resolve between DAGs. + if (!out.has(n.agent)) out.set(n.agent, e.kind); + } + } + return out; +} + +// ─── transients ───────────────────────────────────────────────────────────── export function syncTransientsFromSnapshot(s) { transientsState.clear(); @@ -300,14 +344,9 @@ function buildContainerLi(c, node, opts) { askerCount, targetCount, agentQCount, url, containerBase, forgeBase, s, } = opts; - // A single `pending-running` class now covers the whole "has at - // least one badge" state — there's no more queued-but-not-running - // row to distinguish it from (see the pending-badge derivation - // comment in renderContainers), so the separate no-tint `pending` - // class from before that removal is gone rather than kept as a - // class that would now always co-occur with this one. const li = el('li', { class: 'container-row' + + (pending.length ? ' pending' : '') + (opRunning ? ' pending-running' : '') + (selected ? ' selected' : ''), }); @@ -583,15 +622,27 @@ export function renderContainers(s) { )); } - // No queue-summary banner for now — the previous version derived it - // client-side from GET /api/jobq/graph, which mara flagged as - // replacing one legacy DagView-shaped hack with another rather than - // landing the real fix ("dont replace one legacy thing with another - // ... split it into what can and cannot be done now"). Comes back once - // the dedicated rollup endpoint exists, reading that directly. Until - // then the queue's actual state is still visible per-agent on each - // card via the transient pills; only the hive-wide at-a-glance summary - // is missing. + // Queue-summary banner: when the rebuild queue has active work, + // show one compact at-a-glance line + a link to the full queue on the + // C0R3 page. Replaces the old per-transient spinner list — the actual + // running step is already visible per-agent on each card (transient + + // in-flight-queue badges), so the top of the tab only needs the summary. + const activeQueue = rebuildQueueState.filter( + (e) => e.state === 'Pending' || e.state === 'Running', + ); + if (activeQueue.length) { + const running = activeQueue.filter((e) => e.state === 'Running').length; + const queued = activeQueue.length - running; + const parts = []; + if (running) parts.push(`${running} running`); + if (queued) parts.push(`${queued} queued`); + root.append(el('div', { class: 'queue-summary' }, + el('span', { class: 'glyph spinner' }, '◐'), ' ', + el('strong', {}, 'build queue'), ' — ', + parts.join(' · '), ' ', + el('a', { class: 'queue-summary-link', href: '/builds.html' }, 'view queue →'), + )); + } if (!containers.length && !transientsState.size) { root.append(el('p', { class: 'empty' }, 'no managed containers')); @@ -621,6 +672,10 @@ export function renderContainers(s) { const forgeBase = (s && s.forge_public_url) || null; const ul = existingUl ?? el('ul', { class: 'containers' }); const tree = buildAgentTree(containers); + // Queued (not-yet-running) ops per agent name — see + // docs/web-ui.md::Container row for why this only covers the + // Pending case now (Running is fully covered by transientsState). + const queuedOps = queuedOpsByAgent(); // Build the ordered list of
  • elements, reusing cached rows // whose displayed state hasn't changed. @@ -634,17 +689,20 @@ export function renderContainers(s) { const containerBase = gatewayLinks ? `/agent/${encodeURIComponent(c.name)}` : `http://${hostname}:${c.port}`; - // Pending-badge derivation: transients only — "what is running," - // full stop (mara, on review: "remove the per agent pending stuff - - // only show what is running"; rendered one badge per pill — mara, - // earlier: "show all running nodes that name the agent"). See - // docs/web-ui.md::Container row. + // Pending-badge derivation: an agent's transients win outright when + // any exist (rendered one badge per pill — mara: "show all running + // nodes that name the agent"), the rebuild-queue's queued-only + // fallback otherwise. See docs/web-ui.md::Container row. const transientKindsMap = transientsState.get(c.name); // Sorted for stable badge order across renders — Map iteration // order is insertion order, which shifts as pills clear/re-add. - const pending = transientKindsMap + const transientKinds = transientKindsMap ? Array.from(transientKindsMap.keys()).sort() : []; - const opRunning = pending.length > 0; + const queuedKind = transientKinds.length === 0 ? queuedOps.get(c.name) : null; + const pending = transientKinds.length > 0 + ? transientKinds + : (queuedKind ? [queuedKind] : []); + const opRunning = transientKinds.length > 0; const selected = selectionState.has(c.name); // Pending questions where this agent is the asker (awaiting an // answer) or the target (owes a reply). Derived live from diff --git a/frontend/packages/dashboard/src/tabs.js b/frontend/packages/dashboard/src/tabs.js index 40dda34e..ab079b78 100644 --- a/frontend/packages/dashboard/src/tabs.js +++ b/frontend/packages/dashboard/src/tabs.js @@ -42,8 +42,8 @@ import { renderQuestions, activeQuestionCount, } from './call.js'; import { - syncTransientsFromSnapshot, - applyContainerStateChanged, applyContainerRemoved, + syncRebuildQueueFromSnapshot, syncTransientsFromSnapshot, + applyRebuildQueueChanged, applyContainerStateChanged, applyContainerRemoved, applyTransientSet, applyTransientCleared, renderContainers, renderContainersFromState, renderSelectionBar, renderPeerHives, @@ -234,6 +234,9 @@ window.marked = marked; // `transientsState` + `containersState`, not from `s.*`). syncTransientsFromSnapshot(s); syncContainersFromSnapshot(s); + // Rebuild-queue state feeds the SW4RM agent-card badges + // (inFlightOpsByAgent); its own panel now lives on /core.html. + syncRebuildQueueFromSnapshot(s); renderContainers(s); // Sync the derived approvals + questions stores from the // snapshot, then render. Live `*_added` / `*_resolved` events @@ -304,10 +307,8 @@ window.marked = marked; container_removed: applyContainerRemoved, // tombstones_changed / meta_inputs_changed / meta_update_running are // handled on /core.html now (the SYST3M panels moved there). - // rebuild_queue_changed: this page no longer subscribes to it at - // all (see swarm.js's transients-section comment) — the SW4RM tab - // has nothing left that reads it, unlike /builds.html which still - // does (its own separate subscription). + // rebuild_queue_changed stays: it refreshes the SW4RM badges. + rebuild_queue_changed: applyRebuildQueueChanged, schedules_changed: applySchedulesChanged, capabilities_changed: applyCapabilitiesChanged, tool_groups_changed: applyToolGroupsChanged, @@ -329,7 +330,7 @@ window.marked = marked; const es = openStream( '/api/dashboard/stream?kinds=sent,approval_added,approval_resolved,' + 'question_added,question_resolved,transient_set,transient_cleared,' + - 'container_state_changed,container_removed,' + + 'container_state_changed,container_removed,rebuild_queue_changed,' + 'schedules_changed,capabilities_changed,tool_groups_changed', ); es.onmessage = (e) => {