zeph-durable
Native durable execution layer for Zeph — journals the control flow of an execution (steps, promises, timers) so a crashed or interrupted run can resume at the point of failure instead of restarting from scratch.
[!NOTE] Spec-064 (epic #4707) is complete — all 11 child issues shipped and the epic is closed. The type-level foundation, the AEAD payload contract, the persistence engine (
LocalBackend, the backgroundJournalWriteractor, the sealedExecutionBackenddispatcher), the execution heart (the&selfDurableContextwith deterministic step ids, the fingerprint-guarded replay cursor, the exactly-once intent/result protocol, andparallel()batches), the promise/timer layer (DurablePromise,DurableHandle,DurableTimerService), and journal retention (DurableRetentionService, including the flock-verified crash-orphan staleness sweep) have all landed. Thezeph durableCLI (list/show/inspect/prune/resume/cancel) and the TUI durable-execution widget are wired, and all four consuming adapters — agent tool loop, orchestration (/plan resume), scheduler, and subagent — journal their steps throughDurableContext.zeph durable cancel <id>(issue #6362) durably marks a specific execution as intentionally stopped via a terminalCanceledstatus, distinct from the crash-drivenAbortedstate, so crash-resume sweeps never resurrect it.
Overview
zeph-durable is a Layer-0 infrastructure crate, analogous to zeph-db and zeph-common. It is a
pure infrastructure primitive: it sees opaque serialized payloads, never domain types. Domain
meaning lives in thin adapter modules inside each consuming crate (the agent tool-loop,
orchestration, scheduler, and subagent layers).
The eventual design provides a DurableContext facade (step() / parallel() / promise() /
sleep_until()), an explicit EffectClass contract per step, a background journal-writer actor
with group-commit, AEAD payload encryption, and a fingerprint-guarded replay cursor — all backed by
a dedicated durable.db (SQLite) or a feature-gated Restate backend.
Key Modules
- ids — journal-boundary newtypes:
ExecutionId/PromiseId/TimerId(UUIDv7),StepId,JournalSeq,IdempotencyKey, and theExecutionKinddiscriminator. Private fields, smart constructors, serde-round-trip stable. - journal — the
Journaltrait plus its data model:JournalEntry, the closedEntryKindenum, andExecutionStatus. - effect —
EffectClass, the per-step side-effect contract (Idempotent/AtLeastOnce/ExactlyOnceGuarded), plusEffectIntentSubClassand theOnAmbiguouspolicy that govern the ambiguous window. - step — the durable step typestate:
StepDescriptor(with the construction-time ambiguity rule),StepHandle(exposes the idempotency key for boundary dedup),StepError, theLive/ReplayedStepOutcome, and theDurableSteprecord. - handle — the
&selfDurableContextfront door:step()/step_recorded()/parallel(), deterministicAtomicU32step ids, a BLAKE3 replay-divergence guard, the exactly-once intent/result protocol, and theParallelScopefor completion-order-independent batches. - cipher — the
PayloadCipherAEAD seal/open contract, thePayloadAadlocation binding, and the read-sideensure_payload_within_limitguard. The concrete cipher lives in a consuming crate (INV-1). - config — re-exports the pure-data
DurableConfig,RetentionPolicy, andDurableBackendtypes (defined inzeph-config, mirroring the[durable]TOML section with spec defaults applied on deserialization) and addsencryption_gate/EncryptionGate, which resolves whether payload encryption is optional, required, or unavailable for a given backend + config pair. - backend — the sealed
ExecutionBackendtrait,BackendCapabilities, theDurableBackendEnumenum dispatcher, andLocalBackend(a dedicateddurable.dbpool implementingJournal, sealing payloads through the injected cipher). Includesopen_execution_exclusive, aflock(2)-backed process-exclusivity lock that rejects a second concurrent holder for the sameExecutionId(guards against collidingagent_turnexecutions across processes). - writer — the background
JournalWriteractor and its cloneableJournalWriterHandle: group-commit for buffered appends, flush-before-commit ACKs for exactly-once entries, andMAX(seq)restart resume. - promise — the durable promise primitive:
DurablePromise<T>(a journaled, resumable await point) andDurableHandlefor out-of-band resolution. - timer —
DurableTimerService, a polling actor that fires journaled timers on resume. - retention —
DurableRetentionService, the background pruner that enforcesRetentionPolicy(TTL, execution/journal-byte caps) against thedurable.dbpool. Also folds in the crash-orphan staleness sweep: astale_running_after_secsknob reclaimsstatus='running'rows abandoned by an ungraceful process exit, gated on a non-blocking advisory-lock liveness probe so a still-live owner is never aborted out from under it. - error — the crate-wide
DurableError.
Architecture & invariants
- Layer 0, no business-logic dependencies (INV-1).
zeph-durableMUST NOT depend onzeph-llm,zeph-memory,zeph-core,zeph-sanitizer, or any business-layer crate. Its only directzeph-*dependency iszeph-db; the rest are infrastructure crates (tokio,tracing,metrics,bytes,blake3,serde,uuid). The concrete payload cipher lives inzeph-core. - Closed enums make illegal states unrepresentable. Control entries (
EffectIntent,PromiseCreated,TimerArmed) carry no payload field — a "control entry with payload" cannot be constructed. - Domain-separated idempotency keys.
IdempotencyKey::deriveuses BLAKE3derive_keywith a fixed context string and length-delimited (injective) input, so an attacker-controlled fingerprint cannot collide with a different(execution_id, step_id)pair. - Tamper-evident replay (issue #6360).
LocalBackend::with_hwm_keyattaches an authenticated per-execution high-water-mark (HWM) — a signed{execution_id, max_committed_step_id, committed_result_count, key_epoch}tuple, verified O(1) on every resume — that detects deletion of a committedStepResultrow, including across acheckpoint_foldcompaction. It activates unconditionally wheneverZEPH_DURABLE_KEYis provisioned, unlike the row-HMAC above which stays opt-in for shared-database deployments. - Downgrade-resistant, vault-sealed (issue #6449).
LocalBackend::with_integrity_sealed/with_grandfatherclose the gap the HWM alone left open: deleting the wholedurable_execution_integrityrow used to be trusted as "predates the feature." Once an operator runszeph durable seal-integrity(which refuses while any resumable execution has committed results but no integrity row), an absent row on a keyed, non-grandfathered execution with ≥1 committedStepResultis unconditional tamper — the seal marker and grandfather set are vault-stored, never a DB column, so a DB-write attacker cannot forge or evade them. Residual: a grandfatheredexecution_idremains a permanent forge-able slot (an explicit, documented operator opt-out, not free protection) — prefer draining where practical. - Windowed key rotation (issue #6460).
LocalBackend::with_previous_hmac_keyandwith_previous_hwm_keyregister a previous key alongside the current one, mirroring the AEAD cipher's ownpreviousslot: verification tries the current key then the previous key, sozeph durable rotate-keyno longer force-aborts every in-flight execution the moment it runs. Three drop-scans (control-entry HMAC, high-water-mark, and post-rotationcheckpoint_foldcompaction) backrotate-key --drop-previous's default-on safety check before the window closes.
[!NOTE] Schema ownership (INV-14).
zeph-durableowns no.sqlfiles and nosqlx::migrate!. The fourdurable_*tables (durable_executions,durable_journal,durable_promises,durable_timers) live as numbered migrations inzeph-db/migrations/{sqlite,postgres}/and are applied viazeph_db::run_migrationsagainst a dedicateddurable.dbpool.
Installation
This crate is an internal workspace member of Zeph. To use it from another workspace crate:
[]
= { = "../zeph-durable" }
# or with the postgres backend:
= { = "../zeph-durable", = false, = ["postgres"] }
Feature Flags
Backend selection is forwarded to zeph-db; exactly one backend is active at a time.
| Feature | Description | Default |
|---|---|---|
sqlite |
Enables the SQLite backend via zeph-db/sqlite |
Yes |
postgres |
Enables the PostgreSQL backend via zeph-db/postgres |
No |
[!WARNING]
sqliteandpostgresare mutually exclusive (enforced byzeph-db). Building with--all-featuresis intentionally unsupported — use--features fullor--features full,postgres.
Usage
Idempotency keys are deterministic for a given (execution, step, fingerprint) and domain-separated
from any other BLAKE3 use:
use ;
let execution = new; // fresh, time-ordered UUIDv7
let key = derive;
assert_eq!;
Configuration deserializes from the [durable] TOML table with every field defaulted to its spec
value:
use DurableConfig;
let cfg: DurableConfig = from_str.unwrap; // empty table => all defaults
assert!;
assert_eq!;
assert_eq!;
A DurableContext wraps each unit of work in a step. A fresh run executes the closure and journals
its result; a resumed run replays the journaled result without re-running it. The closure receives a
StepHandle carrying the step's idempotency key for boundary deduplication:
use ;
// Read-only work is idempotent and replays for free.
let preview: String = ctx
.step
.await?;
// A paid call is exactly-once-guarded: its intent is journaled before the call and its result
// after, and the idempotency key is forwarded to the provider for boundary dedup.
let reply: String = ctx
.step
.await?;
MSRV
Rust 1.97 (Edition 2024, resolver 3).
License
Licensed under either of MIT or Apache License, Version 2.0 at your option.