feat(stats): surface first-turn ctx tokens on the per-agent stats page
Builds the read/surface half of the per-session first-turn-tokens metric (the capture — sessions table + turn_stats.session_id — landed separately). A fresh claude session's first turn pays the full static prefix (system prompt + tools + CLAUDE.md + first wake) as uncached input, so its input_tokens is a clean proxy for prompt / CLAUDE.md sprawl — watching it over time surfaces creep. - stats.rs: add `Snapshot.first_turn_ctx: Option<u64>` populated by `read_first_turn_ctx` — the agreed per-session derive (first turn, `ORDER BY started_at LIMIT 1`, of the most recent session that started in the window). Inert-until-capture: `.ok()` maps both "no fresh session yet" and "older db without the sessions table" to None, the same decoupling as read_bash_breakdown; the field is skipped from the JSON when None. Pre-capture rows have a NULL session_id and are excluded. - agent stats.js: add a "first-turn ctx" summary chip, guarded on a numeric value so it stays hidden until capture has data. clippy + cargo fmt clean; agent bundle builds.
This commit is contained in:
parent
62c41baf63
commit
8db8bd610f
2 changed files with 43 additions and 0 deletions
|
|
@ -123,6 +123,13 @@ window.Chart = Chart;
|
|||
['reminders pending', fmtInt(s.reminder_stats.pending)],
|
||||
);
|
||||
}
|
||||
// First-turn ctx: input tokens of the most recent fresh session's
|
||||
// first turn — the cold system-prompt + CLAUDE.md cost, a sprawl
|
||||
// proxy. Omitted from the JSON (and so absent here) until the
|
||||
// per-session capture has data.
|
||||
if (typeof s.first_turn_ctx === 'number') {
|
||||
chips.push(['first-turn ctx', fmtInt(s.first_turn_ctx)]);
|
||||
}
|
||||
for (const [label, value] of chips) {
|
||||
const chip = document.createElement('span');
|
||||
chip.className = 'chip';
|
||||
|
|
|
|||
|
|
@ -112,6 +112,14 @@ pub struct Snapshot {
|
|||
/// None if the RPC call failed or hasn't been integrated yet.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub reminder_stats: Option<ReminderStats>,
|
||||
/// First-turn input tokens of the most recent fresh claude session
|
||||
/// that started in the window — a proxy for system-prompt + CLAUDE.md
|
||||
/// sprawl (a fresh session's first turn pays the full static prefix
|
||||
/// uncached, so this is the current "cold context" cost). `None` until
|
||||
/// the sessions capture (per-session `session_id`) has data; every
|
||||
/// pre-capture `turn_stats` row has a NULL `session_id` and is excluded.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub first_turn_ctx: Option<u64>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
|
|
@ -189,6 +197,7 @@ fn empty_snapshot(window: Window) -> Snapshot {
|
|||
models: Vec::new(),
|
||||
duration_summary: DurationSummary::default(),
|
||||
reminder_stats: None,
|
||||
first_turn_ctx: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -290,9 +299,36 @@ fn snapshot(path: &Path, window: Window) -> Result<Snapshot> {
|
|||
models,
|
||||
duration_summary,
|
||||
reminder_stats: None, // filled in by api_stats in web_ui.rs via fetch_reminder_stats RPC
|
||||
// Inert-until-capture: `.ok()` maps both "no fresh session in the
|
||||
// window yet" (QueryReturnedNoRows) and "sessions table absent on
|
||||
// an older db" (Err) to None, same decoupling as read_bash_breakdown.
|
||||
first_turn_ctx: read_first_turn_ctx(&conn, from).ok(),
|
||||
})
|
||||
}
|
||||
|
||||
/// First-turn input tokens of the most recent fresh claude session that
|
||||
/// started in `[from, now]`. Tracks system-prompt + CLAUDE.md sprawl:
|
||||
/// the first turn of a fresh session (`--continue` suppressed) pays the
|
||||
/// full static prefix as uncached input, so watching this over time
|
||||
/// surfaces creep. Uses the agreed per-session derive — the first turn
|
||||
/// (`ORDER BY started_at LIMIT 1`) of the latest session row.
|
||||
///
|
||||
/// Returns `Err` when the `sessions` table doesn't exist (older db) or no
|
||||
/// fresh session in the window has a recorded turn yet; the caller maps
|
||||
/// that to `None` (inert-until-capture), same as `read_bash_breakdown`.
|
||||
fn read_first_turn_ctx(conn: &Connection, from: i64) -> rusqlite::Result<u64> {
|
||||
conn.query_row(
|
||||
"SELECT input_tokens FROM turn_stats
|
||||
WHERE session_id = (
|
||||
SELECT id FROM sessions WHERE started_at >= ?1 ORDER BY started_at DESC LIMIT 1
|
||||
)
|
||||
ORDER BY started_at ASC
|
||||
LIMIT 1",
|
||||
[from],
|
||||
|row| row.get::<_, i64>(0).map(u64_from_i64),
|
||||
)
|
||||
}
|
||||
|
||||
/// Aggregate the top shell-command heads ("favorite tools") over
|
||||
/// `[from, now]` from the `bash_commands` table — one row per bash task
|
||||
/// (`ts INTEGER NOT NULL, head TEXT NOT NULL`), written by hive-bash-mcp.
|
||||
|
|
|
|||
Loading…
Reference in a new issue