From 270ef199203a61e50106548d5155245e0aa26a57 Mon Sep 17 00:00:00 2001 From: iris Date: Wed, 20 May 2026 15:01:28 +0200 Subject: [PATCH 1/3] dashboard: add per-agent ctx-window usage chip to container rows MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reads the most recent turn's context-window token count directly from each agent's hyperhive-turn-stats.sqlite (same path the host-side stats_vacuum uses). Adds ctx_tokens: Option to ContainerView; populated in build_all via a single best-effort SQL query. Dashboard app.js renders a 'ctx·Nk' badge colour-coded by harness watermarks: green <100k (safe), yellow 100-150k (near auto-reset), red ≥150k (compact territory). Badge only shown when ctx_tokens is present (agent has run at least one turn). Closes #17 --- hive-c0re/assets/app.js | 15 ++++++++++++++ hive-c0re/assets/dashboard.css | 14 +++++++++++++ hive-c0re/src/container_view.rs | 36 +++++++++++++++++++++++++++++++++ 3 files changed, 65 insertions(+) diff --git a/hive-c0re/assets/app.js b/hive-c0re/assets/app.js index a186030..91c3a21 100644 --- a/hive-c0re/assets/app.js +++ b/hive-c0re/assets/app.js @@ -550,6 +550,21 @@ }, `⏰ ${c.pending_reminders}`)); } + if (c.ctx_tokens != null) { + // Colour thresholds mirror the harness compaction watermarks: + // < 100k = safe (green), 100k–150k = approaching reset (yellow), + // ≥ 150k = compact territory (red). + const k = Math.round(c.ctx_tokens / 1000); + const ctxClass = c.ctx_tokens >= 150_000 ? 'badge-ctx-warn' + : c.ctx_tokens >= 100_000 ? 'badge-ctx-caution' + : 'badge-ctx-ok'; + head.append(el('span', + { + class: `badge ${ctxClass}`, + title: `last turn context size: ${c.ctx_tokens.toLocaleString()} tokens`, + }, + `ctx·${k}k`)); + } li.append(head); // ── line 2: action buttons ─────────────────────────────────── diff --git a/hive-c0re/assets/dashboard.css b/hive-c0re/assets/dashboard.css index 7eaf67a..cb453c2 100644 --- a/hive-c0re/assets/dashboard.css +++ b/hive-c0re/assets/dashboard.css @@ -123,6 +123,20 @@ a:hover { color: var(--cyan); border-color: var(--cyan); text-shadow: 0 0 6px rgba(137, 220, 235, 0.4); } +/* Context-window usage badges on dashboard container rows. + Green < 100k, yellow 100–150k, red ≥ 150k (mirrors harness watermarks). */ +.badge-ctx-ok { + color: var(--green); border-color: var(--green); + opacity: 0.85; +} +.badge-ctx-caution { + color: var(--amber); border-color: var(--amber); + text-shadow: 0 0 6px rgba(250, 179, 135, 0.5); +} +.badge-ctx-warn { + color: var(--red); border-color: var(--red); + text-shadow: 0 0 6px rgba(243, 139, 168, 0.5); +} .container-row.tombstone { border-style: dashed; background: rgba(24, 24, 37, 0.35); diff --git a/hive-c0re/src/container_view.rs b/hive-c0re/src/container_view.rs index a946a88..7db7328 100644 --- a/hive-c0re/src/container_view.rs +++ b/hive-c0re/src/container_view.rs @@ -8,6 +8,7 @@ use std::collections::HashMap; use std::path::Path; +use rusqlite::Connection; use serde::Serialize; use crate::coordinator::Coordinator; @@ -36,6 +37,13 @@ pub struct ContainerView { /// not real-time on remind/cancel-reminder but close enough. #[serde(default)] pub pending_reminders: u64, + /// Context-window size (prompt tokens) from the agent's most recent + /// completed turn, read directly from the turn-stats SQLite. + /// `None` when the file is absent or the agent has no turns yet. + /// Stale by up to one crash-watch cycle (~10s); good enough for + /// the "which agent is close to the window?" dashboard glance. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub ctx_tokens: Option, } /// Build the full container list. Wraps `lifecycle::list()` and @@ -74,6 +82,7 @@ pub async fn build_all(coord: &Coordinator) -> Vec { .broker .count_pending_reminders_for(reminder_recipient) .unwrap_or(0); + let ctx_tokens = read_last_ctx_tokens(&logical); out.push(ContainerView { port: lifecycle::agent_web_port(&logical), running: lifecycle::is_running(&logical).await, @@ -84,6 +93,7 @@ pub async fn build_all(coord: &Coordinator) -> Vec { needs_login, deployed_sha, pending_reminders, + ctx_tokens, }); } out @@ -102,6 +112,32 @@ pub fn claude_has_session(dir: &Path) -> bool { .any(|e| e.file_type().is_ok_and(|t| t.is_file())) } +/// Read the most recent completed turn's context-window size (prompt +/// tokens) from the agent's turn-stats SQLite. Returns `None` when +/// the file is absent or has no rows. Best-effort — any DB error +/// silently yields `None` so a missing/corrupt file never blocks +/// `build_all`. +/// +/// Context tokens = `last_input_tokens + last_cache_read_input_tokens +/// + last_cache_creation_input_tokens`, mirroring +/// `hive_ag3nt::events::TokenUsage::context_tokens`. +fn read_last_ctx_tokens(name: &str) -> Option { + let path = Coordinator::agent_notes_dir(name).join("hyperhive-turn-stats.sqlite"); + let conn = Connection::open_with_flags( + &path, + rusqlite::OpenFlags::SQLITE_OPEN_READ_ONLY, + ) + .ok()?; + conn.query_row( + "SELECT last_input_tokens + last_cache_read_input_tokens + last_cache_creation_input_tokens \ + FROM turn_stats ORDER BY started_at DESC LIMIT 1", + [], + |row| row.get::<_, i64>(0), + ) + .ok() + .and_then(|v| u64::try_from(v).ok()) +} + /// Map of `agent-` → locked sha from meta's flake.lock. Used to /// render the `deployed:` chip per container row. fn read_meta_locked_revs() -> HashMap { From de13e800828efb37e3f59d7557a3c1649eca3b80 Mon Sep 17 00:00:00 2001 From: iris Date: Wed, 20 May 2026 15:01:35 +0200 Subject: [PATCH 2/3] screen: fix VeNCrypt security negotiation for weston VNC backend weston's VNC backend (neatvnc) uses VeNCrypt (security type 19) as the outer type even with --disable-transport-layer-security, offering sub-type 1 (None, no TLS, no password) within it. The old screen.html only handled type 1 (plain None) and type 2 (VNC auth), causing 'auth failed' against weston. Add VeNCrypt states to the RFB state machine: - vencrypt-version: echo back server's major.minor - vencrypt-subtypes: pick sub-type 1 (None) if available - vencrypt-accept: check server's acceptance byte Then falls through to the normal security-result / server-init path. --- hive-ag3nt/assets/screen.html | 57 ++++++++++++++++++++++++++++++++--- 1 file changed, 52 insertions(+), 5 deletions(-) diff --git a/hive-ag3nt/assets/screen.html b/hive-ag3nt/assets/screen.html index 810759c..8f076c8 100644 --- a/hive-ag3nt/assets/screen.html +++ b/hive-ag3nt/assets/screen.html @@ -167,21 +167,68 @@ canvas { display: block; cursor: default; } if (n === 0) { state = 'error'; return false; } const types = drainTo(n); if (!types) { chunks.unshift(b); totalBytes += 1; return false; } - // Prefer type 1 (None), else use first offered - const prefer = types.indexOf(1) !== -1 ? 1 : types[0]; + // Prefer type 1 (None), then type 19 (VeNCrypt — used by neatvnc/weston + // even with --disable-transport-layer-security), else first offered. + let prefer; + if (types.indexOf(1) !== -1) prefer = 1; // plain None + else if (types.indexOf(19) !== -1) prefer = 19; // VeNCrypt + else prefer = types[0]; send(new Uint8Array([prefer])); - state = prefer === 1 ? 'security-result' : 'security-vnc-challenge'; + if (prefer === 1) state = 'security-result'; + else if (prefer === 19) state = 'vencrypt-version'; + else state = 'security-vnc-challenge'; return true; } case 'security-vnc-challenge': { - // VNC auth: skip challenge bytes, respond with zeros (will fail, - // but we're in plain-RFB mode for hyperhive — see weston-vnc.nix) + // VNC auth (type 2): we don't have the password, so send zeros. + // This will fail for password-protected servers; fine for our + // weston VNC which uses None via VeNCrypt. const b = drainTo(16); if (!b) return false; send(new Uint8Array(16)); state = 'security-result'; return true; } + // ── VeNCrypt (type 19) sub-handshake ─────────────────────────────── + // neatvnc (weston VNC backend) uses VeNCrypt as the outer type even + // with --disable-transport-layer-security, offering sub-type 1 (None). + case 'vencrypt-version': { + // Server sends: major (u8), minor (u8) — e.g. 0, 2 + const b = drainTo(2); + if (!b) return false; + // Echo same version back + send(new Uint8Array([b[0], b[1]])); + state = 'vencrypt-subtypes'; + return true; + } + case 'vencrypt-subtypes': { + // Server sends: nSubtypes (u8), then nSubtypes × u32 sub-type ids + const nb = drainTo(1); + if (!nb) return false; + const nSub = nb[0]; + const raw = drainTo(nSub * 4); + if (!raw) { chunks.unshift(nb); totalBytes += 1; return false; } + // Build sub-type array from big-endian u32s + const subs = []; + for (let i = 0; i < nSub; i++) subs.push(u32be(raw, i * 4)); + // Prefer sub-type 1 (VeNCrypt None) — no TLS, no password. + // Fall back to first offered. + const sub = subs.includes(1) ? 1 : subs[0]; + // Send chosen sub-type as big-endian u32 + send(new Uint8Array([sub>>>24, (sub>>>16)&0xff, (sub>>>8)&0xff, sub&0xff])); + state = 'vencrypt-accept'; + return true; + } + case 'vencrypt-accept': { + // Server sends 1 byte: 1=accepted, 0=refused + const b = drainTo(1); + if (!b) return false; + if (b[0] !== 1) { setStatus('VeNCrypt sub-type refused', 'error'); return false; } + // Sub-type 1 (None): proceed to SecurityResult + state = 'security-result'; + return true; + } + // ────────────────────────────────────────────────────────────────── case 'security-result': { const b = drainTo(4); if (!b) return false; From 808b9cbe1a0e51495f80a8c102bad8da736baac1 Mon Sep 17 00:00:00 2001 From: iris Date: Wed, 20 May 2026 15:05:42 +0200 Subject: [PATCH 3/3] extract ctx token thresholds into named constants --- hive-c0re/assets/app.js | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/hive-c0re/assets/app.js b/hive-c0re/assets/app.js index 91c3a21..7a042e7 100644 --- a/hive-c0re/assets/app.js +++ b/hive-c0re/assets/app.js @@ -3,6 +3,13 @@ // and tails the unified dashboard event channel over `/dashboard/stream`. (() => { + // ─── constants ────────────────────────────────────────────────────────── + // Context-window token thresholds — mirror the harness compaction watermarks + // in hive-ag3nt (HIVE_COMPACT_WATERMARK_TOKENS = 150k; auto-reset at 100k). + // TODO: source these from model metadata once damocles lands that feature. + const CTX_WARN_TOKENS = 150_000; // ≥ this → compact territory (red) + const CTX_CAUTION_TOKENS = 100_000; // ≥ this → approaching reset (yellow) + // ─── helpers ──────────────────────────────────────────────────────────── const $ = (id) => document.getElementById(id); const esc = (s) => String(s).replace(/[&<>"]/g, (c) => @@ -551,12 +558,9 @@ `⏰ ${c.pending_reminders}`)); } if (c.ctx_tokens != null) { - // Colour thresholds mirror the harness compaction watermarks: - // < 100k = safe (green), 100k–150k = approaching reset (yellow), - // ≥ 150k = compact territory (red). const k = Math.round(c.ctx_tokens / 1000); - const ctxClass = c.ctx_tokens >= 150_000 ? 'badge-ctx-warn' - : c.ctx_tokens >= 100_000 ? 'badge-ctx-caution' + const ctxClass = c.ctx_tokens >= CTX_WARN_TOKENS ? 'badge-ctx-warn' + : c.ctx_tokens >= CTX_CAUTION_TOKENS ? 'badge-ctx-caution' : 'badge-ctx-ok'; head.append(el('span', {