Raxol.Agent.Journal.FileStore.Reader (Raxol Journal v2.8.0)

Copy Markdown View Source

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.

Summary

Types

Result of a replay scan.

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

result()

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

Result of a replay scan.

Functions

chained?(dir)

@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(dir)

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

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

resume_last_offset(dir)

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

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

resume_scan(dir)

@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(dir, opts \\ [])

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