Horon

Temporal Epochs: opt-in history retention + temporal reader

Status: shipped 2026-07-10. Companion: horon-engine's docs/SEMANTIC_INDEX.md — shares the epoch model. Format details: HTT_FORMAT.md §4.1. Tests: tests/temporal.rs.

The insight

The WAL already records the movement of the manifold: every set_semantic is a seq-ordered, CRC-guarded OP_SET_SEMANTIC entry; every structural change is an OP_INSERT/OP_DELETE. Trajectory information is already being written — and then compact() truncates it. The temporal pillar is the decision to stop discarding history we already pay to record, plus the minimal machinery to address it: epoch markers, retention across compaction, and a reader for time-scoped queries.

Deltas beat snapshot series on every axis that matters here:

Non-negotiable: the simple htt pays nothing

Temporal is opt-in. With retention off (the default), behavior is byte-identical to today: compact() truncates the WAL, no epoch entries exist unless written, no sidecars appear, no read/write/recovery path changes. A user of htt as a fast hierarchical KV store never encounters this layer. Verified by an off-mode identity test (same op sequence with and without the feature compiled path → identical main-file bytes).

Design

1. Epoch markers — OP_EPOCH (0x06)

A new WAL op, appended by Horon::seal_epoch():

payload: epoch_id (u64 LE) | flags (u8)    // bit 0: SPECULATIVE
key:     "" (empty — epochs are file-scoped, not node-scoped)

Compatibility (honest statement): OP_EPOCH is a new WAL op code. Files that never call seal_epoch() never contain it and remain readable by every existing tool. A pre-epochs reader opening a live WAL that does contain OP_EPOCH fails with unknown-op — acceptable for private-repo stage; noted in HTT_FORMAT.md. The main-file header, version byte, and snapshot layout are untouched.

2. Retention across compaction — sidecar history segments

HoronConfig gains history_retention: HistoryRetentionOff (default) | Archive. With Archive, compact() moves the old WAL into a sealed, zstd-compressed sidecar instead of truncating it:

data.htt          — main file, format unchanged, always self-sufficient
data.htt.h000001  — history segment 1 (oldest), zstd, own header + CRC
data.htt.h000002  — history segment 2, ...

3. Temporal reader — HoronHistory

Read-only, opens main file + sidecars, one sequential scan (segments are cold; this is an analysis path, not a hot path):

HoronHistory::open(path) -> Self
  .epochs() -> Vec<EpochInfo>                      // id, seq span, speculative
  .as_of(epoch) -> HoronStateView                    // full state at that seal
  .trajectory(key, dim_range) -> Vec<(epoch, Vec<FixedPoint>)>
  .delta(epoch_a, epoch_b) -> Vec<KeyDelta>        // who moved, which dims, how far

4. Interaction with the engine's semantic index

seal_epoch() marks the calibrate → query transition. Within an epoch the manifold is frozen, so the engine's lazy per-slice VP-trees stay valid; the epoch counter is the natural invalidation signal. The two features are independently useful and independently shippable — the index works without epochs (any semantic write bumps its internal counter), and epochs work without the index.

Known limits (stated up front)

Verification plan