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/2returns{: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 offsetHEADanchors, 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.jsonsays 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.
Summary
Functions
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.
Highest offset (id) present in a healthy replay, or 0 if empty/damaged.
last_offset/1 for the owning Writer's resume; repairs a torn tail.
scan/1 for the OWNING Writer at resume, which additionally REPAIRS a torn
tail.
Replay all journal segments under dir in ascending order.
Types
Functions
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.
@spec last_offset(Path.t()) :: non_neg_integer()
Highest offset (id) present in a healthy replay, or 0 if empty/damaged.
@spec resume_last_offset(Path.t()) :: non_neg_integer()
last_offset/1 for the owning Writer's resume; repairs a torn tail.
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.
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.