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

Checkpoint pointer records.

A checkpoint is an **in-log pointer record**, not a second store. The snapshot
payload lives content-addressed and out-of-line at
`<session>/snapshots/<sha256-hex>.json`; the `checkpoint` record carries only
the pointer + hash + the conversational tip it was taken at. It is appended
through the **same single Writer** as every other record, consuming exactly one
offset from the one id space.

The record shape (`t/0`) and the `write/3` / `restore/2` seam are a behaviour.
The default backend (no registration) is the file-store `FileBackend`, and
`write`/`restore` dispatch to it. `{:error, :not_implemented}` is only the
explicit nil-backend opt-out.

## Write discipline (frozen)

  * The snapshot file is written **before** the record is appended (a record
    cannot be named by its own offset before it exists), via an atomic
    temp+fsync+rename. A crash between file-write and record-append leaves a
    harmless orphan file, which is never deleted implicitly.
  * `snapshot_hash` = lowercase-hex `sha256` of the snapshot file bytes; it is
    verified at restore. A missing file → `{:error, :snapshot_missing}`; a
    hash mismatch → `{:error, :snapshot_corrupt}` — and in **both** cases the
    journal stays `:ok` and nothing is deleted.
  * `snapshot_ref: nil` is a legal tip-only pointer;
    restore then falls back to a full `fold(0..tip_offset)`.
  * `tip_offset` is frozen at write time and validated at write time: the
    record it names MUST satisfy `conversational?/1` on the checkpoint's own
    branch, else `{:error, :invalid_tip}` and **nothing is appended**.
  * A checkpoint MUST NOT be appended mid-turn (between `turn_started` and its
    close) nor mid-reserve (between a spend-gate reserve and its terminal),
    else `{:error, :mid_turn}` / `{:error, :mid_reserve}`.
  * The record body carries **no model content** — pointer + hash only.
    The default backend's surrogate codec typed-rejects any non-JSON-native
    model (`{:error, :surrogate_backend_unbound}`) and writes a JSON-safe
    model **verbatim, with no redaction** — see "No redaction" in
    `Checkpoint.FileBackend`'s moduledoc.

# `model`

```elixir
@type model() :: term()
```

A folded harness model: snapshot content, opaque to this module.

# `t`

```elixir
@type t() :: %{
  id: non_neg_integer(),
  schema_version: String.t(),
  kind: String.t(),
  branch_id: String.t(),
  session_id: String.t(),
  ts: integer(),
  tip_offset: pos_integer(),
  snapshot_ref: String.t() | nil,
  snapshot_hash: String.t() | nil,
  reason: String.t()
}
```

A `checkpoint` record — the frozen on-disk shape (string keys on disk; this is
the Elixir-side view). Every field below is frozen: none is
ever renamed, repurposed, type-narrowed, or flipped optional→required.

# `protected_floor`

```elixir
@callback protected_floor(journal :: term(), records :: [map()]) ::
  {:offset, pos_integer()} | :none
```

The GC-protected floor for `journal`'s already-read `records`: the `tip_offset`
of the newest **healthy** checkpoint (its snapshot present + hash-verified,
restore succeeds). Records at or above this offset MUST NEVER be truncated.
`:none` when no healthy checkpoint exists.

# `restore`

```elixir
@callback restore(journal :: term(), opts :: keyword()) ::
  {:ok, model()} | {:error, term()}
```

Restore the model captured by the latest healthy checkpoint of `journal`.

Locates the latest healthy checkpoint, verifies `sha256(bytes) ==
snapshot_hash`, loads the snapshot, and folds the journal forward
from `tip_offset` — equal to a full fold over the persistent slice. A
missing/corrupt snapshot surfaces
`{:error, :snapshot_missing | :snapshot_corrupt}` with the journal left `:ok`
and nothing deleted. A tip-only checkpoint (`snapshot_ref: nil`) falls
back to a full `fold(0..tip_offset)`.

# `restore_checkpoint`

```elixir
@callback restore_checkpoint(journal :: term(), records :: [map()], checkpoint :: map()) ::
  {:ok, model()} | {:error, term()}
```

Restore the model captured by ONE specific already-read `checkpoint` record of
`journal` (the hardened single-checkpoint restore).

This is the seam a compaction resume delegates to for its newest-first walk:
it applies the SAME restore-path hardening as `restore/2`
(`tip_offset`/`snapshot_ref` presence guard ⇒ `:malformed_checkpoint`;
`@ref_re` path-traversal reject ⇒ `:malformed_pointer`; snapshot size ceiling,
depth-bounded decode, `$s` deref-gadget guard ⇒ `:snapshot_corrupt`; hash
verify ⇒ `:snapshot_corrupt`; absent file ⇒ `:snapshot_missing`) — the journal
stays `:ok` and nothing is deleted on any failure. `restore/2` is
exactly `restore_checkpoint/3` applied to the newest checkpoint.

