swarm.js: drop per-agent pending-badge fallback, transients-only now

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."
This commit is contained in:
iris 2026-08-03 19:18:52 +02:00 committed by mara
commit 17f61d1da6
4 changed files with 66 additions and 106 deletions

View file

@ -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<kind, since_unix>`) — 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<kind, since_unix>`) — 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
`<hive-jobq-graph>` 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 · <name>`, blue) appears when the
container is running and the harness has persisted a model name
(`harness/hyperhive-model`). Read by hive-c0re's `ContainerView`

View file

@ -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);

View file

@ -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: <li>, 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 <li> 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

View file

@ -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