Compare commits

..
4 changed files with 148 additions and 105 deletions

View file

@ -209,12 +209,8 @@ Three sub-tabs: **R3BU1LD QU3U3** (default), **M3T4 1NPUTS**,
**BUILD L0GS**. Its own esbuild bundle (`builds.js`); cold-loads **BUILD L0GS**. Its own esbuild bundle (`builds.js`); cold-loads
`/api/state` and subscribes to `/api/dashboard/stream` for `/api/state` and subscribes to `/api/dashboard/stream` for
`rebuild_queue_changed`, `meta_inputs_changed`, `meta_update_running`. `rebuild_queue_changed`, `meta_inputs_changed`, `meta_update_running`.
The SW4RM tab (`/dashboard.html`) does **not** read this endpoint (or The dashboard tab keeps the rebuild-queue *state* for the SW4RM
any job-queue state) at all — mara, on review: "swarm.js should not card badges without rendering these panels.
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 **R3BU1LD QU3U3** — pending, in-flight, and recently-settled container
operations: rebuilds, meta-update cascades, and first-spawns. One operations: rebuilds, meta-update cascades, and first-spawns. One
@ -226,11 +222,9 @@ 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 below it that the generic view doesn't show. The component owns
fetching, cold and live: `GET /api/jobq/graph` on mount, and fetching, cold and live: `GET /api/jobq/graph` on mount, and
`.refresh()` on every `rebuild_queue_changed` SSE tick (that event `.refresh()` on every `rebuild_queue_changed` SSE tick (that event
still carries its own `Vec<QueueEntry>` payload — `DagView`-shaped, still carries its own `Vec<QueueEntry>` payload on the wire — the
also read by `hivectl`'s own wait/progress loop, a separate migration SW4RM tab still consumes it for the badges, untouched — this page just
— on the wire, but neither dashboard page reads it anymore; both ignores the payload and treats the tick as a refetch trigger).
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 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 per-agent subgraphs and sub-steps render as nodes within that one
@ -885,19 +879,22 @@ fetch entirely.
independent badge rather than being collapsed into one label, independent badge rather than being collapsed into one label,
matching the existing multi-badge convention this line already uses matching the existing multi-badge convention this line already uses
for `paused`/`needs_update`/model/ctx. for `paused`/`needs_update`/model/ctx.
Any pending badge means the row is actually **running** something The row visual splits queued vs running: a **queued** entry (no
right now — there is no separate queued-but-not-started row state transient yet, see below) shows only the pending-state pill (no row
to visually distinguish it from (see **Pending-badge derivation** tint, so a long queue doesn't paint half the tab amber); a
below), so every row carrying ≥1 badge keeps the amber row tint AND **running** entry (at least one transient) keeps the amber row tint
draws a **rotating amber ring** around the agent icon. AND draws a **rotating amber ring** around the agent icon, so it's
obvious at a glance which container is actually moving.
**Pending-badge derivation:** transients only (`transientsState`, **Pending-badge derivation:** two separate stores, but no longer a
keyed `agent -> Map<kind, since_unix>`) — a transient is **derived priority *order* between them — the second only ever applies when
from a job-queue node currently `Running`** against that agent, not the first has nothing to say. (1) **Transients**
declared per request, so its label follows the operation as it (`transientsState`, keyed `agent -> Map<kind, since_unix>`) — a
progresses (a rebuild reads `stop_for_update`, then `swap`, then transient is **derived from a job-queue node currently `Running`**
`reconcile` rather than one constant `rebuilding` for its whole against that agent, not declared per request, so its label follows
life). Two consequences for anything rendering it: 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 - The label vocabulary is **open** — it is the node's own wire tag
(`NodeKind::as_str`, the same strings `NodeView.kind` carries), (`NodeKind::as_str`, the same strings `NodeView.kind` carries),
@ -915,16 +912,20 @@ fetch entirely.
their own label directly via `TransientSet`/`TransientCleared` their own label directly via `TransientSet`/`TransientCleared`
events carrying no backing node at all. events carrying no backing node at all.
**Queued (not-yet-started) work shows nothing on the card, and (2) The **rebuild-queue fallback** (`rebuildQueueState`) only
`swarm.js` reads no job-queue state at all.** An earlier version of fires when an agent has **zero** transients — since (1) now covers
this page had a job-queue-backed fallback badge for the `Pending` every `Running` node unconditionally, a `Running` rebuild-queue
case (`queuedOpsByAgent()`, reading a `GET /api/jobq/graph` fetch); entry can never usefully reach this fallback by the time it's
removed per mara, on review of hyperhive#3028's PR: *"swarm.js consulted; the fallback exists purely for the **`Pending`
should not need to pull in the jobq to do its job,"* followed by (queued, not yet started)** case, which `running_transients()`'s
*"remove the per agent pending stuff - only show what is running."* Running-only test cannot represent. `queuedOpsByAgent()` (swarm.js)
The queue-summary banner below was, for the same reason, also pulled reflects this: it only ever looks at `Pending`-state queue nodes,
rather than kept on that same client-side graph-fetch — see its own and (mara, on review) the badge shows the queue entry's raw `kind`
entry below for why. 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.
An **active model badge** (`model · <name>`, blue) appears when the An **active model badge** (`model · <name>`, blue) appears when the
container is running and the harness has persisted a model name container is running and the harness has persisted a model name
(`harness/hyperhive-model`). Read by hive-c0re's `ContainerView` (`harness/hyperhive-model`). Read by hive-c0re's `ContainerView`
@ -965,23 +966,13 @@ per-agent actions and navigation links. Contents:
agent is stale. Banner pulses on each broker SSE event agent is stale. Banner pulses on each broker SSE event
(`pulseBanner` with a 4s grace timer). (`pulseBanner` with a 4s grace timer).
**Build-queue summary banner — currently removed, coming back on a **Build-queue summary banner** — when the rebuild queue has any
different data source.** Previously a compact amber banner above the `queued` / `running` entries, a compact amber banner sits above the
container list (`◐ build queue — N running · M queued — view container list: `◐ build queue — N running · M queued — view queue →`
queue →`, linking to the BU1LDS page), derived client-side from `GET (the link goes to the BU1LDS page's R3BU1LD QU3U3). It replaces the
/api/jobq/graph` (filter to `parent: null` group roots — each an old per-transient spinner list; the actual running node for each
independent operation, not a raw node count — bucket by `state`). agent is already shown on its card (transient + in-flight-queue
Pulled per mara, on review of hyperhive#3028's PR: *"dont replace one badges), so the top of the tab only needs the at-a-glance summary.
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 ### Themed dialogs

View file

@ -347,11 +347,8 @@ hive-agent-menu {
.container-row:hover hive-agent-menu { .container-row:hover hive-agent-menu {
--menu-btn-opacity: 1; --menu-btn-opacity: 1;
} }
/* Card actions dim while a row has any pending-state badge (transient- /* Pending state splits queued vs running. */
driven only now see swarm.js's pending-badge derivation comment; .container-row.pending .actions { opacity: 0.4; pointer-events: none; }
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 { .container-row.pending-running {
border-color: var(--amber); border-color: var(--amber);
background: color-mix(in srgb, var(--amber) 5%, transparent); background: color-mix(in srgb, var(--amber) 5%, transparent);
@ -458,11 +455,7 @@ hive-agent-menu {
/* Build-queue summary banner on the SW4RM tab: one compact line /* 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 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 { .queue-summary {
display: flex; display: flex;
align-items: center; align-items: center;

View file

@ -1,7 +1,7 @@
// SW4RM (containers) domain — extracted from tabs.js. // SW4RM (containers) domain — extracted from tabs.js.
// Agent topology, container-row rendering, selection bar, peer-hives block, // Agent topology, container-row rendering, selection bar, peer-hives block,
// and all live-update apply handlers for container-state and transient ops. // and all live-update apply handlers for container-state, rebuild-queue, and
// See docs/web-ui.md::Container row for the rendering contract. // transient ops. See docs/web-ui.md::Container row for the rendering contract.
import { import {
$, form, fmtAgeSecs, $, form, fmtAgeSecs,
@ -27,6 +27,8 @@ const CTX_CAUTION_TOKENS = 100_000; // fallback yellow threshold (~= 50% of 200k
// ─── module-level state ───────────────────────────────────────────────────── // ─── module-level state ─────────────────────────────────────────────────────
let rebuildQueueState = [];
// Keyed container row cache. Maps agent name -> { el: <li>, fingerprint }. // Keyed container row cache. Maps agent name -> { el: <li>, fingerprint }.
// Allows renderContainers to skip rebuilding rows whose displayed state // Allows renderContainers to skip rebuilding rows whose displayed state
// hasn't changed — prevents full-wipe flicker + avoids redundant async // hasn't changed — prevents full-wipe flicker + avoids redundant async
@ -50,20 +52,62 @@ const transientsState = new Map();
// tab-gated visibility). // tab-gated visibility).
const selectionState = new Set(); const selectionState = new Set();
// ─── transients ───────────────────────────────────────────────────────────── // ─── rebuild queue ──────────────────────────────────────────────────────────
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 "<kind> 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.
renderContainersFromState();
}
// Map from agent name -> the queued op's raw `kind` 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.
// //
// This is now the ONLY job-queue-derived state on this page — mara, on // The returned `kind` is displayed as-is (mara, on review: "drop
// review of the DagView migration: "swarm.js should not need to pull in // queuedLabelFor - just show what the backend sends") — same opaque-
// the jobq to do its job." An earlier version of this file also fetched // string treatment `pending`'s transient half already gets, no
// GET /api/jobq/graph directly for two things now gone: a per-agent // English-phrase lookup table translating it first.
// queued-but-not-running badge (`queuedOpsByAgent()`, removed per "remove //
// the per agent pending stuff - only show what is running" — a card's // Agent is per-node, not per-DAG (a DAG can span agents — e.g. the
// pending badges are transients-only now, which already means "what is // startup sweep's MetaLock cascade, or a hive-wide restart), so this
// running") and the queue-summary banner (removed per "dont replace one // derives each agent's queued state from its own node(s) within the
// legacy thing with another" — a client-side derivation over the generic // entry rather than the DAG's overall `state`/`kind`.
// graph was itself judged a stopgap not worth shipping; the banner comes function queuedOpsByAgent() {
// back once the dedicated rollup endpoint exists, and reads that const out = new Map();
// directly instead of pulling the graph in here at all). 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);
}
}
return out;
}
// ─── transients ─────────────────────────────────────────────────────────────
export function syncTransientsFromSnapshot(s) { export function syncTransientsFromSnapshot(s) {
transientsState.clear(); transientsState.clear();
@ -300,14 +344,9 @@ function buildContainerLi(c, node, opts) {
askerCount, targetCount, agentQCount, askerCount, targetCount, agentQCount,
url, containerBase, forgeBase, s, url, containerBase, forgeBase, s,
} = opts; } = 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', { const li = el('li', {
class: 'container-row' class: 'container-row'
+ (pending.length ? ' pending' : '')
+ (opRunning ? ' pending-running' : '') + (opRunning ? ' pending-running' : '')
+ (selected ? ' selected' : ''), + (selected ? ' selected' : ''),
}); });
@ -583,15 +622,27 @@ export function renderContainers(s) {
)); ));
} }
// No queue-summary banner for now — the previous version derived it // Queue-summary banner: when the rebuild queue has active work,
// client-side from GET /api/jobq/graph, which mara flagged as // show one compact at-a-glance line + a link to the full queue on the
// replacing one legacy DagView-shaped hack with another rather than // C0R3 page. Replaces the old per-transient spinner list — the actual
// landing the real fix ("dont replace one legacy thing with another // running step is already visible per-agent on each card (transient +
// ... split it into what can and cannot be done now"). Comes back once // in-flight-queue badges), so the top of the tab only needs the summary.
// the dedicated rollup endpoint exists, reading that directly. Until const activeQueue = rebuildQueueState.filter(
// then the queue's actual state is still visible per-agent on each (e) => e.state === 'Pending' || e.state === 'Running',
// card via the transient pills; only the hive-wide at-a-glance summary );
// is missing. if (activeQueue.length) {
const running = activeQueue.filter((e) => e.state === 'Running').length;
const queued = activeQueue.length - running;
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 →'),
));
}
if (!containers.length && !transientsState.size) { if (!containers.length && !transientsState.size) {
root.append(el('p', { class: 'empty' }, 'no managed containers')); root.append(el('p', { class: 'empty' }, 'no managed containers'));
@ -621,6 +672,10 @@ export function renderContainers(s) {
const forgeBase = (s && s.forge_public_url) || null; const forgeBase = (s && s.forge_public_url) || null;
const ul = existingUl ?? el('ul', { class: 'containers' }); const ul = existingUl ?? el('ul', { class: 'containers' });
const tree = buildAgentTree(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 // Build the ordered list of <li> elements, reusing cached rows
// whose displayed state hasn't changed. // whose displayed state hasn't changed.
@ -634,17 +689,20 @@ export function renderContainers(s) {
const containerBase = gatewayLinks const containerBase = gatewayLinks
? `/agent/${encodeURIComponent(c.name)}` ? `/agent/${encodeURIComponent(c.name)}`
: `http://${hostname}:${c.port}`; : `http://${hostname}:${c.port}`;
// Pending-badge derivation: transients only — "what is running," // Pending-badge derivation: an agent's transients win outright when
// full stop (mara, on review: "remove the per agent pending stuff - // any exist (rendered one badge per pill — mara: "show all running
// only show what is running"; rendered one badge per pill — mara, // nodes that name the agent"), the rebuild-queue's queued-only
// earlier: "show all running nodes that name the agent"). See // fallback otherwise. See docs/web-ui.md::Container row.
// docs/web-ui.md::Container row.
const transientKindsMap = transientsState.get(c.name); const transientKindsMap = transientsState.get(c.name);
// Sorted for stable badge order across renders — Map iteration // Sorted for stable badge order across renders — Map iteration
// order is insertion order, which shifts as pills clear/re-add. // order is insertion order, which shifts as pills clear/re-add.
const pending = transientKindsMap const transientKinds = transientKindsMap
? Array.from(transientKindsMap.keys()).sort() : []; ? Array.from(transientKindsMap.keys()).sort() : [];
const opRunning = pending.length > 0; const queuedKind = transientKinds.length === 0 ? queuedOps.get(c.name) : null;
const pending = transientKinds.length > 0
? transientKinds
: (queuedKind ? [queuedKind] : []);
const opRunning = transientKinds.length > 0;
const selected = selectionState.has(c.name); const selected = selectionState.has(c.name);
// Pending questions where this agent is the asker (awaiting an // Pending questions where this agent is the asker (awaiting an
// answer) or the target (owes a reply). Derived live from // answer) or the target (owes a reply). Derived live from

View file

@ -42,8 +42,8 @@ import {
renderQuestions, activeQuestionCount, renderQuestions, activeQuestionCount,
} from './call.js'; } from './call.js';
import { import {
syncTransientsFromSnapshot, syncRebuildQueueFromSnapshot, syncTransientsFromSnapshot,
applyContainerStateChanged, applyContainerRemoved, applyRebuildQueueChanged, applyContainerStateChanged, applyContainerRemoved,
applyTransientSet, applyTransientCleared, applyTransientSet, applyTransientCleared,
renderContainers, renderContainersFromState, renderContainers, renderContainersFromState,
renderSelectionBar, renderPeerHives, renderSelectionBar, renderPeerHives,
@ -234,6 +234,9 @@ window.marked = marked;
// `transientsState` + `containersState`, not from `s.*`). // `transientsState` + `containersState`, not from `s.*`).
syncTransientsFromSnapshot(s); syncTransientsFromSnapshot(s);
syncContainersFromSnapshot(s); syncContainersFromSnapshot(s);
// Rebuild-queue state feeds the SW4RM agent-card badges
// (inFlightOpsByAgent); its own panel now lives on /core.html.
syncRebuildQueueFromSnapshot(s);
renderContainers(s); renderContainers(s);
// Sync the derived approvals + questions stores from the // Sync the derived approvals + questions stores from the
// snapshot, then render. Live `*_added` / `*_resolved` events // snapshot, then render. Live `*_added` / `*_resolved` events
@ -304,10 +307,8 @@ window.marked = marked;
container_removed: applyContainerRemoved, container_removed: applyContainerRemoved,
// tombstones_changed / meta_inputs_changed / meta_update_running are // tombstones_changed / meta_inputs_changed / meta_update_running are
// handled on /core.html now (the SYST3M panels moved there). // handled on /core.html now (the SYST3M panels moved there).
// rebuild_queue_changed: this page no longer subscribes to it at // rebuild_queue_changed stays: it refreshes the SW4RM badges.
// all (see swarm.js's transients-section comment) — the SW4RM tab rebuild_queue_changed: applyRebuildQueueChanged,
// has nothing left that reads it, unlike /builds.html which still
// does (its own separate subscription).
schedules_changed: applySchedulesChanged, schedules_changed: applySchedulesChanged,
capabilities_changed: applyCapabilitiesChanged, capabilities_changed: applyCapabilitiesChanged,
tool_groups_changed: applyToolGroupsChanged, tool_groups_changed: applyToolGroupsChanged,
@ -329,7 +330,7 @@ window.marked = marked;
const es = openStream( const es = openStream(
'/api/dashboard/stream?kinds=sent,approval_added,approval_resolved,' + '/api/dashboard/stream?kinds=sent,approval_added,approval_resolved,' +
'question_added,question_resolved,transient_set,transient_cleared,' + 'question_added,question_resolved,transient_set,transient_cleared,' +
'container_state_changed,container_removed,' + 'container_state_changed,container_removed,rebuild_queue_changed,' +
'schedules_changed,capabilities_changed,tool_groups_changed', 'schedules_changed,capabilities_changed,tool_groups_changed',
); );
es.onmessage = (e) => { es.onmessage = (e) => {