Skip to content
4 min read · 730 words

The Lifecycle

Edits are refused in executable. The only way back is unfreeze, which lands the plan in editable — and the only route out of editable runs through the gate again.

Three states. You mutate a plan in editable, inspect it in reviewable, and execute it in executable. That is the entire topology. Every illegal transition triggers an explicit, model-addressed error rather than a silent no-op: mutating an executable plan, freezing an editable plan that fails submit checks, gating an editable plan directly, or attempting to run one that is merely reviewable.

The gate is the transition

The permission gate is the reviewable → executable transition. This is the single most load-bearing decision in the lifecycle, and it exists because boolean flags are a broken way to model authorization.

The prevailing pattern in software is treating approval as a boolean column on a mutable record. That immediately manufactures two independent facts: "this plan was approved" and "this plan may run." Once you have two facts, you need synchronization code to keep them from contradicting each other. Every execution pathway must remember to check the flag. Every mutation pathway must remember to clear it. When someone inevitably adds a code path that forgets either check, unapproved or modified plans execute in production.

Collapsing the gate into the state transition eliminates the failure mode by construction. There is no flag to inspect. A plan that is not in executable cannot run because the engine refuses to touch it:

typescript
// A frozen but ungated plan simply refuses to run.
await orchestration.executePlan('my-plan', options)
// throws: cannot run plan "my-plan": not_executable

Re-gating is equally mechanical. You cannot edit a plan while it sits in reviewable or executable; you must unfreeze it back to editable. From editable, every legal route back to executable must cross the gate again. No defensive application logic has to track mutations or clear cached permissions; the state machine closes the forgot-to-check failure mode outright.

Freezing

freezePlan folds the op log, runs every submit-time check against the folded graph, and invokes the store's transition only when zero issues are blocking:

typescript
const { ok, issues } = await orchestration.freezePlan('my-plan')
if (!ok) {
  // Every issue names the node and the fix — these are read by a model, not just logged.
  for (const issue of issues) console.log(issue.code, issue.message)
}

The cryptographic content digest is what makes this commit race-free. Validation evaluates the plan at digest D. The store commits the transition only if the plan is still at digest D when the write lands. If a concurrent edit appends an op while validation is running, the digest changes and the transition aborts. An edit cannot slip past an already-passed check.

Issues carry one of two severities. A blocking issue halts the freeze outright; an advisory issue is surfaced to the caller and permits the transition. A duplicate edge ID, for example, is advisory during the op-log fold—rejecting it there would make graph reduction order-dependent—but becomes strictly blocking at freeze.

One plan, one run, ever

A plan ID admits at most one run across its entire lifetime. Not "one run at a time"—one run, ever. That invariant is enforced by claimRun in the durable store, never by an advisory distributed lock. Deployments can omit the lock; the guarantee cannot rest on optional infrastructure.

Run outcomes belong to the run, not to the plan lifecycle. The plan remains in executable, and the run fold reports what happened:

OutcomeMeaningResumable
completedRan to the end.No — it is finished.
abortedStopped short (turn abort, unhandled node failure).Yes, via resumeRunId.
haltedCannot proceed (an unsatisfiable join, say).Yes, once the cause is addressed.

aborted and halted are pauses, not conclusions. Resumption occurs entirely within executable, restarting against the exact same content digest. Because the digest cannot mutate beneath an interrupted run, the persisted execution frontier remains trustworthy.

typescript
const interrupted = await orchestration.executePlan('my-plan', { input, signal })
// interrupted.outcome === 'aborted', interruption.kind === 'turn_abort'
// interrupted.frontier.frames tells you where it stopped.

const finished = await orchestration.executePlan('my-plan', {
  input,
  resumeRunId: interrupted.runId,   // re-enter the SAME run
  invokeCall,
})

Recovery is a clone

A halting failure does not reopen the plan for editing. Allowing someone to edit a partially executed plan would invalidate the approval the run operated under while mutating history after the fact. Recovery is clonePlan. It produces a fresh editable, unapproved, completely cold plan that carries durable provenance linking it to its parent:

typescript
await orchestration.store.clonePlan('my-plan', 'my-plan-v2')
// v2 is editable, unapproved, and has no run of its own.

When rendered to prose for operator review, the clone explicitly warns that executing it will repeat side effects the parent already committed, listing each completed node by name. That friction is deliberate: a clone is not an edit. It is an entirely distinct plan that happens to share history with one that already touched the outside world.