Skip to content

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.

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:

  1. What was state exactly at position p?
  2. What entries lie beneath the current tail?
  3. 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.

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.

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 Position

The 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.

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.

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.

1. Your only snapshot stands at 900 and the report asks for state at 850. Can 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?

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?

Because impossible operations are absent. An inspector creates no writer and exposes no commit surface, so mutation cannot be a missed flag check.

Stokker Technologies markDesigned and built by Stokker Technologies