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

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.** `CONVERSATIONAL` is a whitelist and the ONLY door:
    every future record kind and every future `family: "loop"` type is
    tip-excluded unless explicitly added here. Nothing becomes a tip by default.
  * **The exclusions.** `state_change`, `idle`, every
    `family: "meta"` event, every non-`"event"` kind, and — deliberately, though
    it is `family: "loop"` — `woken` are excluded. A trailing checkpoint / meta
    event / idle marker / `woken` must never be selected as the tip.
  * **`approval_requested` IS included** — a pending approval is exactly where a
    resumed conversation must land (emitted `family: "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.

# `conversational?`

```elixir
@spec conversational?(map()) :: boolean()
```

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.

# `conversational_types`

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

The frozen CONVERSATIONAL whitelist, as a list of type strings.

# `tip`

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

---

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