The FROZEN conversational-tip predicate — the single contract implementation
of conversational?/1 and tip/2.
Frozen contract. A resume locates its point by a backward scan under this
predicate. Raxol.Agent.Journal.Records.Checkpoint.FileBackend validates a
stamped tip_offset against the same predicate, with its own copy of the
whitelist.
The predicate
conversational?(record) :=
record.kind == "event"
∧ record.family == "loop"
∧ record.type ∈ CONVERSATIONAL
tip(journal, branch := "main") :=
the record with the HIGHEST offset satisfying
(record.branch_id == branch ∧ conversational?(record))
(undefined on an empty/no-conversational branch → :no_tip)Frozen decisions this module encodes
- The grandfather clause. A record with no
"kind"reads as"event"; a record with no"branch_id"reads as"main". Every journal written before those keys existed therefore keeps its historical tip byte-for-byte. - The closure rule.
CONVERSATIONALis a whitelist and the ONLY door: every future record kind and every futurefamily: "loop"type is tip-excluded unless explicitly added here. Nothing becomes a tip by default. - The exclusions.
state_change,idle, everyfamily: "meta"event, every non-"event"kind, and — deliberately, though it isfamily: "loop"—wokenare excluded. A trailing checkpoint / meta event / idle marker /wokenmust never be selected as the tip. approval_requestedIS included — a pending approval is exactly where a resumed conversation must land (emittedfamily: "loop", not:meta).
This module is pure and reader-side only: it never touches the journal, the bus, or a writer. It reads decoded journal records (string-keyed maps) as produced by the tolerant Reader or by an independent raw decoder.
Summary
Functions
Is record a conversational loop event — a legal tip candidate?
The frozen CONVERSATIONAL whitelist, as a list of type strings.
The conversational tip of records on branch (default "main").
Functions
Is record a conversational loop event — a legal tip candidate?
true iff kind == "event" (grandfathered from absent), family == "loop",
and type ∈ CONVERSATIONAL. Everything else — checkpoints, family: "meta"
events, idle/woken/state_change loop events, unknown future kinds — is
false, by the closure rule.
@spec conversational_types() :: [String.t()]
The frozen CONVERSATIONAL whitelist, as a list of type strings.
@spec tip([map()], String.t()) :: {:tip, non_neg_integer()} | :no_tip
The conversational tip of records on branch (default "main").
Returns {:tip, offset} for the highest-offset record satisfying
branch_id == branch ∧ conversational?, or :no_tip when the branch has no
conversational record. records may arrive in any order — the tip is defined
by highest offset, not list position (a backward scan and a max_by agree).
tip(records) is shorthand for tip(records, "main"); every pre-branch_id
record reads as branch "main", so the default is grandfather-safe.