The run lifecycle: create and step durable runs with no live Session process, the loop this package exists to package.
A step runs in ADR-0004 decision 3's order, and the order is the
contract: liveness check on the run record -> load (guarded) -> re-stamp
routes/invoke_types unconditionally (with the nil tripwire from
st-ADR-0064: the fields are pattern-matched nil before stamping, so an
upstream regression fails loudly here, not silently downstream) -> step
via Interpreter.handle_event/2 -> execute effects via the executor
seam -> consume :done and :budget_exhausted into run status -> assert
MachineState.internal_queue_empty?/1 -> persist.
Effect execution is at-least-once: a crash between step and persist
re-drives the same event and re-emits the same effects with identical
deterministic keys (st-ADR-0054 decision 3, st-ADR-0059), and this loop
never dedupes - idempotency is the consumer's. :done is the only path
to :completed (ADR-0004 decision 6); an event delivered to a terminal
run is discarded with a typed {:discarded, run} result, never an
exception and never a silent step.
Executor failures on actionable effects re-enter the chart as
error.communication events through Statifier.Interpreter.deliver_internal/5
(st-ADR-0039's seam), per st-ADR-0051's failed-communication row: the core
alone mints the planning-time execution-error events, before any effect is
emitted, so every failure an executor can report re-enters uniformly as
error.communication (ADR-0004
decision 4). Failures on observational effects are discarded. Re-entry is
single-wave per step: effects the re-entries emit are executed too, but
their failures are not re-entered again, so a deterministically failing
executor cannot loop this library.
Concurrent deliveries to one run are ordered by a pluggable per-run
serialization strategy (ADR-0004 decision 5): every entry point runs its
fetch-to-persist tail inside the strategy's
StatifierPersistence.Serialization.with_run/3, selected per call with
serialization: {module, config} and defaulting to
{StatifierPersistence.Serialization.AdapterLock, store} - the adapter's
own optional lock_run/3. A strategy refusal surfaces unchanged as
{:error, {:serialization, reason}}.
Summary
Types
This module's error vocabulary: the facade's arms, unflattened, plus the
{:budget_exhausted, payload} arm returned after a budget-exhausted step
or create has persisted its :failed run record, plus the serialization
strategy's own refusal, surfaced unchanged
({:serialization, :not_supported} from the default strategy over an
adapter with no lock_run/3).
A run's caller-supplied opaque key (ADR-0004 decision 2).
Functions
Creates a run: Statifier.Interpreter.initialize/2 (which cannot fail),
then the shared persist tail - effects through the executor seam,
:done/:budget_exhausted consumed into run status, quiescence
asserted, the record inserted with its encoded position.
Abandons a run: the only host-driven terminal transition (ADR-0004 decision 6). No interpreter is involved - abandonment is a host decision about the run, not a chart transition - so the stored position is left untouched and only the record's status and failure reason change.
Delivers one external event to a run, in ADR-0004 decision 3's order (the moduledoc quotes it).
Types
@type error() :: StatifierPersistence.Storage.error() | {:budget_exhausted, Statifier.Effect.BudgetExhausted.t()} | {:serialization, term()}
This module's error vocabulary: the facade's arms, unflattened, plus the
{:budget_exhausted, payload} arm returned after a budget-exhausted step
or create has persisted its :failed run record, plus the serialization
strategy's own refusal, surfaced unchanged
({:serialization, :not_supported} from the default strategy over an
adapter with no lock_run/3).
@type opt() :: {:executor, StatifierPersistence.Executor.t()} | {:routes, Statifier.MachineState.routes()} | {:invoke_types, Statifier.MachineState.invoke_types()} | {:initialize, keyword()} | {:serialization, {module(), term()}}
Options create/4 and step/5 accept:
executor:(required) - theStatifierPersistence.Executor.t/0every non-lifecycle effect is handed to, in list order.routes:- theStatifier.Send.Routes.t/0snapshot stamped onto the loaded position before the step; host-supplied per call, never read back from storage (st-ADR-0048). Defaults tonil, "no determination made".invoke_types:- theStatifier.Invoke.Types.t/0snapshot, stamped the same way (st-ADR-0051). Defaults tonil, "the built-in set only".initialize:(create/4only) - passed toStatifier.Interpreter.initialize/2unchanged.serialization:- the{module, config}per-run serialization strategy the fetch-to-persist tail runs inside (ADR-0004 decision 5;fail/4accepts it too). Defaults to{StatifierPersistence.Serialization.AdapterLock, store}.
@type run_id() :: StatifierPersistence.Storage.Adapter.run_id()
A run's caller-supplied opaque key (ADR-0004 decision 2).
Functions
@spec create( store :: StatifierPersistence.Storage.t(), run_id :: run_id(), machine :: Statifier.Machine.t(), opts :: [opt()] ) :: {:ok, StatifierPersistence.Run.t(), Statifier.MachineState.t()} | {:error, error()}
Creates a run: Statifier.Interpreter.initialize/2 (which cannot fail),
then the shared persist tail - effects through the executor seam,
:done/:budget_exhausted consumed into run status, quiescence
asserted, the record inserted with its encoded position.
Create-exactly-once rests on the adapter's atomic :run_exists refusal
(ADR-0004 decision 2), not on a pre-check here: creating an existing
run_id returns {:error, :run_exists}.
A create whose initialize/2 exhausts its macrostep budget persists a
:failed run with no position blob (there is no quiescent position to
store - ADR-0004 decision 1) and then returns
{:error, {:budget_exhausted, payload}}, so the caller sees both the
durable state and the reason.
@spec fail( store :: StatifierPersistence.Storage.t(), run_id :: run_id(), reason :: String.t(), opts :: keyword() ) :: {:ok, StatifierPersistence.Run.t()} | {:discarded, StatifierPersistence.Run.t()} | {:error, error()}
Abandons a run: the only host-driven terminal transition (ADR-0004 decision 6). No interpreter is involved - abandonment is a host decision about the run, not a chart transition - so the stored position is left untouched and only the record's status and failure reason change.
A terminal run is discarded, same as step/5: {:discarded, run}.
reason is the short string stored as the run's failure - keep it a
prefixed, console-readable reason, not an inspect dump.
opts accepts serialization: only - the same {module, config}
strategy create/4 and step/5 take, with the same default.
@spec step( store :: StatifierPersistence.Storage.t(), run_id :: run_id(), machine :: Statifier.Machine.t(), event :: Statifier.Event.t(), opts :: [opt()] ) :: {:ok, StatifierPersistence.Run.t(), Statifier.MachineState.t()} | {:discarded, StatifierPersistence.Run.t()} | {:error, error()}
Delivers one external event to a run, in ADR-0004 decision 3's order (the moduledoc quotes it).
An event delivered to a terminal run returns {:discarded, run} from the
run record alone, before any position decode. handle_event/2's
{:error, :not_running} arm is the structural backstop for a run record
whose :active status lies about a terminal stored position: it discards
too, and repairs the record's status to :completed on the way out.