fix(#2515): derive per-agent in-flight badge from node state, not DAG state

inFlightOpsByAgent() read e.agent, a DAG-level field that no longer
exists (agent moved to per-node when DAGs became agent-per-node). So
the SW4RM tab's 'building...'/'meta-updating...' badges never matched
any real agent, and multi-agent DAGs (a startup sweep's MetaLock
cascade, a hive-wide restart) showed nothing at all on the per-agent
cards even while the rebuild queue clearly had them in flight.

Derive each agent's in-flight kind+state from its own node(s) within
the entry instead: a DAG can be 'running' overall while a given
agent's subgraph hasn't started (still queued behind an earlier node
in its chain), so per-node state is also more accurate than the old
per-DAG state for the badge, not just more available.
This commit is contained in:
iris 2026-07-15 23:15:14 +02:00
commit 614e8c6d5a

View file

@ -59,11 +59,18 @@ export function applyRebuildQueueChanged(ev) {
// See docs/web-ui.md::Container row for the badge taxonomy.
renderContainersFromState();
}
// Map from agent name -> highest-priority in-flight queue entry
// (`running` beats `queued`). Used by the container row renderer
// to surface "building..." / "meta-updating..." badges on the
// SW4RM tab when an op is still in the rebuild queue but no
// operator-initiated transient is set.
// Map from agent name -> highest-priority in-flight op ({ kind, state })
// (`running` beats `queued`). Used by the container row renderer to
// surface "building..." / "meta-updating..." badges on the SW4RM tab
// when an op is still in the rebuild queue but no operator-initiated
// transient is set.
//
// 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 in-flight state from its own node(s) within the
// entry rather than the DAG's overall `state`/`kind` — a DAG can be
// `running` overall while a given agent's subgraph hasn't started yet
// (still queued behind an earlier node in its chain), and vice versa.
function inFlightOpsByAgent() {
const out = new Map();
for (const e of rebuildQueueState) {
@ -74,10 +81,20 @@ function inFlightOpsByAgent() {
// surfacing if the spawn op happens to land in the queue
// while the row exists transiently.
if (e.kind === 'spawn') continue;
const cur = out.get(e.agent);
// Prefer running over queued; otherwise keep the first match.
if (!cur || (cur.state === 'queued' && e.state === 'running')) {
out.set(e.agent, e);
const perAgentState = new Map();
for (const n of e.nodes || []) {
if (!n.agent) continue;
if (n.state !== 'queued' && n.state !== 'running') continue;
const cur = perAgentState.get(n.agent);
if (!cur || (cur === 'queued' && n.state === 'running')) {
perAgentState.set(n.agent, n.state);
}
}
for (const [agent, state] of perAgentState) {
const cur = out.get(agent);
if (!cur || (cur.state === 'queued' && state === 'running')) {
out.set(agent, { kind: e.kind, state });
}
}
}
return out;