---
url: 'https://adk.nht.io/batteries/orchestration/store.md'
description: >-
  BYO persistence for plans and runs — and an unforgiving conformance suite that
  proves an implementation actually upholds the contract.
---

# The Store Contract

## LLM summary — orchestration PlanStore

* `PlanStore` covers plans AND runs. Reference implementation: `InMemoryPlanStore` from `/in_memory`.
* Plan side: `createPlan`, `clonePlan`, `appendOps` (refused unless `editable`), `readOps({sinceLamport?, throughRevision?})`, `readProvenance`, `transition`, `readState`, `list`.
* Run side: `claimRun(planId, expectedDigest, resumeRunId?)`, `appendRunEvents(planId, runId, events)` — ATOMIC as a batch — and `readRunEvents`.
* `transition` is the ONE atomic lifecycle op: proves state + `expectedDigest`, checks legality, persists `approval` in the SAME COMMIT for `reviewable→executable`. Returns the losing outcome; never throws.
* A store does NOT evaluate policy. The battery validates; the store commits.
* Settled means COMPLETED. `aborted`/`halted` MUST admit re-entry via `resumeRunId`.
* `runPlanStoreConformance(makeStore)` from `/conformance` — a vitest suite; `vitest` is an optional peer.

`PlanStore` is the single seam you must implement for durable persistence. `InMemoryPlanStore` is the reference implementation: mathematically correct, and completely useless for production for the obvious reason that memory disappears when the process exits.

## The split: the battery validates, the store commits

A store does **not** evaluate policy. It could not even if you wanted it to. Deciding "is an evaluator wired", "is this tool on the tier-C allowlist", or "does this reference taint a call arg" requires orchestrator domain knowledge that an external storage adapter cannot see. Shoving policy checks down into storage would force every backend implementor to reimplement the battery's validator from scratch.

So `transition` is deliberately **narrow**. It proves the plan is in the expected state at `expectedDigest`, checks that the target state is a legal successor, and applies it — persisting the `ApprovalRecord` in the **same commit** during a `reviewable → executable` move. It returns the losing outcome rather than throwing exceptions: callers that lost a race need to inspect what actually happened, not unpack an error trace.

The digest is what prevents concurrent corruption: the battery validates the plan's contents against digest D, and the store commits if and only if the plan remains at D.

## Two things that are easy to get subtly wrong

**`appendRunEvents` must commit the batch atomically.** The entire commit protocol depends on `node_settled`, every resulting `edge_taken`, and the updated `frontier_snapshot` landing in one indivisible write. A store that persists them across separate statements will eventually be killed between them. When that happens, the event fold reads a settled node whose child edges never made it to disk, corrupting the graph state permanently.

**"Settled" means COMPLETED.** `aborted` and `halted` are *interruption points*, not finality. The engine's interruption taxonomy classifies an aborted turn as resumable with its frontier preserved at the exact same digest. A store that treats every `run_settled` event as terminal makes `resumeRunId` answer `run_already_settled` for the exact recovery paths it was built to handle. Re-entry must be admitted after `aborted` or `halted`, and refused only after `completed`.

::: warning We are about to state an opinion
That second point is not hypothetical advice. The reference store shipped with that exact bug, and the conformance suite failed to catch it because its claim-run test checked only the `completed` branch — it validated only the half that already worked. A conformance suite that only ever passes is useless theatre; these assertions exist because someone already broke production.
:::

## Verify it

```typescript
import { runPlanStoreConformance } from '@nhtio/adk/batteries/orchestration/conformance'

runPlanStoreConformance(async () => new MyDurablePlanStore(connection))
```

It is a vitest suite (`vitest` is an optional peer) that rigorously exercises the edges of the contract where naive implementations silently fail:

* an append rejected in `reviewable` **and** in `executable` (`not_editable`)
* the settlement batch committing atomically, verified by killing the write between the two appends
* the **stale-approval interleaving** — actor A inspects `reviewable`@D1, actor B unfreezes, edits, and refreezes to D2, and actor A's approval targeting D1 is rejected
* `createPlan` rejecting duplicate identifiers
* `clonePlan` atomically generating an `editable`, unapproved, run-free plan bearing parent provenance
* **`claimRun` succeeding exactly once** under concurrency — two simultaneous workers race to claim; one gets `ok`, the loser gets `run_already_claimed` carrying the winner's `runId`
* `claimRun` **admitting re-entry after `aborted` and after `halted`**, and rejecting claims after `completed`
* round-tripped `frontier_snapshot` and `edge_taken` entries **preserving artifact HANDLES** — the restored execution frame returns with an `artifacts` table whose reader rebinds and reads successfully
* `readOps({throughRevision})` serving a historical prefix and rejecting requests for an unreached revision

That last group is where real durable backends routinely diverge from in-memory mocks, and where storage bugs stay invisible until production load hits.

## Artifacts persist as handles

An `ArtifactTable` attaches to `PendingFrame.artifacts`, `edge_taken.artifacts`, and `JoinState.arrivals[].artifacts`. It persists strictly as encoder **handles** — structured `{tag, locator}` pairs, never raw bytes — and rebinds upon resumption via `resolveSpoolReader`.

::: warning We are about to state an opinion
**Registering your durable store's reader resolver is load-bearing architecture, not optional hygiene.** In-memory and fetch resolvers register automatically; a durable resolver is entirely on you, because only your runtime holds the live connection that a serialized locator cannot carry across processes. Without it, a downstream `transform` executed after a resume will crash citing an unrecognized tag. It crashes loudly — which is correct — but the fix is wiring your registration at boot, not modifying the plan.
:::

## The lock is optional, and honest about it

`PlanLockFactory` is a BYO integration point used only during execution. If your deployment runs a single executor process, omit it entirely without penalty. Provide one — whether backed by Verrou or any distributed locking mechanism — and concurrent execution races become *unlikely rather than routine*.

Notice the phrase: unlikely, not impossible. It is a TTL lease **without a fencing token**. It cannot prevent a partitioned or GC-stalled process from running past lease expiration while a second executor legitimately takes over. Fixing that properly requires a monotonic epoch token issued with the lease, threaded through every persistent write, and verified by a store that rejects stale-epoch writes. Mandating that would complicate every single `PlanStore` implementation to accommodate distributed consensus. We chose not to lie: this seam is explicitly best-effort. The residual risk is handled where that failure mode belongs: per-node `onIndeterminate` policies and `replaySafe` flags.

Plan editing requires **no** locks at all. Op-log convergence handles concurrent authoring, and submit-time validation is what catches an incoherent result before execution ever begins.
