Fixes#2822.
`swarm.js` had two independent per-agent "is this in flight" sources:
`transientsState` (operator/worker-initiated ops the backend chose to
flag) and `inFlightOpsByAgent()`, a separate derivation straight from
`rebuildQueueState` covering everything else. Since #3010/#3016,
`running_transients()` is a status-only test — any `Running` job-queue
node naming a non-empty agent lights a transient pill, not just a
curated subset — so the second source's Running-state handling is now
provably redundant: a Running node with an agent always already has a
transient by the time `queuedOpsByAgent()` (renamed from
`inFlightOpsByAgent`) would be consulted.
## What changed
- `transientsState`: `Map<name, {kind, since_unix}>` (one pill per
agent) -> `Map<name, Map<kind, since_unix>>` (several pills per
agent). `applyTransientSet`/`applyTransientCleared` now add/remove
by `(name, kind)` rather than overwrite/delete by name alone, using
`TransientCleared`'s `transient_kind` field (landed in #3016) to
know which pill cleared. `syncTransientsFromSnapshot` groups the
now-flat `TransientView` list by name instead of assuming one row
per agent.
- `inFlightOpsByAgent()` -> `queuedOpsByAgent()`: trimmed to the
`Pending` (queued, not yet started) case only. The `Running` branch
and its "running beats queued" priority logic are gone entirely —
dead weight now that transients cover every running case
unconditionally.
- Render loop: an agent's transients win outright whenever any exist
(rendered as **one badge per pill**, not collapsed into one label —
mara: "show all running nodes that name the agent"); the queued
fallback only applies when a agent has zero transients. `opRunning`
simplifies to "does this agent have at least one transient".
- `docs/web-ui/dashboard.md`'s Container-row section rewritten to
match — it described a "transient, then in-flight-queue, in
priority order" model that's no longer accurate now that the second
source only ever fires for the one case the first can't represent.
## Verification
`npm run build` clean for both packages (dashboard + agent). Standalone
re-derivation of the transient-map + queued-fallback logic
(`/tmp/verify-swarm-transients.mjs`, not part of this diff) run against
constructed event sequences: single-pill lifecycle, two simultaneous
pills on one agent with independent clear-by-kind, clearing an unknown
kind is a safe no-op, a flat snapshot with duplicate agent names groups
correctly, the queued fallback only fires when no transient exists and
steps aside the instant one arrives, and a Running-state rebuild-queue
entry produces no queued badge (confirming the Pending-only trim is
correct, not just assumed). All 17 checks passed.
Verified directly against the merged backend rather than trusting
summaries: `job_queue/mod.rs::running_transients()` filters
`State::Running` only (not Pending — an earlier note of mine claiming
otherwise was imprecise paraphrasing), and `NodeView.agent` /
`running_transients()`'s agent both resolve through the same
`payload.agent()`, so a Running node's presence in `rebuild_queue`
and its presence as a transient are guaranteed consistent, not just
usually so.
#2985 (DagView/NodeView deletion) unblocks once this merges — atlas is
waiting on a ping.