perf(trajectory): read the ledger once per view request, not twice #144
No reviewers
Labels
No labels
burndown-2026-08
autonomy
async-consult
autonomy
epic
autonomy
headless
autonomy
live-collab
coherence-core
priority
P0
priority
P1
priority
P2
priority
P3
priority
P4
qa-fixture
role/advocate
role/director
role/exec
role/frontend
role/gamedev
role/human
role/platform
role/qa
role/science
role/sysadmin
state
ambient
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
coilyco-flight-deck/agent-proxy!144
Loading…
Reference in a new issue
No description provided.
Delete branch "task/142-single-ledger-read"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Refs #142. Takes the half of it that needs no design decision, and deliberately leaves the rest.
The problem
_operational_builderruns on every/v1/trajectory/views/*and/dossiers/*request and callediter_eventstwice: once insidematerialize_retained_events, then again for the evaluation half. Reading and JSON-parsing the whole events table is the dominant cost of that path, so it was being paid twice over.Measurements
Same-process, synthetic ledger, steady state (nothing changed, every write a no-op). Baseline is merged
mainat9327158.It was also a latent correctness problem
The two reads are separate SQLite snapshots. An event ingested between them appeared in the evaluation half and not in the materialized half, so a single request could build its view from two different ledgers. That window is narrow but the ingest emitter writes concurrently on the hot path, so it is reachable rather than theoretical. One snapshot removes it by construction, which is why
materialize_retained_eventsgrew aneventsparameter instead of the builder just caching a local.Callers that pass no
eventsare unaffected and still read the store.Verification
tests/test_trajectory_view_reads.py, 3 tests. Mutation-checked: restoring the double read fails the suite. One test drives the real_operational_builderand counts full-table reads, the others pin that a supplied snapshot is what gets materialized and that the default path is unchanged.just format-check,just lint,just typecheck, andpre-commit run --all-filesclean.What is still open on #142
The growth curve. Read cost is still linear in ledger size, because the rebuild still materializes everything on every request. Closing that means choosing between a staleness window, incremental materialization, or cached views. That changes what a read promises, so it stays yours to call and #142 stays open.
Between this and #143, a 2000-turn rebuild is down from 617.3 ms to 262.3 ms without touching that promise.
🤖 Generated with Claude Code
`_operational_builder` runs on every view and dossier request and called `iter_events` twice: once inside `materialize_retained_events` and again for the evaluation half. Reading and JSON-parsing the whole events table is the dominant cost of that path, so it was being paid twice over. `materialize_retained_events` now takes an optional `events`, and the builder reads once and hands the same tuple to both halves. Measured same-process on a synthetic ledger in the steady state: 500 turns 109.6 ms -> 60.0 ms 1000 turns 186.8 ms -> 121.9 ms 2000 turns 369.0 ms -> 262.3 ms The second read was also a latent correctness problem, not only a cost. An event ingested between the two calls appeared in the evaluation half and not in the materialized half, so one request could report two different ledgers. One snapshot removes that by construction. Callers that pass no `events` are unaffected and still read the store. Growth stays linear in ledger size, so #142 remains open for the freshness decision behind incremental materialization. Refs #142 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Co-authored-by: Kai Siren <coilysiren@gmail.com> Co-authored-by: Claude <noreply@anthropic.com>