jevsnes.git / packages / replay / CLAUDE.md
1@README.md
2
3## Notes for agents
4
5- **Frame numbering is per-branch-relative to a bot lifetime.** A branch's
6  log entry at local index `i` was recorded during the tick where
7  `zbanks::Bot`'s own internal frame counter was `i` - true by construction,
8  because a recording always starts exactly when a fresh `Bot` does (window
9  start, or a rebuilt branch). `src/recorder.rs` and `apps/native`'s
10  integration must preserve this: never start a recording or a branch
11  mid-bot-lifetime, or `src/rebuild.rs`'s goal-choice replay (keyed to tick
12  order, not a stored frame number) silently mismatches with no error.
13- **A non-root branch's own keyframe at its fork frame is not optional.**
14  `crate::tree::Tree::owner_of` resolves a frame exactly at a fork point to
15  the CHILD, on the assumption that the child always has its own keyframe
16  there (`src/recorder.rs`'s `store_fork_keyframe`). Skip that call when
17  creating a branch and seeking to its very first frame silently reads the
18  wrong keyframe file and panics on a missing one instead.
19- **`keyframes.rs` bounds seek cost by tier, and pruning is only safe
20  because of one invariant:** a keyframe delta-references only a keyframe of
21  its own tier or coarser, never a finer one. That is what lets `prune`
22  delete a whole aged-out dense or medium bucket without orphaning a
23  survivor's reference. Change `preferred_reference` and pruning silently
24  becomes a way to corrupt keyframes. `prune` also never deletes a branch's
25  first (anchor) keyframe, whatever its tier.
26- **Keyframes are `Console::snapshot_fixed`, never `Console::snapshot`.**
27  The keyframe codec (`src/keyframe.rs`) XORs at fixed offsets, and varint
28  snapshots shift every field whenever one integer crosses a width
29  boundary: 2 s deltas were 866 KB that way, 115 KB fixed-width. The
30  recorder takes `&Console` and snapshots itself so a caller cannot hand it
31  the wrong kind, and `seek.rs` restores keyframes with `restore_fixed` but
32  the header's frame-0 snapshot with `restore`. Changing a keyframe's
33  encoding means bumping `header::FORMAT_VERSION`: older recordings are
34  then deleted at window start (`storage::remove_other_formats`), not read.
35- **The compression is `ruzstd` (pure-Rust zstd) over the XOR.** If disk
36  per hour needs to come down, the knobs are `recorder::RECORD_EVERY`, the
37  tier retention constants in `keyframes.rs`, and `storage`'s cap - measure
38  with `apps/replay-probe` before and after.
39- **`seek.rs`, `rebuild.rs` and `storage.rs` were built only after
40  `apps/replay-probe` proved the core deterministic** - two independent
41  10,000-frame replay passes, byte-identical to a bot-driven recording and
42  to each other (2026-09-22). All three assume that; if a future core
43  upgrade needs re-proving it, rerun the probe before trusting any of them
44  again - a diverged core makes every one of them silently produce a wrong
45  machine, not a loud error.
46- **`seek::entries_up_to` is for `rebuild.rs`; `seek_to` is for scrubbing.**
47  They are not interchangeable: `seek_to` starts from a keyframe ALREADY
48  inside the target branch and only needs that branch's own tail;
49  `entries_up_to` starts from frame 0 and stitches every ancestor branch's
50  contribution together, because a fresh bot has no keyframe to jump to -
51  it must tick every logged frame from the beginning.
52- **Never snapshot on every frame "to be safe".** A snapshot is 1.3 MB;
53  `recorder::Recorder::record` takes the console and only snapshots on the
54  frame a keyframe is due.
55- The design notes behind this crate (Doom, Quake, Source, BizHawk,
56  RetroArch; "determinism first") live in the owner's private notes, as
57  `replay-timeline.md`; source comments cite them by that name. They are not
58  in this repository, so do not add links to them here.