Repository navigation
Usage history: hourly totals in usage.db, served at /summary/history - #380
Merged
Merged
Conversation
- 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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to subscribe to this conversation on GitHub.
Already have an account?
Sign in.
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What this changes
Core keeps hourly usage totals in a second database file,
usage.db, and serves them atGET /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.dbis rebuilt whenever its schema changes. The hourly totals live in their own file with their ownuser_version, so neither the rebuild nor pruning removes them.usage.dbis attached to the request database's connection. When a request settles, its final row is added to its hour withINSERT … 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, insettle.usage.dbis filled from the request rows already kept;since_msis the oldest of them, including local answers, or the creation time when there are none. It is not refilled later.usage.dbis 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_msis the start of the oldest request row (local answers included, as inRouteStats::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_msdoes not.usage.dbis created0600before SQLite opens it, likedata.db, and its WAL siblings are restricted too.How it was verified
tw-store(usage::testsand the recorder): an hour matches/summary/bucketsand 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 newusage.dbis filled from existing rows with the rightsince_ms; an empty store startssince_msat creation; the history and itssince_mssurvive adata.dbrebuilt for an old schema; pruning keeps the history; bucketing counts fromfrom, crosses hours and stops atto; older, newer and brokenusage.dbfiles;detail_since_msfollows the oldest row as pruning removes rows whilesince_msstays; both files and their WAL siblings are0600.tw-control:/summary/historyend to end, including backfill, defaults and the bucket floor.cargo fmt --all --check,cargo clippy --workspace --all-targets -D warnings, thetsfeature clippy and tests, the TypeScript export,cargo test --workspace, andscripts/smoke.sh(86 checks, including the newusage.dbmode check and the endpoint with a window).Notes for review
from_msthat is not on the hour puts each hour in the bucket where the hour starts.usage.dbis filled from existing rows.🤖 Generated with Claude Code