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:
iris 2026-06-10 20:06:54 +02:00 committed by mara
commit 8db8bd610f
2 changed files with 43 additions and 0 deletions

View file

@ -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';