replay-journal
Deterministic offline replay of a command journal through the real core, fenced on a final state hash.
"Send me your journal": replay_journal deterministically replays a journal directory and verifies
its determinism fence. The fence verifies exec-lane determinism — that re-folding the journaled
exec lane from the first checkpoint reproduces the recorded final order and account state
(crates/vike-core/src/bin/replay_journal.rs).
replay_journal <journal-dir>Called with no argument it prints usage: replay_journal <journal-dir> and exits 2. On success it
prints the source journal's total record count, how many snapshots were verified, the final state
hash, and one line per engine giving its venue, symbol, open-order count, position count and
balance. On failure it prints REPLAY FAILED with the error and exits 1. Protocol and result
output stays raw on stdout — the repo's logging rule for tools.
The mechanism
The bin is a thin wrapper around vike_core::replay::replay_offline, and the interesting part is
where the replay starts.
- Restore the engine from the journal's first
Snap— the earliest full-config checkpoint. Restoring from the latest would leave nothing to replay on a clean run, because its exit snapshot already reflects everything: a worthless fence. Cadence snapshots are taken after the fold, so every command before the first snapshot is already folded into it and every command after it is re-folded forward, re-deriving the whole post-checkpoint history. - Spawn a real core from that engine with a pure no-op
ReplayClient— venue side effects suppressed, because the original venue events are already journaled and get re-pumped, and a client that also emitted events would double the state — aQueueClockreplaying the tail's recordednow_msso timestamp-derived state matches, the base snapshot's client-order-id session, and a temp journal configured so only the exit snapshot fires. - Pump every post-base command through the handle in order, shut down, join.
- Fence: read the replay journal's final snapshot and require its hash to equal the source journal's last snapshot hash.
Why the fence is a final hash and not a per-record comparison
The replay core's temporary sequence numbers differ from the source's, so per-sequence comparison is impossible. Final-hash equality is the essential guarantee: re-folding from the first checkpoint reproduces the exact final state.
This both gates the journal format — any nondeterminism in the fold surfaces as a mismatch — and is
the substrate the crash-restart path builds on. That second entry point,
vike_core::replay::restore_from_journal, shares the same engine but restores from the latest
snapshot, re-folds only the (usually empty) crash tail, and returns the reproduced engines and
session with no fence: it is a restore, not a proof.
What replays, and what does not
A mounted strategy reacts to non-journaled bar and tick messages, but its order flow does not:
each already-resolved OrderIntent is journaled write-ahead as JournalRecord::StrategySubmit,
with mint and tag resolution already done, and the tail replays it exactly like a command. Replay
never re-runs the strategy's hooks — the replay core mounts no strategy — so it does not matter
that the triggering ticks are absent; only the strategy's already-decided orders need to reproduce.
Emulated conditional fires replay the same way: the
decision is journaled write-ahead as JournalRecord::ConditionalFire (arm id plus the released
order request) and re-applied through the same submit site, never re-evaluating a trigger, so a
fire can neither double nor vanish. Disarms ride an ordinary write-ahead command carrying the
complete arm id. Identity is what makes that sound: the replay core's arm counter is seeded from
the base snapshot's stamped arm_seq, so tail arms re-mint the identical ids the live core minted
and a tail disarm finds its arm.
Armed conditionals also survive a restart. The resting books ride every snapshot — each arm's
minted id plus the terms the book held, including a trailing arm's current ratcheted extreme,
the one value neither the write-ahead arm command nor the seed record can carry, because it moves
on non-journaled market data. Crash-tail membership is a pure fold of the journal's conditional
records (fold_conditionals), never the replay core's own book state: that core pumps no market
data, so a fired arm would linger in it and a trailing arm would be refused for lack of a mark —
the fold gets both right from the records.
Two shapes are refused rather than guessed
Replay is single-engine only: a multi-engine base returns ReplayError::Unsupported, because
per-engine seed_cash is not part of EngineSnapshot and a faithful multi-engine restore is
deferred. A watchdog-tick tail is Unsupported too — the stuck-order and dead-man wall-clock
sweeps it carries are not deterministic functions of the journal.
Related: the command journal, replay, segments, locking, replay a recorded session, the execution core.