The file-store backend for Raxol.Agent.Journal.Records.Checkpoint.
Implements the checkpoint pointer-record discipline over a
Raxol.Agent.Journal.FileStore session.
A checkpoint is an in-log pointer, not a second store: the snapshot payload is
content-addressed and written out-of-line to <session>/snapshots/<sha256>.json
(atomic temp+fsync+rename) before the pointer record is appended
through the one single Writer, consuming exactly one offset from the one id
space. The record body carries only the pointer + hash + tip — never model
content.
Write path (write/3)
- read the journal (tolerant Reader; STRING-keyed maps);
- resolve
tip_offset(explicit opt, else the derived conversational tip); - reject an invalid tip (
:invalid_tip) — the named record must exist and be CONVERSATIONAL on the branch; nothing is appended; - reject a mid-turn / mid-reserve write (
:mid_turn/:mid_reserve); - stage the snapshot file (content-addressed, atomic) — skipped for a
nilmodel, which is a legal tip-only pointer; - append the pointer record through the single Writer ⇒
{:ok, offset}. The turn-boundary check is re-run atomically at the commit point (FileStore.append_checked/3— the check executes inside the single Writer against the freshest on-disk records), so a concurrentturn_startedlanding between step 1's read and the append can never slip a checkpoint mid-turn. Step 4 is only the cheap pre-flight that avoids staging a snapshot file destined for rejection.
Restore path (restore/2)
Locate the newest checkpoint (highest offset), then:
- tip-only pointer (
snapshot_ref == nil) ⇒ fullfold(0..tip_offset); - else verify
sha256(bytes) == snapshot_hash(mismatch ⇒:snapshot_corrupt; absent file ⇒:snapshot_missing; both leave the journal:okand delete nothing), load the snapshot, then fold the CONVERSATIONAL tail (records withid > tip_offset) forward onto it.
A restore equals a full fold over the persistent slice.
Restore-path hardening
- Bounded nesting BEFORE decode — the raw snapshot bytes are pre-scanned
(
nesting_within_bound?/1, an O(n) byte walk that never builds a term) and rejected:snapshot_corruptwhen JSON nesting exceeds@max_decode_depth— beforeJason.decode/1ever runs, so a hostile small-but-deep snapshot can never force the parser to materialize an unbounded term. The depth-bounded structural walk (safe_term?/2) still runs after decode as defense in depth. $sderef-gadget guard — a decoded map carrying a"$s"struct-module tag is rejected (:snapshot_corrupt); this backend NEVER resolves a caller-controlled atom to a module or callsstruct/2on untrusted disk data (type-confusion / atom-table-DoS class).- Malformed-pointer reject —
snapshot_refmust matchsnapshots/<64-hex>.jsonexactly; anything else (path traversal, wrong shape) is refused before any file read, so a hostile pointer can never escape the session'ssnapshots/directory. - Snapshot-size ceiling — a snapshot file over
@max_snapshot_bytesis refused (:snapshot_corrupt) rather than loaded whole.
Model fold (surrogate)
This backend owns the pointer discipline, not the model. It has no codec for
a harness model and no reducer for its events, so both are surrogates:
dump/1 writes a JSON-native model as it is, and the fold (applying
conversational events onto a model) appends each CONVERSATIONAL event's id
to model["applied"]. The round-trip equation (a restore equals a full
fold) does not depend on the codec.
Surrogate safety (this backend is the DEFAULT — it must fail loud)
Because this backend is the active default, the surrogate is gated rather than trusted:
- Write —
dump/1typed-rejects any model the surrogate JSON codec cannot faithfully round-trip (structs, tuples, pids, refs, functions, atom keys/values, non-UTF-8 binaries, non-map top-level):{:error, :surrogate_backend_unbound}, nothing staged, nothing appended — never an uncaughtJason.EncodeErrorcrashing the caller. - Restore — every successful restore through the surrogate is LOUD:
it emits
[:raxol, :agent, :journal, :checkpoint, :surrogate]telemetry (metadata%{op: :restore, session_id:, tip_offset:}— offsets only, never content) and logs a warning, so a caller can never mistake the surrogate%{"applied" => [ids]}fold for real harness state. Writes that stage a surrogate snapshot emit the same event withop: :write.
No redaction (sanitize / secret exclusion)
The surrogate performs NO redaction: a JSON-safe model is written
verbatim (plaintext) to snapshots/ — the same trust domain and permissions
as the journal segments, which already persist event payloads in plaintext.
The record body itself never carries model content. A codec that replaces
dump/1 must bring redaction with it.
Summary
Functions
Restore ONE specific already-read checkpoint record — the hardened
single-checkpoint restore a compaction resume delegates to for its
newest-first walk. restore/2 is exactly this applied to the newest
checkpoint; both share the same validation + snapshot hardening so
compaction-restore IS checkpoint-restore (structural resume-equivalence).
Functions
Restore ONE specific already-read checkpoint record — the hardened
single-checkpoint restore a compaction resume delegates to for its
newest-first walk. restore/2 is exactly this applied to the newest
checkpoint; both share the same validation + snapshot hardening so
compaction-restore IS checkpoint-restore (structural resume-equivalence).