Read History
Chapter 5 read the present and followed the recent past. This chapter reaches further back: state as it stood at an exact position, entries beneath the in-memory tail, and both of those from a tool with no writer at all. Three verbs over one replay fold, and three refusals that each name a different missing part of the recovery equation.
Three Verbs, One Replay Fold
Section titled “Three Verbs, One Replay Fold”Chapter 5 read the current state and followed recent entries. Persistence then gave the runtime a source beneath that in-memory tail. Historical reads close the loop with three questions:
- What was state exactly at position
p? - What entries lie beneath the current tail?
- Can a tool answer both without opening a writer?
The answers are state_at, storage-backed query, and Runtime::inspect.
They share recovery's fold; there is no second historical model and no query
language hiding behind it.
State at an Exact Position
Section titled “State at an Exact Position”Every fold starts from a caller-owned origin:
let then = runtime .journal .state_at( Origin::initial(Schematic::default()), receipt.position, ) .await?;then is At<Schematic> at exactly the requested position. Harmos neither
clones nor defaults your state. For a later snapshot, anchor the value where it
already stands:
let origin = Origin::at(snapshot.state, snapshot.position);let then = runtime.journal.state_at(origin, requested).await?;An origin above the requested point answers Error::OriginAhead; folding
backwards would invent state. Entries apply through the version stamped when
they were written, and replay still calls no check, performs no I/O in
application behavior, and skips facts.
The result is detached. It holds no state lock and cannot change the live runtime. Historical analysis may take its time without turning somebody else's commit latency into its own runtime.
Query Beneath the Tail
Section titled “Query Beneath the Tail”The verb did not change:
let entries = runtime.journal.query::<Transactions>(after).await;The answer path did. If the requested range is still in the bounded tail,
query snapshots it there. If it falls beneath the tail and a recovery source
is attached, the source-owning supervisor serves the exact stored range. The
iterator is identical either way and positions stay in the one mixed order.
With no source, the old honest answer remains Error::BeyondTail. Beneath a
declared prune horizon the answer is Error::BeneathHorizon. Neither path ever
returns a short prefix and calls it complete.
Historical watching is composition, not another verb:
query from checkpoint until caught up -> remember the last mixed-order Position -> watch live after that PositionThe storage supervisor owns the recovery source after boot. A historical read is a bounded request to that owner, not a second file handle racing it or a source smuggled back into application code.
Inspect Without a Writer
Section titled “Inspect Without a Writer”Audit tools do not need a runtime capable of mutation:
let inspector = Runtime::<SchematicEditor>::inspect( Origin::initial(Schematic::default()), Storage::new().recover(Jsonl::open(path)?),).await?;
let summary = inspector.journal.read(Summary::of).await?;let entries = inspector .journal .query::<Transactions>(Position::ORIGIN) .await;Inspector<A> owns an Inspection<A>, not a Journal<A>. It exposes read,
state_at, query, applied, and stored.
There is no commit, record, undo, redo, publish surface, writer task, or
runtime stop protocol. The absence of write verbs is a type fact rather than a
runtime permission check.
Dropping the inspector closes its source task. It starts no consumers and holds no live publication path, so a CLI report can open a durable copy, answer one question, and end.
Retention Is the Boundary
Section titled “Retention Is the Boundary”Historical reads make retention visible rather than optional. Three refusals name three different impossible folds:
OriginAhead: the supplied state already includes entries beyond the point requested.BeneathHorizon: retention removed entries required to connect the origin to the point requested.BeyondTail: the live runtime has no source and memory no longer holds the requested range.
The snapshot-above-target case is the subtle one. A snapshot at 900 cannot answer state at 850. Harmos refuses rather than subtracting changes, silently falling back to current state, or pretending an incomplete prefix was enough. Choose an origin at or below the target whose connecting entries still exist.
That same rule keeps evidence joins honest. A state reconstructed at receipt
position 850 may be joined with stream rows selected .as_of(Position(850));
both coordinates now mean the same state cut.
Test Your Knowledge
Section titled “Test Your Knowledge”
1. Your only snapshot stands at 900 and the report asks for state at 850. Can state_at use it?
state_at use it?No. The snapshot already contains fifty entries beyond the requested cut, and harmos has no reverse fold. Supply an older origin or take the refusal.
2. A sourced runtime asks query beneath its tail. Should the caller switch APIs?
query beneath its tail. Should the caller switch APIs?No. query is the question; the runtime routes the exact range to its
source-owning supervisor. Only a sourceless runtime answers BeyondTail.
3. Why prefer Inspector over a live runtime with a read-only flag?
Inspector over a live runtime with a read-only flag?Because impossible operations are absent. An inspector creates no writer and exposes no commit surface, so mutation cannot be a missed flag check.