Durable Execution Specification¶
Status: implemented (Task 6). TypeScript implementation in
packages/driver/src/durability/ (events.ts, log.ts, recovery.ts).
1. Model¶
Execution state is an append-only, ordered event log. Every outcome the runtime
must survive a crash is a typed, replayable event with a monotonically
increasing seq:
| Event kind | Persists | Written |
|---|---|---|
transaction |
graph hash, run/thread IDs, state version, read/write paths, patches | after commit |
effect_intent |
idempotency key, task/effect id, input hash, state version | BEFORE an external call |
effect_receipt |
idempotency key, task id, receipt, outcome | AFTER the call, BEFORE committing the patch |
retry_decision |
task id, attempt, decision, backoff | at each retry decision |
interrupt |
run/thread id, value, checkpoint hint | at interrupt |
stream_cursor |
run/thread id, cursor, terminal | at stream advances |
snapshot |
canonical state + state hash + base seq | periodically, for compaction |
2. Confirmed-effect ordering (never repeat a side effect)¶
For every external effect the durable order is:
- append
effect_intent(durable intent), - execute the external call,
- append
effect_receipt, - append the resulting
transaction.
Recovery treats an idempotency key as confirmed only when BOTH its intent and receipt are present. A crash: - before intent → nothing recorded, task reruns; - after intent, before receipt → key unconfirmed, task must not be re-executed blindly (the effect may have happened); - after receipt, before commit → effect confirmed, patch not yet applied, replay applies it; - after commit → transaction replays to canonical state.
UNKNOWN_COMMIT is never retried blindly: recovery inspects the log, resolves
the outcome from the persisted receipt, and either continues or re-drives with
the original idempotency key.
3. Backends¶
DurableLog interface with two implementations:
MemoryLog— in-process, for unit tests and non-durable modes;SqliteLog—node:sqlitefile-backed; a fresh instance over the same file recovers all prior events (verified cross-instance in the test suite).
node:sqlite is loaded lazily via createRequire (it is a Node built-in that
vite-node cannot statically externalize; a type-only import keeps the module
graph clean).
4. Snapshots and compaction¶
recover(log)rebuilds canonical state: starts at the newestsnapshot(empty state if none), then replaystransactionpatches in seq order.compact(log, snapshot)appends the snapshot and truncates everything strictly before the snapshot's own seq; because the snapshot subsumes all prior events, recovery from the compacted log equals recovery from the full log (asserted by the test suite, including a file-backed SQLite persistence check).
5. Testing¶
The failure-injection suite (packages/driver/test/durability/recovery.test.ts)
crashes the durable lifecycle at every boundary: before intent, after intent
before receipt, after receipt before commit, after commit, plus compaction, and
both backends. Every test asserts the confirmed-effect map and the replayed
canonical state.
6. Time travel (replay / fork)¶
CHECKPOINT_OP restore(Python:restore_thread(thread_id, checkpoint_id))
rolls a thread's committed state back to a historical checkpoint snapshot.
Restoration is a new committed transaction — it bumps the store version,
so a subsequent RUN on the thread continues from the restored snapshot
(fork semantics), never a raw memory mutation. An unknown checkpointId
raises an error carrying a Hint (use checkpoint_op list first).