Skip to content

Serve range aggregation from the durable ledger - #103

Merged
ekalb81 merged 4 commits into
mainfrom
feat/ledger-aggregation
Aug 3, 2026
Merged

Serve range aggregation from the durable ledger#103
ekalb81 merged 4 commits into
mainfrom
feat/ledger-aggregation

Conversation

@ekalb81

@ekalb81 ekalb81 commented Aug 3, 2026

Copy link
Copy Markdown
Owner

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.

  • Schema v3: durable_tool_events and durable_finding_events fact 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.
  • One implementation, two paths: the ledger fetches each session's in-span facts once on a dedicated read connection (WAL — the writer is never blocked by sweeps) and evaluates all windows through a minimal session shell running the very same Session::range_totals_multi as 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.
  • Degraded modes are complete, never silent: sessions whose persist failed are tracked (in-process and — for the overlay-advance case — durably via a ledger_dirty column that survives restarts) and computed from memory; a missing store or failed query falls back wholesale. The perf event records source=ledger|mixed|memory|fallback so recordings verify which path served each request.
  • The in-memory oracle now compares timestamps at millisecond precision, matching both the ledger's storage granularity and the wire bounds' actual precision — the two paths cannot classify a boundary event differently.
  • Wire note: with the ledger authoritative, explicitly requested session ids that are no longer resident in memory (e.g. retained sessions after a future memory cap) return data instead of being silently dropped.

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

ekalb81 and others added 4 commits August 3, 2026 01:18
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]>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment thread src-tauri/src/model.rs
self.tokens_history
.partition_point(|event| event.timestamp < *from)
self.tokens_history.partition_point(|event| {
event.timestamp.timestamp_millis() < from.timestamp_millis()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

@ekalb81
ekalb81 merged commit 32fc4cb into main Aug 3, 2026
10 checks passed
@ekalb81
ekalb81 deleted the feat/ledger-aggregation branch August 3, 2026 05:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant