# `Raxol.Agent.Journal.FileStore.Reader`
[🔗](https://github.com/DROOdotFOO/raxol/blob/v2.8.0/lib/raxol/agent/journal/file_store/reader.ex#L1)

Tolerant replay of a session's JSONL journal segments (see the package
README, "The journal").

Segments are replayed in ascending order into an ordered record stream. Two
failure policies, per the spec:

  * **Torn tail (Ra policy).** A parse failure on the *final* line of the *last*
    segment is a crash mid-write. The torn bytes are truncated away, everything
    before is recovered, and the session stays healthy (`{:ok, records}`).

  * **Interior corruption.** A parse failure anywhere else marks the session
    damaged. A hard alarm fires (`Logger.error` + a telemetry event), **nothing
    is deleted**, and the damaged content is **never** returned — `scan/2`
    returns `{:damaged, records_before}` and callers must not surface it.

  * **Chain break (chained journals).** A record that does not link to its
    predecessor's hash, does not match its own hash, or is not its own
    canonical JSON (see `Raxol.Agent.Journal.Chain`) is interior corruption.
    So is a journal that ends below the offset `HEAD` anchors, or whose
    anchored record carries a different hash: on a chained journal a torn
    tail can only be a line that was never synced.

  * **Chain downgrade.** A journal is chained when `meta.json` says so, but
    the flag is not the only witness: `HEAD`'s `"tip_hash"` and the first
    record's `"prev_hash"`/`"hash"` are only ever written to a chained
    journal. A journal that carries any of them without the flag had its
    flag removed, which would otherwise switch every check above off; it is
    damaged from offset 1.

# `result`

```elixir
@type result() :: {:ok, [map()]} | {:damaged, [map()]}
```

Result of a replay scan.

# `chained?`

```elixir
@spec chained?(Path.t()) :: boolean()
```

Is the journal under `dir` chained? True when `meta.json` says
`"chain": true`, and also when it does not but the journal still carries a
chain marker (see "Chain downgrade") -- such a journal reads as damaged.

# `last_offset`

```elixir
@spec last_offset(Path.t()) :: non_neg_integer()
```

Highest offset (id) present in a healthy replay, or 0 if empty/damaged.

# `resume_last_offset`

```elixir
@spec resume_last_offset(Path.t()) :: non_neg_integer()
```

`last_offset/1` for the owning Writer's resume; repairs a torn tail.

# `resume_scan`

```elixir
@spec resume_scan(Path.t()) :: result()
```

`scan/1` for the OWNING Writer at resume, which additionally REPAIRS a torn
tail.

The Writer must truncate the torn bytes before it appends again, or the next
record concatenates onto them and turns a recoverable tail into permanent
damage. It is the only caller that may: it holds the lock, and nothing else
is appending while it resumes.

# `scan`

```elixir
@spec scan(Path.t(), keyword()) :: result()
```

Replay all journal segments under `dir` in ascending order.

Returns `{:ok, records}` for a healthy or torn-tail-tolerated journal, or
`{:damaged, records}` when interior corruption was detected. In the damaged
case `records` are only the complete records preceding the corruption and
MUST NOT be surfaced downstream — callers map this to an error.

READS NEVER WRITE. A torn tail is ignored, not repaired: repairing is the
owning Writer's job (`resume_scan/1`). A reader computes the cut from a file
it read moments ago, so against a live session it can delete a record the
Writer already committed and counted — whose next append then lands past the
hole and leaves the session permanently `{:damaged, _}`. Read-only media is
the same hazard in a smaller way: the repair could only crash the reader.

---

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