dashboard: add per-agent ctx-window usage chip to container rows
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<u64> 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
This commit is contained in:
parent
25659ee9f3
commit
270ef19920
3 changed files with 65 additions and 0 deletions
|
|
@ -550,6 +550,21 @@
|
||||||
},
|
},
|
||||||
`⏰ ${c.pending_reminders}`));
|
`⏰ ${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);
|
li.append(head);
|
||||||
|
|
||||||
// ── line 2: action buttons ───────────────────────────────────
|
// ── line 2: action buttons ───────────────────────────────────
|
||||||
|
|
|
||||||
|
|
@ -123,6 +123,20 @@ a:hover {
|
||||||
color: var(--cyan); border-color: var(--cyan);
|
color: var(--cyan); border-color: var(--cyan);
|
||||||
text-shadow: 0 0 6px rgba(137, 220, 235, 0.4);
|
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 {
|
.container-row.tombstone {
|
||||||
border-style: dashed;
|
border-style: dashed;
|
||||||
background: rgba(24, 24, 37, 0.35);
|
background: rgba(24, 24, 37, 0.35);
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
|
use rusqlite::Connection;
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
|
|
||||||
use crate::coordinator::Coordinator;
|
use crate::coordinator::Coordinator;
|
||||||
|
|
@ -36,6 +37,13 @@ pub struct ContainerView {
|
||||||
/// not real-time on remind/cancel-reminder but close enough.
|
/// not real-time on remind/cancel-reminder but close enough.
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub pending_reminders: u64,
|
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<u64>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Build the full container list. Wraps `lifecycle::list()` and
|
/// Build the full container list. Wraps `lifecycle::list()` and
|
||||||
|
|
@ -74,6 +82,7 @@ pub async fn build_all(coord: &Coordinator) -> Vec<ContainerView> {
|
||||||
.broker
|
.broker
|
||||||
.count_pending_reminders_for(reminder_recipient)
|
.count_pending_reminders_for(reminder_recipient)
|
||||||
.unwrap_or(0);
|
.unwrap_or(0);
|
||||||
|
let ctx_tokens = read_last_ctx_tokens(&logical);
|
||||||
out.push(ContainerView {
|
out.push(ContainerView {
|
||||||
port: lifecycle::agent_web_port(&logical),
|
port: lifecycle::agent_web_port(&logical),
|
||||||
running: lifecycle::is_running(&logical).await,
|
running: lifecycle::is_running(&logical).await,
|
||||||
|
|
@ -84,6 +93,7 @@ pub async fn build_all(coord: &Coordinator) -> Vec<ContainerView> {
|
||||||
needs_login,
|
needs_login,
|
||||||
deployed_sha,
|
deployed_sha,
|
||||||
pending_reminders,
|
pending_reminders,
|
||||||
|
ctx_tokens,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
out
|
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()))
|
.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<u64> {
|
||||||
|
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-<n>` → locked sha from meta's flake.lock. Used to
|
/// Map of `agent-<n>` → locked sha from meta's flake.lock. Used to
|
||||||
/// render the `deployed:<sha12>` chip per container row.
|
/// render the `deployed:<sha12>` chip per container row.
|
||||||
fn read_meta_locked_revs() -> HashMap<String, String> {
|
fn read_meta_locked_revs() -> HashMap<String, String> {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue