From 17f61d1da68fc729607060bfe035ffbaf5237ff0 Mon Sep 17 00:00:00 2001 From: iris Date: Mon, 3 Aug 2026 19:18:52 +0200 Subject: [PATCH] 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