From 45710ab739d0ab89ad149d9cc12c15b9c778f61c Mon Sep 17 00:00:00 2001 From: iris Date: Mon, 3 Aug 2026 19:07:41 +0200 Subject: [PATCH 1/4] swarm.js: migrate pending-row fallback + queue-summary banner off DagView MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit hyperhive#2822/PR#3026 moved swarm.js's per-agent in-flight status off the rebuild queue. Two other reads of the same rebuild_queue field survived that PR by design (a different feature, atlas flagged it on #2985) and are the last DagView/NodeView consumers on the frontend: queuedOpsByAgent()'s pending-row fallback and the SW4RM queue-summary banner. Both now read GET /api/jobq/graph (hive-jobq-wire's generic GraphNode shape) instead, matching the pattern builds.js already established for . Along the way: DagView no longer carries state/kind fields (removed in an earlier refactor that pushed roll-up derivation client-side), so both migrated functions were silently reading undefined fields and had become permanent no-ops — the pending-badge fallback never lit and the queue-summary banner never rendered. This restores real behavior rather than porting broken logic forward. The queue-summary banner's node-count-vs-group-count question (flagged on hyperhive#3028 as needing a decision) resolves cleanly: a GraphNode group root (parent: null) is an ordinary node whose own state already IS the group's roll-up per hive-jobq-wire's contract, so counting roots by state is a direct filter, not a parent-chain walk or a client-side rollup calculation. Verified the derivation logic against constructed GraphNode fixtures (multi-step chains, settled history that must not count, Finishing roots, multi-agent single-DAG groups) before wiring it in — 13/13 checks passed. docs/web-ui/dashboard.md's Container-row + BU1LDS sections updated to match. --- docs/web-ui/dashboard.md | 60 +++++++----- frontend/packages/dashboard/src/swarm.js | 118 ++++++++++++++--------- frontend/packages/dashboard/src/tabs.js | 13 ++- 3 files changed, 121 insertions(+), 70 deletions(-) diff --git a/docs/web-ui/dashboard.md b/docs/web-ui/dashboard.md index b9a2c4a5..7ab49205 100644 --- a/docs/web-ui/dashboard.md +++ b/docs/web-ui/dashboard.md @@ -209,8 +209,10 @@ 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 dashboard tab keeps the rebuild-queue *state* for the SW4RM -card badges without rendering these panels. +The SW4RM tab (`/dashboard.html`) independently self-fetches the same +`GET /api/jobq/graph` endpoint for its own card badges + queue-summary +banner (`refreshJobqGraph`, `swarm.js`) — same data source, no panel +rendering, no shared fetch between the two pages. **R3BU1LD QU3U3** — pending, in-flight, and recently-settled container operations: rebuilds, meta-update cascades, and first-spawns. One @@ -222,9 +224,11 @@ 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 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). +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). 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 @@ -912,17 +916,21 @@ fetch entirely. their own label directly via `TransientSet`/`TransientCleared` events carrying no backing node at all. - (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. + (2) The **job-queue fallback** (`jobqNodesState`, the flat + `GraphNode[]` from `GET /api/jobq/graph` — `swarm.js`'s + `refreshJobqGraph`, same generic endpoint the BU1LDS page's + `` self-fetches, see above) only fires when an + agent has **zero** transients — since (1) now covers every + `Running` node unconditionally, a `Running` job-queue node 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 nodes carrying a non-empty + `payload.data.agent`, and (mara, on review) the badge shows the + node's raw `payload.label` 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. @@ -966,13 +974,19 @@ 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** — 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. +**Build-queue summary banner** — when `jobqNodesState` has any +`Pending` / `Running`(-or-`Finishing`) **group roots** (`parent: +null` — each an independent queued/running operation, not a raw +node count: a multi-step op's not-yet-started sub-steps don't inflate +the number), 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). A root's own `state` already +carries its subtree's roll-up (`hive-jobq-wire`'s contract), so this +is a direct filter over `jobqNodesState`, not a client-side rollup +derivation. 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/swarm.js b/frontend/packages/dashboard/src/swarm.js index beff26ab..72a45321 100644 --- a/frontend/packages/dashboard/src/swarm.js +++ b/frontend/packages/dashboard/src/swarm.js @@ -27,7 +27,18 @@ const CTX_CAUTION_TOKENS = 100_000; // fallback yellow threshold (~= 50% of 200k // ─── module-level state ───────────────────────────────────────────────────── -let rebuildQueueState = []; +// Flat GraphNode[] from GET /api/jobq/graph (hive-jobq-wire's generic +// shape — see hive-jobq-wire/src/lib.rs) — the last DagView/NodeView +// (job_queue-specific) read left on this page, migrated off in favour of +// the generic endpoint builds.js already established the pattern for +// (see mountJobqGraph there). "Nothing is hidden" on this endpoint: every +// retained job group's nodes ride the wire, `Done` included, and each +// group's root is an ordinary node (`parent: null`) whose own `state` IS +// the group's roll-up already — unlike the old DagView, no client-side +// rollup derivation is needed, just an explicit filter for what's still +// active wherever that's wanted (see queuedOpsByAgent + the queue-summary +// banner in renderContainers). +let jobqNodesState = []; // Keyed container row cache. Maps agent name -> { el:
  • , fingerprint }. // Allows renderContainers to skip rebuilding rows whose displayed state @@ -52,24 +63,35 @@ const transientsState = new Map(); // tab-gated visibility). const selectionState = new Set(); -// ─── rebuild queue ────────────────────────────────────────────────────────── +// ─── job queue graph ────────────────────────────────────────────────────── -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. +// Fetches the graph fresh and re-renders. Called on cold load (see +// tabs.js's refreshState) and whenever `rebuild_queue_changed` fires +// (applyRebuildQueueChanged below) — that event is now read purely as a +// refetch trigger, the same treatment builds.js already gives it for +// .refresh(). Best-effort: a failed fetch just leaves +// the previous snapshot in place rather than wiping badges on a network +// blip (same tolerance the per-row dashboard-state fetch below has). +export async function refreshJobqGraph() { + let nodes; + try { + const r = await fetch('/api/jobq/graph'); + if (!r.ok) return; + nodes = await r.json(); + } catch { + return; + } + jobqNodesState = nodes; renderContainersFromState(); } -// Map from agent name -> the queued op's raw `kind` string, backing -// the SW4RM card's fallback pending badge. Pending only — every +export function applyRebuildQueueChanged() { + // No `ev.queue` payload read anymore — that event still carries its + // own DagView-shaped queue snapshot on the wire, but this page no + // longer reads it. Purely a "something changed, go refetch" signal. + refreshJobqGraph(); +} +// Map from agent name -> the queued node's raw `payload.label` 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 @@ -78,31 +100,31 @@ export function applyRebuildQueueChanged(ev) { // transients can't represent, since `running_transients()` on the // backend is a Running-only test. // -// The returned `kind` is displayed as-is (mara, on review: "drop +// The returned label 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 +// Agent is per-node, not per-group (a group 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`. +// derives each agent's queued state from whichever of its own nodes is +// pending, not from the group root. No "skip agentless/spawn nodes" +// filter is needed here (an earlier version of this function keyed on +// a DAG-level `kind` field that no longer exists on the wire — dead +// code since that refactor): a node with no `data.agent` is silently +// skipped below, and an agent with no existing container row never +// gets its map entry read regardless (see containersState in state.js +// — it's populated straight from the real container roster, not a +// pre-spawn placeholder). 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); - } + for (const n of jobqNodesState) { + if (n.state !== 'Pending') continue; + const agent = n.payload.data && n.payload.data.agent; + if (!agent) continue; + // First node found wins — with only one state to consider + // (Pending), there's no priority to resolve between groups. + if (!out.has(agent)) out.set(agent, n.payload.label); } return out; } @@ -622,17 +644,27 @@ export function renderContainers(s) { )); } - // 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 + // Queue-summary banner: when the job queue has active work, show one + // compact at-a-glance line + a link to the full queue on the BU1LDS + // 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; + // + // Counts distinct *groups* (root nodes, `parent == null`), not raw + // nodes — a single group can have several sequential not-yet-started + // steps, and "3 running · 2 queued" means 3/2 whole operations, not + // 3/2 individual steps. A root's own `state` already IS its subtree's + // roll-up (hive-jobq-wire's contract — no client-side derivation + // needed, unlike the old DagView), so this is a direct filter, not a + // parent-chain walk. `Finishing` counts as running (own work done, + // children still going, still in flight). Settled roots (Done/Failed/ + // Cancelled/Skipped) ride the wire too now ("nothing is hidden" — + // unlike the old DagView snapshot, which dropped them) so both counts + // filter explicitly rather than assuming absence. + const roots = jobqNodesState.filter((n) => n.parent == null); + const running = roots.filter((n) => n.state === 'Running' || n.state === 'Finishing').length; + const queued = roots.filter((n) => n.state === 'Pending').length; + if (running || queued) { const parts = []; if (running) parts.push(`${running} running`); if (queued) parts.push(`${queued} queued`); diff --git a/frontend/packages/dashboard/src/tabs.js b/frontend/packages/dashboard/src/tabs.js index ab079b78..4ac4837d 100644 --- a/frontend/packages/dashboard/src/tabs.js +++ b/frontend/packages/dashboard/src/tabs.js @@ -42,7 +42,7 @@ import { renderQuestions, activeQuestionCount, } from './call.js'; import { - syncRebuildQueueFromSnapshot, syncTransientsFromSnapshot, + refreshJobqGraph, syncTransientsFromSnapshot, applyRebuildQueueChanged, applyContainerStateChanged, applyContainerRemoved, applyTransientSet, applyTransientCleared, renderContainers, renderContainersFromState, @@ -234,9 +234,14 @@ 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); + // Job-queue graph feeds the SW4RM agent-card badges + // (queuedOpsByAgent) + the queue-summary banner; its own detailed + // view lives on /builds.html. Self-fetches GET /api/jobq/graph — + // not read off `s` (this page's snapshot carries no jobq field) — + // fire-and-forget: renderContainers below runs off whatever + // jobqNodesState already holds, and refreshJobqGraph's own + // re-render catches up once the fetch resolves. + refreshJobqGraph(); renderContainers(s); // Sync the derived approvals + questions stores from the // snapshot, then render. Live `*_added` / `*_resolved` events From 17f61d1da68fc729607060bfe035ffbaf5237ff0 Mon Sep 17 00:00:00 2001 From: iris Date: Mon, 3 Aug 2026 19:18:52 +0200 Subject: [PATCH 2/4] swarm.js: drop per-agent pending-badge fallback, transients-only now MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit mara, on review: "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." Deletes queuedOpsByAgent() entirely — no more per-agent badge derived from Pending-state job-queue nodes. A card's pending badges are now driven exclusively by transientsState (i.e. actually-running work); queued-but-not-started work shows nothing on the card until a node starts. jobqNodesState + refreshJobqGraph() stay, now feeding only the queue-summary banner (a separate, still-open question — mara separately asked for a dedicated rollup endpoint for that, tracked apart from this PR). Collapses the now-always-coincident `pending`/`pending-running` row classes into one (`pending-running`) — there's no more queued-only row state to visually distinguish it from. docs/web-ui/dashboard.md's Container-row section rewritten to match: the two-store priority-fallback description is gone, replaced with "transients only." --- docs/web-ui/dashboard.md | 53 ++++------- frontend/packages/dashboard/src/dashboard.css | 7 +- frontend/packages/dashboard/src/swarm.js | 95 +++++++------------ frontend/packages/dashboard/src/tabs.js | 15 +-- 4 files changed, 65 insertions(+), 105 deletions(-) diff --git a/docs/web-ui/dashboard.md b/docs/web-ui/dashboard.md index 7ab49205..db0d078b 100644 --- a/docs/web-ui/dashboard.md +++ b/docs/web-ui/dashboard.md @@ -883,22 +883,19 @@ 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. - 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. + 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. - **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: + **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: - The label vocabulary is **open** — it is the node's own wire tag (`NodeKind::as_str`, the same strings `NodeView.kind` carries), @@ -916,24 +913,14 @@ fetch entirely. their own label directly via `TransientSet`/`TransientCleared` events carrying no backing node at all. - (2) The **job-queue fallback** (`jobqNodesState`, the flat - `GraphNode[]` from `GET /api/jobq/graph` — `swarm.js`'s - `refreshJobqGraph`, same generic endpoint the BU1LDS page's - `` self-fetches, see above) only fires when an - agent has **zero** transients — since (1) now covers every - `Running` node unconditionally, a `Running` job-queue node 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 nodes carrying a non-empty - `payload.data.agent`, and (mara, on review) the badge shows the - node's raw `payload.label` 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. + **Queued (not-yet-started) work shows nothing on the card.** An + earlier version of this page had a job-queue-backed fallback badge + for the `Pending` case (`queuedOpsByAgent()` in swarm.js, reading + `jobqNodesState`); 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."* `jobqNodesState` still exists in swarm.js, but now + feeds only the queue-summary banner below, not per-card badges. 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` diff --git a/frontend/packages/dashboard/src/dashboard.css b/frontend/packages/dashboard/src/dashboard.css index 8615b003..251652fe 100644 --- a/frontend/packages/dashboard/src/dashboard.css +++ b/frontend/packages/dashboard/src/dashboard.css @@ -347,8 +347,11 @@ hive-agent-menu { .container-row:hover hive-agent-menu { --menu-btn-opacity: 1; } -/* Pending state splits queued vs running. */ -.container-row.pending .actions { opacity: 0.4; pointer-events: none; } +/* 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; } .container-row.pending-running { border-color: var(--amber); background: color-mix(in srgb, var(--amber) 5%, transparent); diff --git a/frontend/packages/dashboard/src/swarm.js b/frontend/packages/dashboard/src/swarm.js index 72a45321..1d09b63c 100644 --- a/frontend/packages/dashboard/src/swarm.js +++ b/frontend/packages/dashboard/src/swarm.js @@ -28,16 +28,18 @@ const CTX_CAUTION_TOKENS = 100_000; // fallback yellow threshold (~= 50% of 200k // ─── module-level state ───────────────────────────────────────────────────── // Flat GraphNode[] from GET /api/jobq/graph (hive-jobq-wire's generic -// shape — see hive-jobq-wire/src/lib.rs) — the last DagView/NodeView -// (job_queue-specific) read left on this page, migrated off in favour of -// the generic endpoint builds.js already established the pattern for -// (see mountJobqGraph there). "Nothing is hidden" on this endpoint: every -// retained job group's nodes ride the wire, `Done` included, and each -// group's root is an ordinary node (`parent: null`) whose own `state` IS -// the group's roll-up already — unlike the old DagView, no client-side -// rollup derivation is needed, just an explicit filter for what's still -// active wherever that's wanted (see queuedOpsByAgent + the queue-summary -// banner in renderContainers). +// shape — see hive-jobq-wire/src/lib.rs) — feeds *only* the queue-summary +// banner in renderContainers (mara, on review: "swarm.js should not need +// to pull in the jobq to do its job" — the per-agent pending-row fallback +// that used to be this state's other consumer is gone, see +// queuedOpsByAgent's removal below). Once a dedicated rollup endpoint +// exists (status-count pairs, requested separately) the banner moves +// onto that instead and this fetch goes away entirely. "Nothing is +// hidden" on this endpoint: every retained job group's nodes ride the +// wire, `Done` included, and each group's root is an ordinary node +// (`parent: null`) whose own `state` IS the group's roll-up already — +// unlike the old DagView, no client-side rollup derivation is needed, +// just an explicit filter for what's still active. let jobqNodesState = []; // Keyed container row cache. Maps agent name -> { el:
  • , fingerprint }. @@ -90,44 +92,13 @@ export function applyRebuildQueueChanged() { // longer reads it. Purely a "something changed, go refetch" signal. refreshJobqGraph(); } -// Map from agent name -> the queued node's raw `payload.label` 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. -// -// The returned label 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-group (a group 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 whichever of its own nodes is -// pending, not from the group root. No "skip agentless/spawn nodes" -// filter is needed here (an earlier version of this function keyed on -// a DAG-level `kind` field that no longer exists on the wire — dead -// code since that refactor): a node with no `data.agent` is silently -// skipped below, and an agent with no existing container row never -// gets its map entry read regardless (see containersState in state.js -// — it's populated straight from the real container roster, not a -// pre-spawn placeholder). -function queuedOpsByAgent() { - const out = new Map(); - for (const n of jobqNodesState) { - if (n.state !== 'Pending') continue; - const agent = n.payload.data && n.payload.data.agent; - if (!agent) continue; - // First node found wins — with only one state to consider - // (Pending), there's no priority to resolve between groups. - if (!out.has(agent)) out.set(agent, n.payload.label); - } - return out; -} +// Per-agent queued-but-not-running badges used to have their own +// fallback here (`queuedOpsByAgent()`, reading `Pending`-state nodes +// off `jobqNodesState`). Removed per mara, on review: "remove the per +// agent pending stuff - only show what is running" — a card's pending +// badges are now driven by `transientsState` alone (see below), which +// is exactly "what is running": queued-not-yet-started work shows +// nothing on the card until a node actually starts. // ─── transients ───────────────────────────────────────────────────────────── @@ -366,9 +337,14 @@ 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' : ''), }); @@ -704,10 +680,6 @@ 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. @@ -721,20 +693,17 @@ export function renderContainers(s) { const containerBase = gatewayLinks ? `/agent/${encodeURIComponent(c.name)}` : `http://${hostname}:${c.port}`; - // 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. + // 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. 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 transientKinds = transientKindsMap + const pending = transientKindsMap ? Array.from(transientKindsMap.keys()).sort() : []; - const queuedKind = transientKinds.length === 0 ? queuedOps.get(c.name) : null; - const pending = transientKinds.length > 0 - ? transientKinds - : (queuedKind ? [queuedKind] : []); - const opRunning = transientKinds.length > 0; + const opRunning = pending.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 4ac4837d..7b3a817f 100644 --- a/frontend/packages/dashboard/src/tabs.js +++ b/frontend/packages/dashboard/src/tabs.js @@ -234,13 +234,14 @@ window.marked = marked; // `transientsState` + `containersState`, not from `s.*`). syncTransientsFromSnapshot(s); syncContainersFromSnapshot(s); - // Job-queue graph feeds the SW4RM agent-card badges - // (queuedOpsByAgent) + the queue-summary banner; its own detailed - // view lives on /builds.html. Self-fetches GET /api/jobq/graph — - // not read off `s` (this page's snapshot carries no jobq field) — - // fire-and-forget: renderContainers below runs off whatever - // jobqNodesState already holds, and refreshJobqGraph's own - // re-render catches up once the fetch resolves. + // Job-queue graph feeds only the SW4RM queue-summary banner now + // (per-agent card badges are transient-only — see swarm.js); its + // own detailed view lives on /builds.html. Self-fetches GET + // /api/jobq/graph — not read off `s` (this page's snapshot + // carries no jobq field) — fire-and-forget: renderContainers + // below runs off whatever jobqNodesState already holds, and + // refreshJobqGraph's own re-render catches up once the fetch + // resolves. refreshJobqGraph(); renderContainers(s); // Sync the derived approvals + questions stores from the From 7b05656e175a7962106b7021708271853c3dd9b8 Mon Sep 17 00:00:00 2001 From: iris Date: Mon, 3 Aug 2026 19:38:44 +0200 Subject: [PATCH 3/4] =?UTF-8?q?tabs.js:=20fix=20stale=20comment=20?= =?UTF-8?q?=E2=80=94=20rebuild=5Fqueue=5Fchanged=20feeds=20the=20queue-sum?= =?UTF-8?q?mary=20banner=20only?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/packages/dashboard/src/tabs.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/frontend/packages/dashboard/src/tabs.js b/frontend/packages/dashboard/src/tabs.js index 7b3a817f..70e9560a 100644 --- a/frontend/packages/dashboard/src/tabs.js +++ b/frontend/packages/dashboard/src/tabs.js @@ -313,7 +313,9 @@ 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 stays: it refreshes the SW4RM badges. + // rebuild_queue_changed stays: it refreshes the SW4RM queue-summary + // banner (see swarm.js — the per-agent badges it fed too, before + // mara's "only show what is running" ask, are transient-only now). rebuild_queue_changed: applyRebuildQueueChanged, schedules_changed: applySchedulesChanged, capabilities_changed: applyCapabilitiesChanged, From 40cc115a0a5efdc6f98c56d86cc2b7983782dcbd Mon Sep 17 00:00:00 2001 From: iris Date: Mon, 3 Aug 2026 20:01:09 +0200 Subject: [PATCH 4/4] swarm.js: drop the queue-summary banner rather than ship it on an interim jobq fetch MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit mara, on the already-approved 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." Splitting: the transients-only per-agent badge fix is real, correct, and fixes a live regression (the old DagView fields it read no longer exist) — nothing about it depends on job-queue data at all, so it ships as-is. The queue-summary banner is the part that doesn't belong in this shape: it was reading GET /api/jobq/graph directly and deriving counts client-side as an interim stand-in for the dedicated rollup endpoint mara separately asked for — exactly the kind of stopgap-on-a-stopgap her comment is calling out, since the endpoint that should serve it doesn't exist on main yet. Removes jobqNodesState, refreshJobqGraph(), the rebuild_queue_changed SSE subscription, and the banner's render block from swarm.js/tabs.js entirely — swarm.js now reads no job-queue state of any kind, fully satisfying "swarm.js should not need to pull in the jobq to do its job." The banner comes back once the rollup endpoint (hyperhive#2985's follow-up) exists, reading that directly instead of the full graph. Until then the per-agent transient pills still show what's actually running on each card; only the hive-wide "N running / M queued" summary line is temporarily gone. CSS classes for the banner (.queue-summary/.queue-summary-link) kept in dashboard.css rather than deleted-then-restored — commented as currently unused, expected to come back unchanged. docs/web-ui/dashboard.md updated to match (Container-row pending- badge section, the removed Build-queue-summary-banner section, and the BU1LDS-page note that used to describe SW4RM's now-removed parallel fetch). --- docs/web-ui/dashboard.md | 58 ++++++---- frontend/packages/dashboard/src/dashboard.css | 6 +- frontend/packages/dashboard/src/swarm.js | 107 ++++-------------- frontend/packages/dashboard/src/tabs.js | 23 ++-- 4 files changed, 69 insertions(+), 125 deletions(-) diff --git a/docs/web-ui/dashboard.md b/docs/web-ui/dashboard.md index db0d078b..075401cd 100644 --- a/docs/web-ui/dashboard.md +++ b/docs/web-ui/dashboard.md @@ -209,10 +209,12 @@ 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`) independently self-fetches the same -`GET /api/jobq/graph` endpoint for its own card badges + queue-summary -banner (`refreshJobqGraph`, `swarm.js`) — same data source, no panel -rendering, no shared fetch between the two pages. +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. **R3BU1LD QU3U3** — pending, in-flight, and recently-settled container operations: rebuilds, meta-update cascades, and first-spawns. One @@ -913,14 +915,16 @@ 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.** An - earlier version of this page had a job-queue-backed fallback badge - for the `Pending` case (`queuedOpsByAgent()` in swarm.js, reading - `jobqNodesState`); 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."* `jobqNodesState` still exists in swarm.js, but now - feeds only the queue-summary banner below, not per-card badges. + **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. 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` @@ -961,19 +965,23 @@ 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** — when `jobqNodesState` has any -`Pending` / `Running`(-or-`Finishing`) **group roots** (`parent: -null` — each an independent queued/running operation, not a raw -node count: a multi-step op's not-yet-started sub-steps don't inflate -the number), 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). A root's own `state` already -carries its subtree's roll-up (`hive-jobq-wire`'s contract), so this -is a direct filter over `jobqNodesState`, not a client-side rollup -derivation. 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. +**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. ### Themed dialogs diff --git a/frontend/packages/dashboard/src/dashboard.css b/frontend/packages/dashboard/src/dashboard.css index 251652fe..843b8b42 100644 --- a/frontend/packages/dashboard/src/dashboard.css +++ b/frontend/packages/dashboard/src/dashboard.css @@ -458,7 +458,11 @@ 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. */ + 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. */ .queue-summary { display: flex; align-items: center; diff --git a/frontend/packages/dashboard/src/swarm.js b/frontend/packages/dashboard/src/swarm.js index 1d09b63c..c86a358c 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, rebuild-queue, and -// transient ops. See docs/web-ui.md::Container row for the rendering contract. +// and all live-update apply handlers for container-state and transient ops. +// See docs/web-ui.md::Container row for the rendering contract. import { $, form, fmtAgeSecs, @@ -27,21 +27,6 @@ const CTX_CAUTION_TOKENS = 100_000; // fallback yellow threshold (~= 50% of 200k // ─── module-level state ───────────────────────────────────────────────────── -// Flat GraphNode[] from GET /api/jobq/graph (hive-jobq-wire's generic -// shape — see hive-jobq-wire/src/lib.rs) — feeds *only* the queue-summary -// banner in renderContainers (mara, on review: "swarm.js should not need -// to pull in the jobq to do its job" — the per-agent pending-row fallback -// that used to be this state's other consumer is gone, see -// queuedOpsByAgent's removal below). Once a dedicated rollup endpoint -// exists (status-count pairs, requested separately) the banner moves -// onto that instead and this fetch goes away entirely. "Nothing is -// hidden" on this endpoint: every retained job group's nodes ride the -// wire, `Done` included, and each group's root is an ordinary node -// (`parent: null`) whose own `state` IS the group's roll-up already — -// unlike the old DagView, no client-side rollup derivation is needed, -// just an explicit filter for what's still active. -let jobqNodesState = []; - // 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 @@ -65,42 +50,20 @@ const transientsState = new Map(); // tab-gated visibility). const selectionState = new Set(); -// ─── job queue graph ────────────────────────────────────────────────────── - -// Fetches the graph fresh and re-renders. Called on cold load (see -// tabs.js's refreshState) and whenever `rebuild_queue_changed` fires -// (applyRebuildQueueChanged below) — that event is now read purely as a -// refetch trigger, the same treatment builds.js already gives it for -// .refresh(). Best-effort: a failed fetch just leaves -// the previous snapshot in place rather than wiping badges on a network -// blip (same tolerance the per-row dashboard-state fetch below has). -export async function refreshJobqGraph() { - let nodes; - try { - const r = await fetch('/api/jobq/graph'); - if (!r.ok) return; - nodes = await r.json(); - } catch { - return; - } - jobqNodesState = nodes; - renderContainersFromState(); -} -export function applyRebuildQueueChanged() { - // No `ev.queue` payload read anymore — that event still carries its - // own DagView-shaped queue snapshot on the wire, but this page no - // longer reads it. Purely a "something changed, go refetch" signal. - refreshJobqGraph(); -} -// Per-agent queued-but-not-running badges used to have their own -// fallback here (`queuedOpsByAgent()`, reading `Pending`-state nodes -// off `jobqNodesState`). Removed per mara, on review: "remove the per -// agent pending stuff - only show what is running" — a card's pending -// badges are now driven by `transientsState` alone (see below), which -// is exactly "what is running": queued-not-yet-started work shows -// nothing on the card until a node actually starts. - // ─── transients ───────────────────────────────────────────────────────────── +// +// 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). export function syncTransientsFromSnapshot(s) { transientsState.clear(); @@ -620,37 +583,15 @@ export function renderContainers(s) { )); } - // Queue-summary banner: when the job queue has active work, show one - // compact at-a-glance line + a link to the full queue on the BU1LDS - // 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. - // - // Counts distinct *groups* (root nodes, `parent == null`), not raw - // nodes — a single group can have several sequential not-yet-started - // steps, and "3 running · 2 queued" means 3/2 whole operations, not - // 3/2 individual steps. A root's own `state` already IS its subtree's - // roll-up (hive-jobq-wire's contract — no client-side derivation - // needed, unlike the old DagView), so this is a direct filter, not a - // parent-chain walk. `Finishing` counts as running (own work done, - // children still going, still in flight). Settled roots (Done/Failed/ - // Cancelled/Skipped) ride the wire too now ("nothing is hidden" — - // unlike the old DagView snapshot, which dropped them) so both counts - // filter explicitly rather than assuming absence. - const roots = jobqNodesState.filter((n) => n.parent == null); - const running = roots.filter((n) => n.state === 'Running' || n.state === 'Finishing').length; - const queued = roots.filter((n) => n.state === 'Pending').length; - if (running || queued) { - 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 →'), - )); - } + // 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. if (!containers.length && !transientsState.size) { root.append(el('p', { class: 'empty' }, 'no managed containers')); diff --git a/frontend/packages/dashboard/src/tabs.js b/frontend/packages/dashboard/src/tabs.js index 70e9560a..40dda34e 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 { - refreshJobqGraph, syncTransientsFromSnapshot, - applyRebuildQueueChanged, applyContainerStateChanged, applyContainerRemoved, + syncTransientsFromSnapshot, + applyContainerStateChanged, applyContainerRemoved, applyTransientSet, applyTransientCleared, renderContainers, renderContainersFromState, renderSelectionBar, renderPeerHives, @@ -234,15 +234,6 @@ window.marked = marked; // `transientsState` + `containersState`, not from `s.*`). syncTransientsFromSnapshot(s); syncContainersFromSnapshot(s); - // Job-queue graph feeds only the SW4RM queue-summary banner now - // (per-agent card badges are transient-only — see swarm.js); its - // own detailed view lives on /builds.html. Self-fetches GET - // /api/jobq/graph — not read off `s` (this page's snapshot - // carries no jobq field) — fire-and-forget: renderContainers - // below runs off whatever jobqNodesState already holds, and - // refreshJobqGraph's own re-render catches up once the fetch - // resolves. - refreshJobqGraph(); renderContainers(s); // Sync the derived approvals + questions stores from the // snapshot, then render. Live `*_added` / `*_resolved` events @@ -313,10 +304,10 @@ 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 stays: it refreshes the SW4RM queue-summary - // banner (see swarm.js — the per-agent badges it fed too, before - // mara's "only show what is running" ask, are transient-only now). - rebuild_queue_changed: applyRebuildQueueChanged, + // 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). schedules_changed: applySchedulesChanged, capabilities_changed: applyCapabilitiesChanged, tool_groups_changed: applyToolGroupsChanged, @@ -338,7 +329,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,rebuild_queue_changed,' + + 'container_state_changed,container_removed,' + 'schedules_changed,capabilities_changed,tool_groups_changed', ); es.onmessage = (e) => {