CovenDocs

Automations lifecycle

The proposed coven.automations.v1 state machines for definitions, occurrences, runs, and attempts, their invariants, and the states the daemon records today.

4 min read

The coven.automations.v1 contract defines four state machines, and the invariants they preserve, in spec/coven-automations/v1/state-machines.json. The diagrams and tables on this page render from a byte-for-byte copy of that file at the reviewed upstream commit, so they cannot drift from it.

Proposed contract, partial implementation

The contract is proposed, not certified. Its conformance manifest records releaseState: "proposed" and productionReady: false. At the reviewed commit, upstream tests compare only the occurrence transitions of an operator retry against these machines; nothing checks the rest of the daemon against them. The daemon records a subset of these states, and in places a different one; each section below says which. Branch on the values the daemon returns, described in Coven automations.

Reading the machines

Clients never write state. Command handlers, the scheduler, the dispatcher, and workers commit every transition:

  • on names the cause.
  • actor names who may commit the transition.
  • guard names the condition that must hold.

Terminal states are drawn in bold, and no transition leaves one. Acceptance, dispatch, and success are separate states: an accepted run has not launched anything, a dispatched attempt has not proved anything, and only runtime evidence settles a run succeeded.

Definition

Rendering diagram…
definition.v1: starts in draft; terminal states are bold.

States: draft, paused, active, disabled, invalid, tombstoned (terminal).

definition.v1 transitions
TransitionOnActorGuard
draft → pauseddefinition_revise_committedcommand_handlerdefinition validated; revision incremented
draft → tombstoneddefinition_tombstonedcommand_handler
paused → activedefinition_activatedcommand_handlerfamiliar, authority, and runtime requirements validated
paused → disableddefinition_disabledcommand_handler
paused → tombstoneddefinition_tombstonedcommand_handler
active → pauseddefinition_pausedcommand_handler
active → disableddefinition_disabledcommand_handler
active → invaliddefinition_invalidatedcommand_handlera revise committed an invalid body or dependencies (familiar/runtime/authority) became unsatisfiable
active → tombstoneddefinition_tombstonedcommand_handler
disabled → pauseddefinition_reenabledcommand_handlerexplicit re-enable; disabled never auto-reactivates
disabled → tombstoneddefinition_tombstonedcommand_handler
invalid → pauseddefinition_revise_committedcommand_handlera revise repaired the body and validation passed
invalid → tombstoneddefinition_tombstonedcommand_handler

What the daemon records today:

  • lifecycleState is draft, paused, active, disabled, or invalid. A definition created with definition.create.v1, or imported from a legacy source, starts in draft, and definition.revise.v1 moves it to paused.
  • Tombstoning sets tombstonedAt instead of a lifecycle value. Events report the change as tombstoned.
  • invalid comes only from the one-time migration of a stored legacy definition that cannot be digested. No command moves an active definition to invalid.
  • No versioned command re-enables a disabled definition yet: definition.activate.v1 requires paused.

Occurrence

Rendering diagram…
occurrence.v1: starts in planned; terminal states are bold.

States: planned, eligible, claimed, dispatching, running, recovering, recovery_required, succeeded (terminal), failed (terminal), cancelled (terminal), timed_out (terminal), skipped (terminal), superseded (terminal).

