Skip to content

Usage history: hourly totals in usage.db, served at /summary/history - #380

Merged
fylorn merged 2 commits into
mainfrom
feat/usage-history
Oct 28, 2026
Merged

fylorn merged 2 commits into
mainfrom
feat/usage-history

Conversation

@fylorn

@fylorn fylorn commented Oct 28, 2026 •

Copy link
Copy Markdown
Contributor

What this changes

Core keeps hourly usage totals in a second database file, usage.db, and serves them at GET /summary/history (BucketQuery → UsageHistory { since_ms, detail_since_ms, buckets }). The buckets are sparse and use the same bucketing and fields as /summary/buckets; the first-token fields are not kept. Control API 55.

Why

A long-range usage view (a year) cannot be read from the request records: they are kept for the retention period only, and data.db is rebuilt whenever its schema changes. The hourly totals live in their own file with their own user_version, so neither the rebuild nor pruning removes them.

  • One row per UTC hour: requests, failed, exact and estimated cost, unpriced and no-usage counts, sent and received bytes, and the four token kinds.
  • usage.db is attached to the request database's connection. When a request settles, its final row is added to its hour with INSERT … SELECT … ON CONFLICT DO UPDATE, using the same SQL list as /summary/buckets (TALLY). Locally answered and locally estimated rows are excluded, as in the summary. Each request is added once, in settle.
  • A new usage.db is filled from the request rows already kept; since_ms is the oldest of them, including local answers, or the creation time when there are none. It is not refilled later.
  • An older usage.db is rebuilt the same way. A newer or unreadable one is left untouched: request recording continues and only the history query fails (control.records_unreadable). A failed write is only logged.
  • detail_since_ms is the start of the oldest request row (local answers included, as in RouteStats::covered_since_ms), or none when there are no rows. Views computed from the request records show zeros before it, so a client can tell which days of the history it can open in detail. It moves forward with pruning; since_ms does not.
  • usage.db is created 0600 before SQLite opens it, like data.db, and its WAL siblings are restricted too.

How it was verified

  • New tests in tw-store (usage::tests and the recorder): an hour matches /summary/buckets and the summary field by field; each cost state accumulates separately; a request written twice is added once; a second ending adds nothing; local answers and locally estimated rows are not added; a new usage.db is filled from existing rows with the right since_ms; an empty store starts since_ms at creation; the history and its since_ms survive a data.db rebuilt for an old schema; pruning keeps the history; bucketing counts from from, crosses hours and stops at to; older, newer and broken usage.db files; detail_since_ms follows the oldest row as pruning removes rows while since_ms stays; both files and their WAL siblings are 0600.
  • tw-control: /summary/history end to end, including backfill, defaults and the bucket floor.
  • cargo fmt --all --check, cargo clippy --workspace --all-targets -D warnings, the ts feature clippy and tests, the TypeScript export, cargo test --workspace, and scripts/smoke.sh (86 checks, including the new usage.db mode check and the endpoint with a window).

Notes for review

  • Hours are the unit. A from_ms that is not on the hour puts each hour in the bucket where the hour starts.
  • A request is added to the hour in which it started, at the moment it settles; requests still running are not in the history yet.
  • Request rows written by a core without this change (for example after a downgrade) are not added later; only a new usage.db is filled from existing rows.

🤖 Generated with Claude Code

fylorn and others added 2 commits October 28, 2026 22:58
- tw-store: a second database file, usage.db, next to data.db (created
  0600 the same way) and attached to the same connection. One row per
  UTC hour with the twelve bucket totals: requests, failed, the three
  cost states, traffic and the four token kinds. It has its own
  user_version, so a data.db rebuilt for a schema change keeps it, and
  pruning request rows does not touch it.
- The recorder adds a request to its hour once, right after its final
  row is written in settle. Locally answered and locally estimated rows
  are excluded, as in the summary. The sums come from the same SQL list
  that /summary/buckets now uses (TALLY), so the two cannot drift.
- A new usage.db is filled from the request rows already kept, and
  since_ms records the oldest of them (or the moment it was created when
  there are none). An older usage.db is rebuilt the same way; a newer or
  unreadable one is left alone, request recording carries on, and only
  the history query fails.
- tw-api: UsageHistory { since_ms, buckets } and the UsageHistory
  endpoint, GET /summary/history with BucketQuery. Control API 55.
- tw-control: the handler takes the same defaults and bucket floor as
  /summary/buckets.
- Docs list usage.db in the data directory; the smoke test checks its
  mode and calls the endpoint with a window.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
- UsageHistory gains detail_since_ms: the start of the oldest request
  row, local answers included, the same measure as
  RouteStats::covered_since_ms. None when there are no request rows.
  It moves forward as the retention period prunes old rows, while
  since_ms, the start of the usage history, does not change.
- Views computed from the request records show zeros before that
  moment, so a client can tell which days in the history it can open
  in detail.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
@fylorn
fylorn merged commit e6ac787 into main Oct 28, 2026
6 checks passed
@fylorn
fylorn deleted the feat/usage-history branch October 28, 2026 15:58
@github-actions github-actions Bot locked and limited conversation to collaborators Oct 28, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant