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;
diff --git a/hive-c0re/assets/app.js b/hive-c0re/assets/app.js
index a186030..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) =>
@@ -550,6 +557,18 @@
},
`⏰ ${c.pending_reminders}`));
}
+ if (c.ctx_tokens != null) {
+ const k = Math.round(c.ctx_tokens / 1000);
+ 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',
+ {
+ 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 {