occurrence.v1 transitions
TransitionOnActorGuard
planned → eligibleeligibility_passedscheduler
planned → skippedmisfireschedulermisfire policy disposes this slot (e.g. overlap_forbid, paused, invalid)
planned → supersededdefinition_supersededschedulera newer revision replaced the definition before this slot was claimed
planned → cancelledcancel_acknowledgedcommand_handlercancellation command committed before any claim
eligible → claimedclaimschedulercompare-and-set on state='eligible' or 'planned' with lease set; increments fence generation
eligible → skippedmisfirescheduler
eligible → supersededdefinition_supersededscheduler
eligible → cancelledcancel_acknowledgedcommand_handler
claimed → plannedoperator_retrycommand_handlerattempt.retry.v1 names the latest immutable failed attempt of an ordinary held run; original deadline remains open, fewer than ten attempts exist, quarantine is released, and no cancellation or stop ownership is unresolved; opens a new adopted attempt
claimed → plannedautomatic_retrydispatcherproven pre-ownership failure is retryable under the pinned policy; opens the next attempt with prior disposition and backoff; the original run deadline still bounds dispatch
claimed → failedrun_settleddispatcherknown pre-ownership failure has no automatic retry or operator hold window, or its held run deadline expires; prior terminal attempt stays immutable
claimed → dispatchingattempt_adopteddispatcherattempt created with adoptionKey; fence generation matches the claim
claimed → recoveringlease_expiredschedulerleaseExpiresAt < now and no runtime evidence recorded
claimed → cancelledcancel_acknowledgedcommand_handlercancellation reconciled before dispatch
dispatching → runningruntime_starteddispatcherruntime acknowledged the session start (attempt started)
dispatching → failedlaunch_refuseddispatcherlaunch failed deterministically before any side effect; occurrence settles failed with reason launch_refused
dispatching → recoveringlease_expiredschedulerno runtime evidence before lease expiry
dispatching → cancelledcancel_acknowledgedcommand_handlerno attempt reached started; otherwise cancellation waits for reconciliation
running → plannedoperator_retrycommand_handlerattempt.retry.v1 names the latest immutable failed attempt of an ordinary held run; original deadline remains open, fewer than ten attempts exist, quarantine is released, and no cancellation or stop ownership is unresolved; opens a new adopted attempt
running → succeededrun_settleddispatcherruntime evidence proves completion; receipt recorded
running → failedrun_settleddispatcherruntime evidence proves failure and no operator hold window remains, or the held run deadline expires; prior terminal attempt stays immutable
running → timed_outtimeoutschedulerperRunMinutes elapsed without settled evidence
running → cancelledcancel_acknowledgedcommand_handlercancellation acknowledged by runtime or reconciled by recovery
running → recovery_requiredevidence_lostschedulerruntime lost mid-run: outcome cannot be determined automatically
recovering → failedrecovery_determinedschedulerno side effects were possible (sideEffectClass bounds proven)
recovering → recovery_requiredrecovery_ambiguousschedulerside effects cannot be ruled out; requires explicit operator recovery
recovery_required → failedoperator_recovery_failedcommand_handleroccurrence.recover.v1 with evidenceDetermination=failed_deterministic
recovery_required → dispatchingoperator_recovery_attemptcommand_handleroccurrence.recover.v1 with evidenceDetermination=retry_with_new_attempt; opens a new attempt carrying priorDisposition=ambiguous

What the daemon records today:

  • Occurrences are planned, claimed, running, or recovery_required, and settle succeeded, failed, cancelled, or skipped. skipped marks a slot collapsed by the misfire policy.
  • eligible is a read view over due planned occurrences (occurrence.list.v1 with view: "eligible"), not a stored state.
  • The daemon does not record dispatching, recovering, or timed_out on an occurrence. A claim moves straight to running when the runtime starts, a claim whose lease expires before dispatch settles failed, and a timeout settles the occurrence failed.
  • superseded appears only on occurrences migrated from the legacy store, where every legacy skipped row became superseded.
  • occurrence.recover.v1 with retry_with_new_attempt returns the occurrence to planned for dispatch rather than to dispatching.
  • occurrence.cancel.v1 and attempt.cancel.v1 settle a planned or claimed occurrence cancelled directly, before anything is dispatched, as the contract's cancel_acknowledged transitions allow.
  • When an ordinary run is held for an operator retry, its occurrence stays claimed (the attempt failed before launch) or running (it failed after start). attempt.retry.v1 returns it to planned, the contract's operator_retry transitions; if the run's deadline passes first, it settles failed.

Run

Rendering diagram…
run.v1: starts in accepted; terminal states are bold.

States: accepted, running, succeeded (terminal), failed (terminal), cancelled (terminal), timed_out (terminal), ambiguous (terminal).

run.v1 transitions
TransitionOnActorGuard
accepted → runningattempt_starteddispatchercommand adoption committed before any side effect; currentAttemptId set
accepted → failedlaunch_refuseddispatcherevery attempt failed deterministically before evidence
accepted → cancelledcancel_acknowledgedcommand_handler
running → succeededattempt_settleddispatcheran attempt settled succeeded; receipt recorded
running → failedattempt_settleddispatcheran attempt settled failed, retry policy opens no further attempt, and no operator hold window remains; held ordinary runs retain running until cancellation or their original deadline
running → timed_outtimeoutscheduler
running → cancelledcancel_acknowledgedcommand_handler
running → ambiguousattempt_ambiguousdispatcherthe current attempt settled ambiguous and no operator recovery has resolved it

What the daemon records today, in the run's status field:

  • A run is created running. The daemon has no separate accepted status.
  • Runs settle succeeded, failed, or cancelled. A timeout settles the run failed, and its attempt records timed_out.
  • Cancelling an attempt that is still adopted and waiting for its retry settles the attempt and its run cancelled with no session to stop.
  • An ordinary run whose attempt failed with no automatic retry left stays running, held for an operator retry, until its deadline; it then settles failed. The failed attempt stays failed either way. See Operator retry.
  • The daemon never records ambiguous on a run. Ambiguous evidence either settles the run failed or leaves it running with an ambiguous attempt while its occurrence waits in recovery_required; see Manual runs and health.

Attempt

Rendering diagram…
attempt.v1: starts in adopted; terminal states are bold.

States: adopted, dispatching, started, observing, succeeded (terminal), failed (terminal), cancelled (terminal), timed_out (terminal), ambiguous (terminal).

attempt.v1 transitions
TransitionOnActorGuard
adopted → dispatchingdispatchdispatcherdispatch generation incremented; exactly one dispatch in flight per attempt
adopted → cancelledcancel_acknowledgedcommand_handlercancellation committed before dispatch
dispatching → startedruntime_starteddispatcherruntime session bound; a second session bind is ILLEGAL_TRANSITION
dispatching → failedlaunch_refuseddispatcherdeterministic pre-side-effect refusal
dispatching → ambiguousdispatch_unconfirmeddispatcherdispatch sent but no deterministic ack before lease expiry; terminal for the attempt
dispatching → cancelledcancel_acknowledgedcommand_handlercancellation acknowledged before runtime start
started → observingevidence_stream_openworkeroutput/event cursors established
started → failedrun_settledworkerterminal failure evidence without an observing phase; terminal attempt remains immutable even when the ordinary run holds for operator retry
started → succeededrun_settledworkerterminal evidence without an observing phase
started → ambiguousevidence_lostschedulersession vanished before evidence
observing → succeededrun_settledworkerverified completion evidence
observing → failedrun_settledworkerverified failure evidence
observing → timed_outtimeoutschedulerperRunMinutes elapsed
observing → cancelledcancel_acknowledgedcommand_handlercancellation acknowledged by runtime
observing → ambiguousevidence_lostschedulerheartbeat/lease evidence expired mid-observation

What the daemon records today:

  • All nine attempt states, and a terminal attempt row can be neither updated nor deleted.
  • A few transitions the contract does not list: a dispatch rolled back before launch returns the attempt to adopted, and a confirmed cancellation or timeout can settle a dispatching or started attempt directly.

Invariants

  1. adoption-before-side-effects: Command adoption commits before any consequential side effect is issued; a run is accepted (run.v1 accepted) before dispatch begins.
  2. one-fence-one-run: One occurrence fence generation cannot own two accepted runs: acceptance requires a compare-and-set on the occurrence fence generation, and a second acceptance with the same generation is a typed conflict.
  3. one-attempt-one-session: One attempt cannot bind two runtime sessions; a second session bind on the same attempt is ILLEGAL_TRANSITION and commits nothing.
  4. terminal-no-regress: Terminal states never regress; no transition in any machine above leaves a terminal state.
  5. no-evidence-no-success: Absence of runtime evidence can never become success; the only transitions out of evidence loss are failed, recovery paths, ambiguous, or timed_out.
  6. cancellation-is-a-request: Cancellation is a request until acknowledged or reconciled; CANCEL_PENDING is returned while a running attempt has not acknowledged, and the occurrence settles cancelled only after acknowledgment or reconciliation.
  7. retry-new-attempt: Retry creates a new attempt with attemptNumber incremented and priorDisposition set; the prior attempt's record is never rewritten.
  8. ambiguous-never-auto-retried: Ambiguous mutating work is not automatically retried; only occurrence.recover.v1 with an explicit operator determination can open work after an ambiguous disposition.
  9. revision-pins-history: Definition revision changes never rewrite historical occurrences, runs, attempts, or receipts; each pins the automationRevision and definition digest it was created and executed against.
  10. tombstone-preserves-history: Deleting a definition tombstones it without erasing required history; tombstoned definitions never plan, claim, or run, and their records remain readable.

These are the contract's normative statements. Upstream's conformance manifest lists a state-machine-property-tests suite for them, but that suite does not exist at the reviewed commit, so read them as the design target rather than verified daemon behavior. Upstream's vector-driven suites cover parts of them: attempt terminal immutability, run terminal monotonicity, occurrence fence uniqueness, occurrence lease recovery, and definition lifecycle transitions (without draft or invalid).

Was this page helpful?No

Last updated on

On this page