# `write`

```elixir
@callback write(journal :: term(), model :: model(), opts :: keyword()) ::
  {:ok, Raxol.Agent.Journal.FileStore.offset()} | {:error, term()}
```

Write a checkpoint for `journal` capturing `model`.

Snapshot-file-before-record, single-Writer append, tip validated at write,
turn-boundary enforced. Returns `{:ok, offset}` (the checkpoint's own dense
journal offset) or a typed `{:error, reason}`
(`:invalid_tip` | `:mid_turn` | `:mid_reserve` | ...).

Options: `:reason` (one of `reasons/0`, default `"manual"`), `:tip_offset`
(explicit tip to freeze; derived from the journal's conversational tip when
absent), `:base_dir`.

# `kind`

```elixir
@spec kind() :: String.t()
```

The record `kind` string every checkpoint carries.

# `protected_floor`

```elixir
@spec protected_floor([map()]) :: {:offset, pos_integer()} | :none
```

Record-only variant of `protected_floor/2` for callers WITHOUT a journal
handle: it can validate record shape but cannot probe snapshot health, so it
returns the newest well-formed checkpoint's tip. On its own it is NOT
sufficient for a truncation admission decision under the fall-back resume
model — a shape-valid checkpoint whose snapshot file is corrupt keeps its
higher tip here, while `protected_floor/2` lands the floor on the older
healthy tip resume actually falls back to. A GC writer with a journal handle
MUST use `protected_floor/2`.

**Fail-closed on malformed checkpoints**: the
tolerant Reader accepts a truncated or hostile checkpoint record missing
`id`/`tip_offset` (or carrying non-integer junk). Restore typed-rejects
those (`:malformed_checkpoint`); this floor — the "never orphan a
checkpoint" safety check — must not raise a `KeyError` on the same records.
When ANY checkpoint record is malformed the floor is unknowable, so it
degrades to `{:offset, 1}`: the entire journal is protected and every
truncation proposal is rejected. Protecting too much is safe; guessing is
not.

# `protected_floor`

```elixir
@spec protected_floor(term(), [map()]) :: {:offset, pos_integer()} | :none
```

The GC-protected floor for `journal`'s already-read `records`: records at or
above this offset MUST NEVER be truncated, so the checkpoint restore path
stays intact (**frozen law: GC never orphans checkpoints**).

It is the `tip_offset` of the newest **healthy** checkpoint — the one resume
actually restores from. "Healthy" means its snapshot is present and
hash-verified (restore succeeds); a corrupt newest is skipped so the floor
lands on the older healthy checkpoint a fall-back restore uses, never on
a corrupt newest's higher tip (which would let GC truncate records the
fall-back fold still needs — an orphaned fall-back tip). `:none` when no
healthy checkpoint exists (nothing to protect).

A future `gc`/truncation writer MUST consult this and reject any proposal
whose truncation range reaches this offset — rejected at the same synchronous
admission seam as `:invalid_tip`, never accepted-then-marked.

Dispatches to the active backend (default `FileBackend`) because health is a
snapshot-file property, not a record-only one. This arity is the
authoritative floor for a truncation admission decision;
`protected_floor/1` is the record-only conservative variant for callers
without a journal handle.

# `put_backend`

```elixir
@spec put_backend(module() | nil) :: :ok
```

Register a module implementing this behaviour as the active backend, or `nil`
to force the inert `{:error, :not_implemented}` seam. Absence ⇒ the default
`FileBackend`.

# `reasons`

```elixir
@spec reasons() :: [String.t()]
```

The v1 grow-only `reason` enum accepted by the producer seam.

# `record_keys`

```elixir
@spec record_keys() :: [String.t()]
```

The complete frozen key set (plus the defaulted `branch_id`) of a checkpoint record.

# `restore`

```elixir
@spec restore(term(), keyword()) :: {:ok, model()} | {:error, term()}
```

See `c:restore/2`. Dispatches to the active backend (default `FileBackend`).

# `restore_checkpoint`

```elixir
@spec restore_checkpoint(term(), [map()], map()) :: {:ok, model()} | {:error, term()}
```

See `c:restore_checkpoint/3`. Dispatches to the active backend (default `FileBackend`).

# `turn_closers`

```elixir
@spec turn_closers() :: [String.t()]
```

The record types that close a `turn_started`. A checkpoint is refused as
`:mid_turn` while more turns were started than closed.

# `write`

```elixir
@spec write(term(), model(), keyword()) ::
  {:ok, Raxol.Agent.Journal.FileStore.offset()} | {:error, term()}
```

See `c:write/3`. Dispatches to the active backend (default `FileBackend`).

---

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