Skip to content

Adds the run lifecycle and the stepper loop - #12

Merged
johnnyt merged 6 commits into
mainfrom
sp-4an.2-run-lifecycle-stepper
Aug 22, 2026
Merged

Adds the run lifecycle and the stepper loop#12
johnnyt merged 6 commits into
mainfrom
sp-4an.2-run-lifecycle-stepper

Conversation

@johnnyt

@johnnyt johnnyt commented Aug 22, 2026

Copy link
Copy Markdown
Member

Why

Hosts running charts that span days or survive deploys currently hand-roll
the load -> step -> execute -> persist loop, the storage guard, and the
crash semantics. This branch ships that loop as the library: the run
lifecycle (sp-4an.2's charter scope bullets 2 and 3), on top of the
sp-4an.1 storage substrate.

What

Five phases, one commit each, recorded up front as ADR-0004:

  • ADR-0004: run lifecycle contracts (record first, code after).
  • Run records on the storage contract: insert_run/2, fetch_run/2,
    update_run/2 adapter callbacks over an opaque run_record (nullable
    position_blob), the identity-guarded facade functions, and conformance
    coverage downstream adapters inherit.
  • The loop: Runs.create/4 and Runs.step/5 in the contract order -
    liveness check before any decode, guarded load, unconditional
    routes/invoke_types re-stamp behind the st-ADR-0064 nil tripwire, step,
    effects to the Executor seam in list order (:done /
    :budget_exhausted consumed by the lifecycle), quiescence assertion,
    persist. Terminal delivery is a typed {:discarded, run}.
  • Failure semantics: executor failures on actionable effects re-enter as
    error.communication via Interpreter.deliver_internal/5 (single-wave;
    this package never mints error.execution - st-ADR-0051's table),
    budget exhaustion persists a :failed run with the prior blob intact,
    Runs.fail/3,4 records host abandonment, and the at-least-once contract
    has an executable proof (injected persist failure re-drives the same
    event with identical deterministic effect keys).
  • Pluggable per-run serialization: the Serialization behaviour
    (with_run/3), the AdapterLock default over the new optional
    lock_run/3 adapter callback (ADR-0003 amended), InMemory's
    token-checked per-run lock, and concurrency tests that distinguish
    serial orders from lost-update interleavings.

Notes

  • Full mix quality green on every commit and after the rebase: 61 tests,
    94.1% coverage (floor 90), dialyzer clean. Doctor/Gettext/Sobelow are
    not installed in this project (standing configuration, not this branch).
  • Gate attestation wiring is Wires gate attestation to mix gate.verify #11 (sp-7cu, separate branch); until it
    merges this branch has no gate.attest line, so mix gate.verify was
    run directly alongside the manifest gate - green.
  • The epic description's "invoke start failure maps to error.execution"
    line was corrected on the bead: st-ADR-0051 classifies a registered
    handler failing to reach its service as error.communication, and
    planning-time error.execution never reaches the executor seam. The
    code follows upstream's record.
  • Re-entry origins for :cancel / :cancel_invoke / :autoforward have
    no upstream failed-communication precedent; they reuse existing
    Cause.origin() arms matching each effect's own identity fields
    (documented in the code) - worth a second opinion.
  • Deferred manual verification items from the plan's loop run are listed
    at the bottom of docs/plans/260822-sp-4an.2.1-run-lifecycle-executor-seam-stepper.md.

Closes sp-4an.2.1

Plans sp-4an.2.1 in five phases: ADR-0004 recording the lifecycle
contract, run records on the adapter behaviour and guarded facade,
the executor seam with the happy-path loop, failure semantics
(error.communication re-entry per st-ADR-0051, budget exhaustion,
fail/4), and the pluggable per-run serialization strategy with the
adapter lock default.

Docs only; the quality gate does not apply to this diff.

Refs: sp-4an.2.1
Phase 1 of the sp-4an.2.1 plan. Six decisions: the run record owns
its current position; opaque caller-supplied run keys and total
statuses; the loop order as contract with at-least-once semantics;
the executor seam over the core effect vocabulary with uniform
error.communication re-entry (st-ADR-0051); pluggable per-run
serialization with the adapter lock default; chart-driven
completion. Indexes the record and checks off the phase.

Docs only; the quality gate does not apply to this diff.

Refs: sp-4an.2.1
Phase 2 of the sp-4an.2.1 plan, encoding ADR-0004 decisions 1-2.
The adapter behaviour gains insert_run/2, fetch_run/2, and
update_run/2 over an opaque run_record (nullable position_blob),
with :run_exists and :run_not_found arms; InMemory implements them
with the exists-check atomic with the write. The facade adds
insert_run/5, update_run/5, fetch_run/2, and the identity-guarded
load_run_position/3 with the :run_position_missing arm; position:
:skip carries the stored blob forward on update. The conformance
suite covers the new callbacks so downstream adapters inherit the
checks.

Refs: sp-4an.2.1
Phase 3 of the sp-4an.2.1 plan, encoding ADR-0004 decisions 3, 4,
and 6. Runs.create/4 initializes a chart into a durable run;
Runs.step/5 drives the loop in the contract order: liveness check
on the run record before any decode, guarded load, unconditional
routes/invoke_types re-stamp behind the st-ADR-0064 nil tripwire,
handle_event, effects to the Executor seam in list order (:done
and :budget_exhausted consumed by the lifecycle), quiescence
assertion, persist. Terminal delivery discards with a typed
{:discarded, run}; the Executor behaviour accepts a module or an
arity-2 fun. Executor failures are collected without re-entry and
budget exhaustion is handled minimally; Phase 4 owns both.

Refs: sp-4an.2.1
Phase 4 of the sp-4an.2.1 plan, completing ADR-0004 decisions 3,
4, and 6. Executor failures on actionable effects re-enter the
chart as error.communication through Interpreter.deliver_internal
(st-ADR-0051's failed-communication row; this package never mints
error.execution), single-wave per step, with origins reusing
upstream's own Cause.origin arms. Budget exhaustion persists a
:failed run with the prior blob intact and returns a typed error.
Runs.fail/3,4 records host abandonment via the new blob-preserving
Storage.update_run_status/4. The at-least-once contract gains its
proof: a failed persist re-drives the same event and re-emits
effects with identical deterministic keys.

Refs: sp-4an.2.1
Phase 5 of the sp-4an.2.1 plan, encoding ADR-0004 decision 5. The
Serialization behaviour carries the ordering guarantee (with_run/3
bodies for one run id never overlap); the lifecycle runs every
fetch-to-persist tail inside it. The default AdapterLock strategy
delegates to the new optional lock_run/3 adapter callback -
recorded as a dated ADR-0003 amendment - which InMemory implements
with a token-checked per-run lock released on any exit. The
conformance suite checks no-overlap and release-after-raise when
an adapter exports the callback; a job-queue strategy can swap in
later with no loop change.

Refs: sp-4an.2.1
@johnnyt
johnnyt merged commit de73210 into main Aug 22, 2026
1 check passed
@johnnyt
johnnyt deleted the sp-4an.2-run-lifecycle-stepper branch August 22, 2026 14:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant