Serve range aggregation from the durable ledger - #103
Conversation
Schema v3 adds durable_tool_events and durable_finding_events as replaceable per-session materializations, written behind the existing snapshot-hash gate and backfilled from current snapshots during migration so historical sessions are queryable immediately. HistoryStore::range_totals_multi computes window rollups entirely from the ledger — token buckets via indexed SQL aggregation, tool metrics by replaying the same telemetry accumulator over in-window fact rows, finding summaries with the in-memory window semantics — and golden tests assert field-for-field equality with Session::range_totals_multi across adversarial windows (boundary-exact timestamps, split mutation retries, undated findings, empty and open windows). Co-Authored-By: Claude Fable 5 <[email protected]>
The range command now aggregates from the history store's normalized facts instead of walking full in-memory sessions. Sessions whose latest persist failed are tracked and computed from memory and merged in, a missing store falls back wholesale, and a failed ledger query recomputes everything in memory — answers stay complete in every degraded mode. The perf event records source=ledger|mixed|memory so recordings can verify which path served each request. Co-Authored-By: Claude Fable 5 <[email protected]>
The metadata-overlay path no longer writes fact tables: an overlay whose history diverges from the durable snapshot durably marks the session ledger-dirty instead, hydrate restores those markings into the stale set across restarts, and a successful observe realigns facts and clears the flag. Aggregation now fetches each session's facts once over the union span on a dedicated read connection (WAL keeps the writer unblocked) and evaluates every window through a session shell running the same range_totals_multi as the in-memory path, eliminating semantic divergence by construction. The in-memory oracle compares timestamps at millisecond precision to match the ledger's storage granularity, the migration backfill streams snapshots instead of materializing them all, finding-row decode errors propagate into the wholesale fallback, and a failed ledger query reports source=fallback distinctly from store-absent. New tests: append-path golden equality, overlay dirty-marking lifecycle, and a sub-millisecond boundary event. Co-Authored-By: Claude Fable 5 <[email protected]>
Co-Authored-By: Claude Fable 5 <[email protected]>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 90011d340e
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| self.tokens_history | ||
| .partition_point(|event| event.timestamp < *from) | ||
| self.tokens_history.partition_point(|event| { | ||
| event.timestamp.timestamp_millis() < from.timestamp_millis() |
There was a problem hiding this comment.
Preserve sub-millisecond range boundaries
When an RFC3339 bound contains sub-millisecond precision, flooring both sides changes the inclusive comparison: for example, an event at 00:00:00.0001Z is incorrectly included by a lower bound of 00:00:00.0009Z, and the analogous upper-bound case includes events after the requested instant. This affects both ledger-backed requests and every other caller of range_totals_multi; retain exact DateTime comparisons and persist enough timestamp precision for the ledger to apply the same semantics.
AGENTS.md reference: AGENTS.md:L40-L40
Useful? React with 👍 / 👎.
Part of #38 (the query-service half) and #97 (prerequisite for the memory cap). Wave 2a of the consolidated plan.
sessions_in_ranges— the source of every cost/usage number in the UI — now aggregates from the history store's normalized facts instead of walking full in-memory sessions.durable_tool_eventsanddurable_finding_eventsfact tables, written behind the observe path's snapshot-hash gate and backfilled (streamed, one blob resident at a time) from current snapshots at migration so historical sessions are queryable immediately.Session::range_totals_multias the in-memory path. Golden tests assert field-for-field equality across adversarial windows: boundary-exact timestamps, mutation-retry chains split by window edges, undated findings, sub-millisecond events, empty and open windows, and append-then-requery.ledger_dirtycolumn that survives restarts) and computed from memory; a missing store or failed query falls back wholesale. The perf event recordssource=ledger|mixed|memory|fallbackso recordings verify which path served each request.An independent adversarial review ran before this PR: three major findings (overlay-path fact corruption with a restart-survival hole, the writer-mutex hold pattern, migration memory) and four minors — all fixed in the second commit, with the dirty-flag lifecycle and append path now under test.
Validation: cargo test --all-targets 225 passed across suites (173 lib including 4 new ledger tests) / fmt clean; svelte-check 0 errors; vitest + node tests green (frontend untouched).
Next (Wave 2b, separate PR): summaries-in-RAM with on-demand hydration — the memory cap this query service exists to enable.
🤖 Generated with Claude Code