jevsnes.git / packages / replay / CLAUDE.md

For agents, on top of README.md, which they read first.

Notes for agents

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