# `Raxol.Agent.Journal.Records.Checkpoint.FileBackend`
[🔗](https://github.com/DROOdotFOO/raxol/blob/v2.8.0/lib/raxol/agent/journal/records/checkpoint/file_backend.ex#L1)

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`)

  1. read the journal (tolerant Reader; STRING-keyed maps);
  2. resolve `tip_offset` (explicit opt, else the derived conversational tip);
  3. reject an invalid tip (`:invalid_tip`) — the named record must exist and
     be CONVERSATIONAL on the branch; **nothing is appended**;
  4. reject a mid-turn / mid-reserve write (`:mid_turn` / `:mid_reserve`);
  5. stage the snapshot file (content-addressed, atomic) — skipped for a
     `nil` model, which is a legal tip-only pointer;
  6. 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 concurrent
     `turn_started` landing 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`) ⇒ full `fold(0..tip_offset)`;
  * else verify `sha256(bytes) == snapshot_hash` (mismatch ⇒
    `:snapshot_corrupt`; absent file ⇒ `:snapshot_missing`; both leave the
    journal `:ok` and delete nothing), load the snapshot, then fold the
    CONVERSATIONAL tail (records with `id > 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_corrupt` when JSON nesting exceeds
    `@max_decode_depth` — **before** `Jason.decode/1` ever 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.
  * **`$s` deref-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 calls `struct/2` on untrusted disk
    data (type-confusion / atom-table-DoS class).
  * **Malformed-pointer reject** — `snapshot_ref` must match
    `snapshots/<64-hex>.json` exactly; anything else (path traversal, wrong
    shape) is refused before any file read, so a hostile pointer can never
    escape the session's `snapshots/` directory.
  * **Snapshot-size ceiling** — a snapshot file over `@max_snapshot_bytes` is
    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/1` typed-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 uncaught `Jason.EncodeError` crashing 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 with
    `op: :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.

# `restore_checkpoint`

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).

---

*Consult [api-reference.md](api-reference.md) for complete listing*
