One gen_statem per owned vnode (implementation.md §6.3): registered under
the bounded-atom name minted by Fief.Seam.vnode_name/2 (the manager
mints it in its own — sim-bound — process and passes it in), holding
{impl_module, impl_state} and running impl callbacks in-process. A vnode
serializes at its agent; impl exceptions crash the agent and the manager's
restart cold-starts it (implementation.md §2).
States: :serving, :donor (lost ownership, open session, impl draining).
The impl state persists across handoff transitions within one agent
lifetime; handoff_out/handoff_in are delivered in
authoritative-observation order.
What this process enforces (implementation.md §6.2)
- Ownership validation before every
handle_message: an ETS read ofFief.Node's published state plus the cached table row — the node's own last-observed truth. The gates, in order: no provable live lease (lease_live?false — the arbiter said:expired, or the node fenced) → the message is dropped silently, a fencing node must be inert; live lease but not(row.owner == me and :serving)→ reply{:moved, epoch, delta}. What this deliberately does NOT catch: an authority-side move this node has not yet observed — the old owner keeps serving until it observes the bump (hint, poll, or first peer message). That is design §6.3's flip window (freeze happens on observation, step 2), bounded by the live-lease gate, and per-key authority during the window is the impl's obligation via the session protocol. The epoch in:msgenvelopes is diagnostics, never an equality check (design §5.4). - The session envelope:
handle_peerpayloads are delivered only when the session matches the agent's authoritative view — the session recorded at its last handoff observation. A lower-epoch session is dropped outright; an unknown session forces an authority re-read (hints accelerate, never authorize — design §6.3 step 3), and only an authoritative match delivers. A donor that first learns of the transfer from a verified peer message freezes right there:handoff_outruns before the impl sees the payload (the spec'sServePullrefresh-and-freeze). Dropped messages are never acknowledged — the transport half of ⊨ rule 6. - Settle only via
Fief.Vnode.report_drained/1— relayed to the settle-relay name; seeFief.Vnode. - Fencing:
{:fief_fence, deadline_ms}from the manager runs the advisoryhandle_fence/2; if it returns, the agent stops normally — the manager's deadline kill (:brutal_kill) happens regardless and is the only bounded mechanism. - The impl-message channel (M6 contract change): non-framework
messages — impl-process reports,
:DOWNfor impl-held monitors, the impl's own seam-timer{:timeout, ref, msg}s — are delivered to the impl's optionalhandle_info/2under the same checkpoint discipline; seeFief.Vnode.handle_info/2for the exact routing rule.
Follows THE M3 process discipline: sim context bound at init, every event
handling ends with the :handled checkpoint via try/after, no
:next_event, no raw timers, stale-timer catch-all.