From a2540be98b771e59a53d887a7c0f0e13e929df13 Mon Sep 17 00:00:00 2001 From: iris Date: Wed, 14 Oct 2026 23:27:57 +0200 Subject: [PATCH 1/6] dashboard: the hive UI no longer shows container load The cpu, memory and disk figures are exported as OTEL metrics and graphed in Grafana, and the caps are set in the swarm UI, so the C0NT41N3R L04D tab on the C0R3 page only duplicated both. C0R3 is now the kept-state page alone, without a tab strip. --- docs/web-ui/dashboard.md | 52 +-- frontend/packages/dashboard/src/common.css | 4 +- frontend/packages/dashboard/src/core.css | 17 +- frontend/packages/dashboard/src/core.html | 77 ++--- frontend/packages/dashboard/src/core.js | 322 +----------------- frontend/packages/dashboard/src/dashboard.css | 6 +- .../packages/dashboard/src/dashboard.html | 2 +- frontend/packages/dashboard/src/index.html | 2 +- frontend/packages/dashboard/src/state.js | 3 +- frontend/packages/dashboard/src/stats.css | 3 +- .../dashboard/src/system-sections.css | 79 +---- 11 files changed, 52 insertions(+), 515 deletions(-) diff --git a/docs/web-ui/dashboard.md b/docs/web-ui/dashboard.md index b6c86cec..29ee1cfc 100644 --- a/docs/web-ui/dashboard.md +++ b/docs/web-ui/dashboard.md @@ -74,13 +74,17 @@ agent is stale. Passive / rare-interaction state, reached from the **Core** tile on the H0M3 hub (served at `/core.html`), with the same minimal chrome as `/builds.html`: a `← home` -back-link + a `createTabStrip` sub-tab nav (**K3PT ST4T3** default, -then **C0NT41N3R L04D**). The page is its own esbuild bundle (`core.js`) -that cold-loads `/api/state` and subscribes to `/api/dashboard/stream` for -`tombstones_changed`, `capabilities_changed`, and `tool_groups_changed` -(the latter two re-render the stale-perms sub-section when permission -data changes). (Rebuild queue and meta inputs live on the BU1LDS -page — see below.) +back-link and a single pane, **K3PT ST4T3**. The page is its own esbuild +bundle (`core.js`) that cold-loads `/api/state` and subscribes to +`/api/dashboard/stream` for `tombstones_changed`, `capabilities_changed`, +and `tool_groups_changed` (the latter two re-render the stale-perms +sub-section when permission data changes). (Rebuild queue and meta inputs +live on the BU1LDS page — see below.) + +Per-container CPU, memory and disk load is not shown in the hive UI. The +same figures are exported as OTEL metrics and graphed in Grafana (see +[observability](../scheduler/observability.md)). An agent's CPU and memory +caps are set per agent in the swarm UI. **K3PT ST4T3** — two sub-sections on one pane: @@ -102,32 +106,11 @@ snapshot-storage rework where the problem changes shape. *Stale permission entries*: agents with explicit capability or tool-group JSON entries but no live container — typically renamed or deleted agents whose entries persisted in `capabilities.json` / -`tool-groups.json`. Lazy-loaded on first K3PT ST4T3 tab activation; +`tool-groups.json`. Loaded when the page opens; autorefreshes on `capabilities_changed` + `tool_groups_changed` SSE events. Each ghost agent gets a `✕ clear perms` button (`DELETE /api/permissions/{name}`). -**C0NT41N3R L04D** — live CPU + memory per agent container, read -straight from cgroup v2 on the host (`cpu.stat`, `memory.current`, -`memory.peak`, `memory.max` under -`/sys/fs/cgroup/machine.slice/machine-h\x2d.scope/`). CPU is a -host-normalised percentage (0..100 across all cores) sampled over a -short (~200 ms) two-read interval; memory shows current + peak with a -bar against the `memory.max` quota. Backed by -`GET /api/container-resources` (`container_stats.rs`), which reads the -files read-only (world-readable; no `hive-priv`) and skips agents whose -scope dir is absent (= not running). Pull-only: `core.js` polls every -5 s **only while the C0NT41N3R L04D sub-tab is active** (CPU needs a fresh -sample each refresh), and stops on sub-tab change. Disk size (`disk_bytes`) -rides the same row, but a separate background sampler feeds it every -30 min: `hive-priv` runs one bounded `du -x` as root over the agent's -`state/` + `work/` + container writable rootfs (shared nix store excluded), -so the 5 s poll stays cheap cgroup-only reads. The row carries the last -measured value; the column shows "unknown" (`null`) before the first -measurement and whenever the last one failed, never 0. Network isn't tracked -per container — see `docs/networking/network.md` for the current -per-agent netns model. - ## BU1LDS page (`/builds.html`) The build lifecycle hub — rebuild queue, live build log, meta inputs, @@ -765,17 +748,6 @@ below — some endpoints aren't in it yet. zero-turn dbs); returns swarm totals, a busiest-first per-agent rollup, swarm model mix, and a labelled `est_cost_usd`. Window defaults to `24h`. -- `GET /api/container-resources` — live per-agent-container CPU + - memory from cgroup v2 (C0R3 › C0NT41N3R L04D panel). Returns one - row per running agent (`name`, `cpu_pct`, `mem_current_bytes`, - `mem_peak_bytes`, `mem_max_bytes`, `disk_bytes`); samples CPU over - ~200 ms so the call briefly awaits. Skips non-running agents (no scope - dir). No network field — agents share the host netns. `disk_bytes` is - the last value from a **separate slow sampler** (not this hot path): - every 30 min `hive-priv` runs a bounded `du -sxb` as root over the agent's - `state/` + `work/` + container writable rootfs, `-x` excluding the shared - read-only nix store (`PrivRequest::MeasureAgentDisk`). `null` means - unknown: not measured yet, or the last measurement failed. - `POST /api/cancel-reminder/{id}` — hard-delete a pending reminder. - `POST /api/retry-reminder/{id}` — re-arm a reminder whose delivery failed (clears the failure state so the scheduler retries). diff --git a/frontend/packages/dashboard/src/common.css b/frontend/packages/dashboard/src/common.css index 22b9f871..5d5d57ec 100644 --- a/frontend/packages/dashboard/src/common.css +++ b/frontend/packages/dashboard/src/common.css @@ -619,8 +619,8 @@ hive-side-panel .md td { /* ─── stats table ───────────────────────────────────────────────── Shared right-aligned numeric table. Used by /stats.html (busiest - agents) and the dashboard SYST3M › C0NT41N3R L04D table — moved here - from dashboard.css when ST4TS became its own page. */ + agents) — moved here from dashboard.css when ST4TS became its own + page. */ .hive-stats-table { width: 100%; border-collapse: collapse; diff --git a/frontend/packages/dashboard/src/core.css b/frontend/packages/dashboard/src/core.css index cb44415e..be613a1d 100644 --- a/frontend/packages/dashboard/src/core.css +++ b/frontend/packages/dashboard/src/core.css @@ -1,6 +1,7 @@ /* ─── /core.html — C0R3 page ─────────────────────────────────────── - Kept state (tombstones) and container load. Section rules imported - from system-sections.css (shared with dashboard.css + builds.css). */ + Kept state (tombstones) and stale permission entries. Section rules + imported from system-sections.css (shared with dashboard.css + + builds.css). */ @import "./system-sections.css"; body.core-shell { @@ -8,22 +9,10 @@ body.core-shell { padding: 0; } -/* Fill the header row next to the ← home back-link, same as the - builds sub-tab strip. */ -.core-tabbar { - flex: 1; -} - .core-main { padding: 1.2em 1.5em 2em; } -/* createTabStrip toggles the native `hidden` attribute on inactive - panes; ensure it wins over any inherited display. */ -.core-pane[hidden] { - display: none; -} - /* ─── K3PT ST4T3 container cards ──────────────────────────────────── core.html doesn't load dashboard.css (that's the operator SPA), so the .container-row card styles aren't inherited. Redefine them diff --git a/frontend/packages/dashboard/src/core.html b/frontend/packages/dashboard/src/core.html index 37210ac8..8728f020 100644 --- a/frontend/packages/dashboard/src/core.html +++ b/frontend/packages/dashboard/src/core.html @@ -13,70 +13,31 @@
- -
-

- kept state from previously tombstoned agents — recreating an agent - with the same name reuses it. -

-
-

loading…

-
-
-
- - -
-

- live cpu + memory per agent container, from cgroup v2 on the host. cpu - is % of total host capacity (all cores), sampled over ~200ms each - refresh; polled every 5s while this tab is open. network is omitted on - purpose — agents share the host netns, so there is no per-container - counter (see docs/networking/network.md). -

-
-

loading…

-
-
+ +

+ kept state from previously tombstoned agents — recreating an agent with + the same name reuses it. +

+
+

loading…

+
+
diff --git a/frontend/packages/dashboard/src/core.js b/frontend/packages/dashboard/src/core.js index b4be8b03..dec4a397 100644 --- a/frontend/packages/dashboard/src/core.js +++ b/frontend/packages/dashboard/src/core.js @@ -1,27 +1,20 @@ // C0R3 page entry (/core.html). The host/coordinator surface, its own -// standalone page so the dashboard tab strip stays lean. Two sub-tabs -// via (default = K3PT ST4T3): kept state (tombstones) -// and container load. +// standalone page so the dashboard tab strip stays lean: kept state +// (tombstones) and the stale permission entries. // -// Rebuild queue + meta inputs live at /builds.html (the build lifecycle -// hub), not here. This page is its own esbuild bundle with its own -// runtime: it cold-loads /api/state and subscribes to /dashboard/stream -// (the same broker event channel the dashboard uses), maintaining its -// own copy of the tombstones state. +// This page is its own esbuild bundle with its own runtime: it cold-loads +// /api/state and subscribes to /dashboard/stream (the same broker event +// channel the dashboard uses), maintaining its own copy of the tombstones +// state. import { $, form, openStream, initServerWarnings } from "./common.js"; import { el } from "@hive/shared/dom.js"; import { asyncBtn, bindAsyncForms } from "@hive/shared/forms.js"; -import "@hive/shared/hive-tab-strip.js"; // ─── derived state (own copies; this bundle has its own runtime) ────────── let tombstonesState = []; -// ContainerView list from the last /api/state snapshot — used by renderContainerLoad -// to splice in the configured cpu_quota/memory_max limits alongside live cgroup data. -let containersState = []; function syncFromSnapshot(s) { tombstonesState = (s.tombstones || []).slice(); - containersState = (s.containers || []).slice(); } // ─── kept state (tombstones) ────────────────────────────────────────────── @@ -187,273 +180,6 @@ async function fetchAndRenderStalePerms() { stalePermsLoaded = true; } -// ─── container load (live cgroup poll while the LOAD sub-tab is open) ────── -let containerLoadTimer = null; - -function cloadFmtBytes(n) { - if (!Number.isFinite(n) || n <= 0) return "0"; - const u = ["B", "KiB", "MiB", "GiB", "TiB"]; - let i = 0; - let v = n; - while (v >= 1024 && i < u.length - 1) { - v /= 1024; - i += 1; - } - return v.toFixed(v < 10 && i > 0 ? 1 : 0) + " " + u[i]; -} -function cloadMeter(pct) { - const p = Math.max(0, Math.min(100, pct)); - const cls = p >= 90 ? "hot" : p >= 70 ? "warn" : ""; - const m = document.createElement("span"); - m.className = "cload-meter"; - m.title = p.toFixed(0) + "%"; - const fill = document.createElement("span"); - fill.className = "fill" + (cls ? " " + cls : ""); - fill.style.width = p + "%"; - m.append(fill); - return m; -} - -// Stash the last load rows so re-renders triggered by SSE state updates can -// call renderContainerLoad(lastLoadRows) without waiting for the next poll. -let lastLoadRows = []; - -function renderContainerLoad(rows) { - lastLoadRows = rows; - const root = $("container-load-section"); - if (!root) return; - if (!Array.isArray(rows) || rows.length === 0) { - root.replaceChildren(); - const p = document.createElement("p"); - p.className = "meta"; - p.textContent = "no running agent containers"; - root.append(p); - return; - } - - // Build a name→ContainerView lookup so we can splice in the configured - // cpu_quota / memory_max limits (set in meta/resource-limits.json and - // resolved by the server) next to the live cgroup readings. - const cvByName = new Map(containersState.map((c) => [c.name, c])); - - const table = document.createElement("table"); - table.className = "hive-stats-table"; - // "cpu cap" / "mem cap" show the configured ceilings from ContainerView - // (effective drop-in values, take effect on next start/restart). - // "limit" remains the live cgroup memory ceiling from /api/container-resources. - table.innerHTML = - "agentcpumemory" + - "peaklimitdisk" + - 'cpu cap' + - 'mem cap' + - ""; - const tb = document.createElement("tbody"); - for (const r of rows) { - const cv = cvByName.get(r.name); - const tr = document.createElement("tr"); - const name = document.createElement("td"); - name.textContent = r.name; - tr.append(name); - const cpu = document.createElement("td"); - cpu.className = "num"; - cpu.append( - (Number(r.cpu_pct) || 0).toFixed(1) + "%", - cloadMeter(Number(r.cpu_pct) || 0), - ); - tr.append(cpu); - const mem = document.createElement("td"); - mem.className = "num"; - const memCur = Number(r.mem_current_bytes) || 0; - if (r.mem_max_bytes) { - mem.append( - cloadFmtBytes(memCur), - cloadMeter((100 * memCur) / r.mem_max_bytes), - ); - } else { - mem.textContent = cloadFmtBytes(memCur); - } - tr.append(mem); - const peak = document.createElement("td"); - peak.className = "num"; - peak.textContent = r.mem_peak_bytes - ? cloadFmtBytes(Number(r.mem_peak_bytes)) - : "—"; - tr.append(peak); - const lim = document.createElement("td"); - lim.className = "num"; - lim.textContent = r.mem_max_bytes - ? cloadFmtBytes(Number(r.mem_max_bytes)) - : "∞"; - tr.append(lim); - // Disk: on-disk footprint (state/ + work/ + container writable rootfs, shared - // nix store excluded). Measured out-of-band every 30 min server-side; - // null means unknown (not measured yet, or the measurement failed). - const disk = document.createElement("td"); - disk.className = "num"; - disk.title = - "state/ + work/ + container writable rootfs (shared nix store excluded); measured every 30 min"; - disk.textContent = - r.disk_bytes != null ? cloadFmtBytes(Number(r.disk_bytes)) : "unknown"; - tr.append(disk); - - // Configured CPU / memory caps from ContainerView (resolved effective - // values: per-agent override when set, hive-wide default otherwise). - const cpuCap = document.createElement("td"); - cpuCap.className = "num cload-cap"; - cpuCap.title = "configured ceiling — takes effect on next start"; - cpuCap.textContent = cv?.cpu_quota || "—"; - tr.append(cpuCap); - const memCap = document.createElement("td"); - memCap.className = "num cload-cap"; - memCap.title = "configured ceiling — takes effect on next start"; - memCap.textContent = cv?.memory_max || "—"; - tr.append(memCap); - - // S3T button toggles the inline edit row for this agent. - const actTd = document.createElement("td"); - const setBtn = document.createElement("button"); - setBtn.type = "button"; - setBtn.className = "btn btn-sm cload-set-btn"; - setBtn.textContent = "S3T"; - setBtn.title = "set CPU / memory cap for " + r.name; - tr.append(actTd); - actTd.append(setBtn); - tb.append(tr); - - // Inline edit row (hidden by default, toggled by the S3T button). - const editRow = document.createElement("tr"); - editRow.className = "cload-edit-row"; - editRow.hidden = true; - const editTd = document.createElement("td"); - editTd.colSpan = 9; - editTd.className = "cload-edit-cell"; - - const editForm = document.createElement("form"); - editForm.className = "cload-edit-form"; - editForm.addEventListener("submit", async (e) => { - e.preventDefault(); - const cpuInput = editForm.querySelector(".cload-cpu-input"); - const memInput = editForm.querySelector(".cload-mem-input"); - const errSpan = editForm.querySelector(".cload-edit-err"); - const submitBtn = editForm.querySelector('[type="submit"]'); - errSpan.hidden = true; - submitBtn.disabled = true; - try { - const body = new URLSearchParams({ - cpu_quota: cpuInput.value.trim(), - memory_max: memInput.value.trim(), - }); - const resp = await fetch( - "/api/resource-limits/" + encodeURIComponent(r.name), - { method: "POST", body }, - ); - if (!resp.ok) { - errSpan.textContent = await resp - .text() - .catch(() => "error " + resp.status); - errSpan.hidden = false; - return; - } - // Success: collapse the edit row. The SSE rescan will push updated - // ContainerView data (cpu_quota/memory_max) to containersState, - // triggering a re-render of the cap columns via refreshContainerLoad. - editRow.hidden = true; - setBtn.textContent = "S3T"; - } catch (err) { - errSpan.textContent = String(err); - errSpan.hidden = false; - } finally { - submitBtn.disabled = false; - } - }); - - const cpuLabel = document.createElement("label"); - cpuLabel.className = "cload-edit-label"; - cpuLabel.textContent = "cpu quota"; - const cpuInput = document.createElement("input"); - cpuInput.type = "text"; - cpuInput.className = "cload-cpu-input"; - // Pre-fill with the current *effective* value (not just a placeholder) - // so submitting after editing only the other field round-trips this one - // unchanged. The form always posts both fields, and the server treats - // an empty field as "clear this override" — a placeholder-only hint - // reads as filled to the eye but posts as empty, silently resetting - // whichever field the user didn't touch. - cpuInput.value = cv ? cv.cpu_quota : ""; - cpuInput.placeholder = "e.g. 200%"; - cpuInput.title = - 'systemd CPUQuota= value (e.g. "400%"). empty = use hive default'; - cpuLabel.append(cpuInput); - - const memLabel = document.createElement("label"); - memLabel.className = "cload-edit-label"; - memLabel.textContent = "mem max"; - const memInput = document.createElement("input"); - memInput.type = "text"; - memInput.className = "cload-mem-input"; - // Same reasoning as cpuInput above. - memInput.value = cv ? cv.memory_max : ""; - memInput.placeholder = "e.g. 8G"; - memInput.title = - 'systemd MemoryMax= value (e.g. "8G", "50%", "infinity"). empty = use hive default'; - memLabel.append(memInput); - - const submitBtn = document.createElement("button"); - submitBtn.type = "submit"; - submitBtn.className = "btn btn-restart cload-save-btn"; - submitBtn.textContent = "S4V3"; - - const hintSpan = document.createElement("span"); - hintSpan.className = "meta cload-edit-hint"; - hintSpan.textContent = "↺ restart to apply to a running container"; - - const errSpan = document.createElement("span"); - errSpan.className = "cload-edit-err"; - errSpan.hidden = true; - - editForm.append(cpuLabel, memLabel, submitBtn, hintSpan, errSpan); - editTd.append(editForm); - editRow.append(editTd); - tb.append(editRow); - - setBtn.addEventListener("click", () => { - const open = !editRow.hidden; - editRow.hidden = open; - setBtn.textContent = open ? "S3T" : "✕"; - }); - } - table.append(tb); - root.replaceChildren(table); -} - -async function refreshContainerLoad() { - try { - const resp = await fetch("/api/container-resources"); - if (!resp.ok) throw new Error("http " + resp.status); - renderContainerLoad(await resp.json()); - } catch (e) { - const root = $("container-load-section"); - if (root) { - root.replaceChildren(); - const p = document.createElement("p"); - p.className = "meta"; - p.textContent = "container load fetch failed: " + e; - root.append(p); - } - } -} -function startContainerLoadPolling() { - refreshContainerLoad(); - if (containerLoadTimer) return; - containerLoadTimer = setInterval(refreshContainerLoad, 5000); -} -function stopContainerLoadPolling() { - if (containerLoadTimer) { - clearInterval(containerLoadTimer); - containerLoadTimer = null; - } -} - // ─── render-all (cold load + any full re-render) ────────────────────────── function renderAll() { renderTombstones({ tombstones: tombstonesState }); @@ -465,19 +191,6 @@ const SSE_HANDLERS = { tombstonesState = (ev.tombstones || []).slice(); renderTombstones({ tombstones: tombstonesState }); }, - // When a container's state changes (e.g. after resource-limits update - // triggers rescan_containers_and_emit), update containersState in place - // so the cap columns in the LOAD table reflect the new configured values. - container_state_changed(ev) { - if (!ev.container) return; - const idx = containersState.findIndex((c) => c.name === ev.container.name); - if (idx >= 0) { - containersState[idx] = ev.container; - } else { - containersState.push(ev.container); - } - if (lastLoadRows.length) renderContainerLoad(lastLoadRows); - }, // Refresh the stale-perms sub-section when perm data changes (a ghost // was cleared, or perms were saved for an agent whose name collides). capabilities_changed(_ev) { @@ -511,33 +224,16 @@ async function init() { // to the bare `ok` response page. bindAsyncForms(() => refreshState()); - // Hash-routed sub-tab strip; default K3PT ST4T3. Container-load - // polling runs only while the LOAD sub-tab is open (cpu is a short - // two-sample read each refresh on the server). - document.getElementById("core-tabbar").configure({ - tabs: [ - { id: "kept", label: "K3PT ST4T3" }, - { id: "load", label: "C0NT41N3R L04D" }, - ], - defaultId: "kept", - onShow: (id) => { - if (id === "load") startContainerLoadPolling(); - else stopContainerLoadPolling(); - // Lazy-load stale-perms on first K3PT ST4T3 activation; always - // re-fetch on subsequent visits in case perms changed. - if (id === "kept") fetchAndRenderStalePerms(); - }, - }); - await refreshState(); + fetchAndRenderStalePerms(); // kinds= matches SSE_HANDLERS below verbatim — narrows this from all - // 13 wire kinds down to the 4 this page acts on. This page was one + // 13 wire kinds down to the 3 this page acts on. This page was one // of 4 unfiltered `/api/dashboard/stream` subscribers before this // (subscription discipline, part 1 of the dashboard-event-stream- // split issue). const es = openStream( - "/api/dashboard/stream?kinds=tombstones_changed,container_state_changed," + + "/api/dashboard/stream?kinds=tombstones_changed," + "capabilities_changed,tool_groups_changed", ); if (es) { diff --git a/frontend/packages/dashboard/src/dashboard.css b/frontend/packages/dashboard/src/dashboard.css index a02618b6..897dc467 100644 --- a/frontend/packages/dashboard/src/dashboard.css +++ b/frontend/packages/dashboard/src/dashboard.css @@ -2,7 +2,7 @@ it defines the CSS custom properties, typography, and shared component rules that this file depends on. */ -/* SYST3M panel rules (rebuild queue / meta inputs / container load) +/* SYST3M panel rules (rebuild queue / meta inputs / kept state) live in system-sections.css, shared with /core.html. A few are still referenced from the dashboard (`.rebuild-queue`), so the dashboard pulls them in here. */ @@ -771,6 +771,4 @@ body.dashboard-shell.has-selection { /* ST4TS moved to its own page (`/stats.html`): the window selector / summary chips / bars live in stats.css, and the - shared `.hive-stats-table` moved to common.css (the SYST3M › - C0NT41N3R L04D table in system-sections.css / on /core.html still - uses it). */ + shared `.hive-stats-table` moved to common.css. */ diff --git a/frontend/packages/dashboard/src/dashboard.html b/frontend/packages/dashboard/src/dashboard.html index 3445d881..2af668cc 100644 --- a/frontend/packages/dashboard/src/dashboard.html +++ b/frontend/packages/dashboard/src/dashboard.html @@ -103,7 +103,7 @@ When OTEL is enabled, **hive-c0re itself** also exports each agent -container's resource load — the same cgroup gauges shown on the dashboard -LOAD tab — to this hive's own collector, exactly like an agent does and with +container's resource load — the same cgroup gauges — +to this hive's own collector, exactly like an agent does and with no separate toggle. These come from the host, not the in-container Claude SDK, so they cover containers even when their agent is idle. @@ -312,7 +312,7 @@ exists, so off-the-shelf OTEL/Grafana container dashboards work. Resource | `hyperhive.container.memory.limit` | `By` | gauge | `memory.max` (custom — semconv has no `.limit` metric; omitted when unlimited) | | `hyperhive.container.memory.peak` | `By` | gauge | `memory.peak` (custom — no semconv metric; omitted if unavailable) | | `hyperhive.container.storage.usage` | `By` | gauge | `state/` + `work/` + writable rootfs, measured by `hive-priv` every 30 min (custom — semconv only has `disk.io`; omitted while unknown: not measured yet, or the last measurement failed) | -| `hyperhive.container.cpu.percent` | `%` | gauge | host-normalised percent (custom — the value the dashboard LOAD tab shows, no `rate()` needed) | +| `hyperhive.container.cpu.percent` | `%` | gauge | host-normalised percent (custom — no `rate()` needed) | The `hyperhive.`-prefixed metrics have no semconv equivalent (memory limit + peak, on-disk footprint, and an instantaneous cpu percent kept diff --git a/hive-c0re/src/agent_config/resource_limits.rs b/hive-c0re/src/agent_config/resource_limits.rs index 2adc3623..d9e058c9 100644 --- a/hive-c0re/src/agent_config/resource_limits.rs +++ b/hive-c0re/src/agent_config/resource_limits.rs @@ -19,7 +19,7 @@ //! absent field all fall back to the hive-wide //! `services.hyperhive.agentCpuQuota` / `agentMemoryMax`. So a swarm cap on //! memory alone leaves CPU on the hive default — see -//! [`effective_from_with_swarm`]. +//! [`effective_from`]. //! //! Read path: `lifecycle::host_config::write_dropins`, on every spawn //! and every rebuild. diff --git a/hive-c0re/src/main.rs b/hive-c0re/src/main.rs index 4ebfec67..c0ff3bed 100644 --- a/hive-c0re/src/main.rs +++ b/hive-c0re/src/main.rs @@ -404,9 +404,9 @@ async fn cmd_serve( // ~hundreds of KB, and the /stats + hive-stats views read it // directly; pruning it would just lose trend history for no gain.) // Slow per-container disk sampler: hive-priv measures each agent's state/ - // + work/ + writable rootfs every 30 min, cached so the 5s container-load - // poll stays cheap cgroup-only reads. Feeds `disk_bytes` on the LOAD - // tab. See container_stats::disk_sampler_loop. + // + work/ + writable rootfs every 30 min, cached so a sample only reads + // cgroup files. Feeds `disk_bytes` to the OTEL exporter. See + // container_stats::disk_sampler_loop. crate::container_stats::spawn_disk_sampler(); // Per-agent container-resource OTEL export: rides the same // cgroup gauges out to the configured OTLP endpoint, reusing the hive diff --git a/hive-c0re/src/stats/container_stats.rs b/hive-c0re/src/stats/container_stats.rs index 99b4a26a..0612b2e9 100644 --- a/hive-c0re/src/stats/container_stats.rs +++ b/hive-c0re/src/stats/container_stats.rs @@ -1,6 +1,6 @@ //! Live per-agent-container resource load (CPU + memory, plus the slow -//! disk sample from hive-priv in [`disk_sampler_loop`]) for the -//! dashboard. Reads cgroup v2 stat files for each running agent +//! disk sample from hive-priv in [`disk_sampler_loop`]) for the OTEL +//! exporter. Reads cgroup v2 stat files for each running agent //! machine directly — same privsep-clean posture as the turn-stats //! sqlite reads (`cpu.stat` / `memory.*` are world-readable; no //! `hive-priv` needed). Runs host-side in hive-c0re, where @@ -31,8 +31,8 @@ use utoipa::ToSchema; use crate::coordinator::Coordinator; -/// Sampling interval for the two CPU reads. Short enough to keep the -/// endpoint snappy, long enough that the delta isn't dominated by read +/// Sampling interval for the two CPU reads. Short enough to keep a sample +/// quick, long enough that the delta isn't dominated by read /// jitter. const CPU_SAMPLE: Duration = Duration::from_millis(200); @@ -72,8 +72,8 @@ pub struct ContainerResource { const DISK_SAMPLE_INTERVAL: Duration = Duration::from_mins(30); /// Last per-agent disk measurement, keyed by agent name; `None` is a failed -/// one. Written by [`disk_sampler_loop`], read by [`gather`] so the hot -/// poll never measures. +/// one. Written by [`disk_sampler_loop`], read by [`gather`], so a sample +/// never measures. fn disk_cache() -> &'static RwLock>> { static CACHE: OnceLock>>> = OnceLock::new(); CACHE.get_or_init(|| RwLock::new(HashMap::new())) @@ -100,7 +100,7 @@ fn disk_reading(name: &hive_types::Ident, measured: anyhow::Result) -> Opti /// Background loop: every [`DISK_SAMPLE_INTERVAL`], remeasure disk usage /// for every kept-state agent and refresh [`disk_cache`]. The first pass -/// runs immediately so the dashboard shows disk shortly after boot. +/// runs immediately so the exporter has disk shortly after boot. /// Runs forever; spawn once at hive-c0re startup via [`spawn_disk_sampler`]. pub async fn disk_sampler_loop() { loop { From 1a86304f922e0378e2b453f6389ad5c3a458d986 Mon Sep 17 00:00:00 2001 From: iris Date: Thu, 15 Oct 2026 00:17:00 +0200 Subject: [PATCH 5/6] hive-c0re: the OTEL exporter doc says it is what the container stats feed, not a second consumer --- hive-c0re/src/stats/otel_metrics.rs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/hive-c0re/src/stats/otel_metrics.rs b/hive-c0re/src/stats/otel_metrics.rs index bfb28f52..d5c394a3 100644 --- a/hive-c0re/src/stats/otel_metrics.rs +++ b/hive-c0re/src/stats/otel_metrics.rs @@ -1,6 +1,5 @@ -//! Per-agent container-resource OTEL export. hive-c0re already samples each -//! agent container's cgroup load for the dashboard -//! ([`super::container_stats`]); this rides those same gauges out to the +//! Per-agent container-resource OTEL export. This is what the cgroup load +//! sampled in [`super::container_stats`] feeds: it rides those gauges out to the //! hive's own collector — the same first hop the agents use, named by the //! standard `OTEL_EXPORTER_OTLP_ENDPOINT` (see `nix/host-modules/hive-c0re`, //! which sets it from the same binding it hands agents). No toggle of @@ -194,7 +193,7 @@ fn register_instruments(provider: &SdkMeterProvider, snapshot: Arc Date: Thu, 15 Oct 2026 04:47:01 +0200 Subject: [PATCH 6/6] docs: rewrite the container load and retired limits prose in active voice for the vale error rules --- docs/agent-lifecycle/persistence.md | 2 +- docs/scheduler/coordinator.md | 2 +- docs/web-ui/dashboard.md | 6 +++--- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/agent-lifecycle/persistence.md b/docs/agent-lifecycle/persistence.md index 7c0a3efe..1dbc2cb8 100644 --- a/docs/agent-lifecycle/persistence.md +++ b/docs/agent-lifecycle/persistence.md @@ -414,7 +414,7 @@ Contents: for them. - `resource-limits.json` — retired. Per-agent overrides set on the hive used to live here; nothing reads or writes it now. An old copy in the meta repo - is ignored and can be deleted. + is harmless and you can delete it. The root agent has the meta dir RO-mounted at `/meta/`. diff --git a/docs/scheduler/coordinator.md b/docs/scheduler/coordinator.md index 74500d3e..4634269f 100644 --- a/docs/scheduler/coordinator.md +++ b/docs/scheduler/coordinator.md @@ -460,7 +460,7 @@ The answer per agent and per field is, first match wins: A swarm cap replaces the hive's answer, so a hive can't raise an agent above it. Like the rest of the drop-in, a changed swarm cap reaches a running agent at its next spawn or rebuild. A hive has no way to set a cap -for one agent: caps are set per agent in the swarm UI. +for one agent: you set caps per agent in the swarm UI. The container can't read its own cap, so `write_dropins` also writes the effective `MemoryMax=` into the agent's runtime dir diff --git a/docs/web-ui/dashboard.md b/docs/web-ui/dashboard.md index 29ee1cfc..f65593e5 100644 --- a/docs/web-ui/dashboard.md +++ b/docs/web-ui/dashboard.md @@ -81,10 +81,10 @@ and `tool_groups_changed` (the latter two re-render the stale-perms sub-section when permission data changes). (Rebuild queue and meta inputs live on the BU1LDS page — see below.) -Per-container CPU, memory and disk load is not shown in the hive UI. The -same figures are exported as OTEL metrics and graphed in Grafana (see +Per-container CPU, memory and disk load isn't shown in the hive UI. The +same figures go out as OTEL metrics and appear in Grafana (see [observability](../scheduler/observability.md)). An agent's CPU and memory -caps are set per agent in the swarm UI. +caps live per agent in the swarm UI. **K3PT ST4T3** — two sub-sections on one pane: