0051 — Context-window meter per chat #131

Merged
chad merged 7 commits from glen/context-meter into main 2026-10-18 22:30:28 -04:00
Collaborator

Implements docs/0051-context-meter.md (approved plan v1 in the ticket thread): ROADMAP item 7, the per-chat context-window meter — a percentage-first chip in the chat header ("oops, context is at 80% — wrap this up", per Chad).

What lands

  • Capture — migration 0056_context_meter.sql adds last_input_tokens/last_cached_tokens/last_output_tokens/last_reasoning_tokens (nullable) to usage_events; turn.ts keeps the turn's LAST slice's counts beside the summed usage and recordUsageStep persists them. The last slice's input re-sent the whole conversation, so lastInput+lastOutput is the conversation's size in tokens. Reasoning is NOT added on top (schema convention: it's a display-only slice of output).
  • Read — readContextUsage (SQL ORDER BY ended_at DESC LIMIT 1, pre-0051 rows excluded outright); readConversationStats gains context {usedTokens, windowTokens, pct} for THIS conversation (never the group), window resolved through the model catalogs. Null → hidden when unmetered or the model is unknown.
  • UI — components/context-meter.tsx: mono percent + thin fraction bar in the chat header, warning ≥85%, destructive ≥95%, exact numbers in the tooltip; refetches on turn settle with a delayed follow-up (the usage row lands after the stream finishes). The stats drawer gains a fuller readout section.
  • Docs — 0051 design doc; AGENTS.md UI bullet line; ROADMAP item 7 marked done.

Verification

  • Gate: npm run typecheck && npm run lint && npm run test — tsc clean, eslint at exactly the 2 pre-existing warnings, 774 passed / 13 skipped (11 new tests: store latest-row/pre-0051/harness exclusion, turn single- vs multi-slice last-slice pin via the steer mock, API present/absent/clamp-at-1/zero-token/unknown-model/tie-break).
  • Migration: fresh-db and main(0055)→branch(0056) upgrade paths both verified, PRAGMA foreign_key_check clean. The upgrade-path check caught a real bug pre-commit: the 0056 journal entry needs when > 0055's future-dated timestamp (drizzle applies only entries newer than the last applied) — the first version silently skipped.
  • Visual QA: preview with seeded metered rows — 92% warning-zone chip, drawer readout 117,600 / 128,000 tokens, post-fix 50% neutral chip; screenshots in the ticket thread.

Fresh-eyes review (fix-first verdict, no blockers)

Both should-fixes taken:

  1. Settle-race — the chip's settle-edge fetch raced the usage write (stream finish fires before recordUsageStep); a one-shot 1.5s follow-up fetch after a turn ran closes it. (Lint then caught my ref-during-render — moved into an effect.)
  2. Doc double-count trap — 0051's semantics paragraph read as adding reasoningTokens; fixed the wording (code was already right).

Nits taken: SQL pick with isNotNull (dead tie-break code gone), pct comment matches reality, drawer copy says "that turn's model's window", plus 3 new guard tests (clamp/zero/latest-row-vs-newer-pre-0051). Nits declined: shared formatTokens helper (two components, different display intents — refactor when a third appears).

Deliberately out of scope (0051 records the decisions): compaction/fresh-thread affordances (warn-only v1), backfill (forward-only per 0047), per-harness-call meters.

Implements docs/0051-context-meter.md (approved plan v1 in the ticket thread): ROADMAP item 7, the per-chat context-window meter — a percentage-first chip in the chat header ("oops, context is at 80% — wrap this up", per Chad). ## What lands - **Capture** — migration `0056_context_meter.sql` adds `last_input_tokens/last_cached_tokens/last_output_tokens/last_reasoning_tokens` (nullable) to `usage_events`; `turn.ts` keeps the turn's LAST slice's counts beside the summed usage and `recordUsageStep` persists them. The last slice's input re-sent the whole conversation, so lastInput+lastOutput is the conversation's size in tokens. Reasoning is NOT added on top (schema convention: it's a display-only slice of output). - **Read** — `readContextUsage` (SQL `ORDER BY ended_at DESC LIMIT 1`, pre-0051 rows excluded outright); `readConversationStats` gains `context {usedTokens, windowTokens, pct}` for THIS conversation (never the group), window resolved through the model catalogs. Null → hidden when unmetered or the model is unknown. - **UI** — `components/context-meter.tsx`: mono percent + thin fraction bar in the chat header, warning ≥85%, destructive ≥95%, exact numbers in the tooltip; refetches on turn settle with a delayed follow-up (the usage row lands after the stream finishes). The stats drawer gains a fuller readout section. - **Docs** — 0051 design doc; AGENTS.md UI bullet line; ROADMAP item 7 marked done. ## Verification - Gate: `npm run typecheck && npm run lint && npm run test` — tsc clean, eslint at exactly the 2 pre-existing warnings, **774 passed / 13 skipped** (11 new tests: store latest-row/pre-0051/harness exclusion, turn single- vs multi-slice last-slice pin via the steer mock, API present/absent/clamp-at-1/zero-token/unknown-model/tie-break). - Migration: fresh-db and main(0055)→branch(0056) upgrade paths both verified, `PRAGMA foreign_key_check` clean. **The upgrade-path check caught a real bug pre-commit**: the 0056 journal entry needs `when` > 0055's future-dated timestamp (drizzle applies only entries newer than the last applied) — the first version silently skipped. - Visual QA: preview with seeded metered rows — 92% warning-zone chip, drawer readout `117,600 / 128,000 tokens`, post-fix 50% neutral chip; screenshots in the ticket thread. ## Fresh-eyes review (fix-first verdict, no blockers) Both should-fixes taken: 1. **Settle-race** — the chip's settle-edge fetch raced the usage write (stream `finish` fires before `recordUsageStep`); a one-shot 1.5s follow-up fetch after a turn ran closes it. (Lint then caught my ref-during-render — moved into an effect.) 2. **Doc double-count trap** — 0051's semantics paragraph read as adding reasoningTokens; fixed the wording (code was already right). Nits taken: SQL pick with `isNotNull` (dead tie-break code gone), pct comment matches reality, drawer copy says "that turn's model's window", plus 3 new guard tests (clamp/zero/latest-row-vs-newer-pre-0051). Nits declined: shared `formatTokens` helper (two components, different display intents — refactor when a third appears). Deliberately out of scope (0051 records the decisions): compaction/fresh-thread affordances (warn-only v1), backfill (forward-only per 0047), per-harness-call meters.
ROADMAP item 7. Last-slice tokens on the existing usage_events row,
context block on the stats read path, percentage-first chip in the
chat header. Warn-only near the limit; forward-only metering.
Migration 0056 adds last_input/cached/output/reasoning_tokens
(nullable, forward-only per 0047). turn.ts keeps the last slice's
counts beside the summed turnUsage; recordUsageStep persists them.
readContextUsage reads the conversation's latest metered chat row.
readConversationStats gains context {usedTokens, windowTokens, pct}
from the conversation's own latest metered chat row (never the
group's) against the catalog window. Null when unmetered/unknown.
The percentage leads (Chad's call): mono % with a thin fraction bar
in the chat header, warning tone at 85%, destructive at 95%. The
drawer gains a fuller readout. Hidden until metered turns exist.
Store: readContextUsage latest-row pick, pre-0051 rows skipped,
harness rows never answer. Turn: single-slice equals last-slice;
multi-slice records the LAST slice, not the sum (steer mock, two
slices, one executor per file — the queue targets one base URL).
API: 0.8 at 102.4k/128k; null when unmetered/pre-0051/unknown model.

Journal fix caught by the upgrade-path check: the 0056 entry needed
drizzle's real shape (breakpoints:true, no sql key) and a `when`
above 0055's future-dated timestamp — the migrator applies only
entries with when > last applied. Verified main→branch applies.
fix(usage): review fixes — settle-race follow-up, SQL pick, doc double-count trap
All checks were successful
deploy / checks (pull_request) Successful in 42s
deploy / deploy (pull_request) Has been skipped
c96fb3cae7
Fresh-eyes review (fix-first verdict; no blockers):

1. Settle-edge fetch raced the usage write (stream finish fires BEFORE
   recordUsageStep): one delayed 1.5s follow-up fetch after a turn ran.
2. Doc said to ADD reasoningTokens; per the schema's own convention
   reasoning is a display-only slice of outputTokens (already inside) —
   doc fixed, code unchanged.
3. readContextUsage: SQL ORDER BY ended_at DESC LIMIT 1 with
   isNotNull(lastInputTokens) — pre-0051 rows excluded outright, no
   dead tie-break code.
4. pct comment matches reality (clamped, unrounded); drawer copy says
   'that turn's model's window' (accurate after a route switch).
5. Tests: clamp at 1 over window, zero-token null, latest-row pick
   with a newer pre-0051 row never winning.

Lint caught the ref-during-render my first fix had — now in an effect.
Gate: tsc clean, eslint at the 2 pre-existing warnings, 774 passed.
chad approved these changes 2026-10-18 22:30:22 -04:00
chad merged commit 5f24ec5746 into main 2026-10-18 22:30:28 -04:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
chad/sweatshop-legacy!131
No description provided.