# ADR 0182: A durability port exposes step, sleep and wait over the store

> Source: https://github.com/max-listov/stitchkit/blob/master/docs/decisions/0182-a-durability-port-exposes-step-sleep-and-wait.md

---

# ADR 0182 — A durability port exposes step, sleep and wait

## Decision

`createAgentRuntime({ durability: true })` provides optional `step`, `sleep`
and `waitFor` functions to runtime-tool handlers mounted with `mountAgent`.
A factory may supply a host-owned port instead; it receives the store,
conversation, run, tool name, tool call identity and abort signal. Plain mounts
without a runtime port leave these functions undefined.

The local implementation records JSON step results, sleep deadlines and event
deliveries in the canonical store ledger. The runtime scopes it by the tuple
of run id, tool name and tool call id. Standalone ports use the supplied runId
as their durable scope. Equal keys in distinct store objects never share
in-flight work. Incremental reads advance through the append-only ledger.

A completed step returns its recorded result on replay. The host must make
external effects idempotent: a crash between an effect and its result record
can repeat that effect. The port is not an exactly-once transaction across
arbitrary services, and it does not acquire a cross-process lease.

`sleep` holds a lightweight, unreferenced timer with bounded timer intervals;
`waitFor` holds a promise. These are local waits, not automatic process
suspension. Cancellation releases local waiters but preserves durable facts.
After a host observes the old executor has exited, it may call
`runtime.recover` with a replay-safe decision. Stable tool call identities are
required for replay to find the previous scope.

## External delivery

The host can supply `subscribe(wake)` when constructing the local port to wake
waiters after another process appends a delivery. The port reads the ledger
after subscribing, so a delivery racing registration is not lost. The returned
unsubscribe runs on every completion or cancellation. Without this notification,
external writes become visible on reconstruction; same-object-store `deliver`
wakes local waiters directly. No hidden polling or workflow SDK is installed.

## Validation

Step results are lossless JSON values. Non-finite numbers, negative zero, undefined,
sparse arrays, accessors and non-plain objects are refused rather than coerced by
JSON.stringify. Effect-only bodies return null explicitly. First execution and
replay return detached snapshots; consumer mutation cannot change the ledger or
the next replay. Validation happens after the body, so a rejected result cannot
roll back an external effect and does not replace host idempotency.

A child Bun process runs a real mounted runtime tool against SQLite and exits
after the first recorded step. A second process recovers the same run and
executes only the second effect. Separate tests cover store isolation,
concurrent deduplication, cancellation, corrupt records and ledger cursor reads.
