zeph-durable 0.22.0

Native durable execution layer for Zeph: journaled control flow with crash-resume
Documentation

zeph-durable

Crates.io docs.rs License: MIT MSRV

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.

[!IMPORTANT] This crate is under active construction (spec-064, epic #4707). The type-level foundation, the AEAD payload contract, the persistence engine (LocalBackend, the background JournalWriter actor, the sealed ExecutionBackend dispatcher), and the execution heart — the &self DurableContext with deterministic step ids, the fingerprint-guarded replay cursor, the exactly-once intent/result protocol, and parallel() batches — have landed, as have the promise/timer layer (DurablePromise, DurableHandle, DurableTimerService) and journal retention (DurableRetentionService). The CLI/TUI integration and the consuming adapters land in follow-up issues of the epic.

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 the ExecutionKind discriminator. Private fields, smart constructors, serde-round-trip stable.
  • journal — the Journal trait plus its data model: JournalEntry, the closed EntryKind enum, and ExecutionStatus.
  • effectEffectClass, the per-step side-effect contract (Idempotent / AtLeastOnce / ExactlyOnceGuarded), plus EffectIntentSubClass and the OnAmbiguous policy 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, the Live/Replayed StepOutcome, and the DurableStep record.
  • handle — the &self DurableContext front door: step() / step_recorded() / parallel(), deterministic AtomicU32 step ids, a BLAKE3 replay-divergence guard, the exactly-once intent/result protocol, and the ParallelScope for completion-order-independent batches.
  • cipher — the PayloadCipher AEAD seal/open contract, the PayloadAad location binding, and the read-side ensure_payload_within_limit guard. The concrete cipher lives in a consuming crate (INV-1).
  • config — pure-data DurableConfig and RetentionPolicy mirroring the [durable] TOML section, with spec defaults applied on deserialization.
  • backend — the sealed ExecutionBackend trait, BackendCapabilities, the DurableBackendEnum enum dispatcher, and LocalBackend (a dedicated durable.db pool implementing Journal, sealing payloads through the injected cipher).
  • writer — the background JournalWriter actor and its cloneable JournalWriterHandle: group-commit for buffered appends, flush-before-commit ACKs for exactly-once entries, and MAX(seq) restart resume.
  • promise — the durable promise primitive: DurablePromise<T> (a journaled, resumable await point) and DurableHandle for out-of-band resolution.
  • timerDurableTimerService, a polling actor that fires journaled timers on resume.
  • retentionDurableRetentionService, the background pruner that enforces RetentionPolicy (TTL, execution/journal-byte caps) against the durable.db pool.
  • error — the crate-wide DurableError.

Architecture & invariants

  • Layer 0, no business-logic dependencies (INV-1). zeph-durable MUST NOT depend on zeph-llm, zeph-memory, zeph-core, zeph-sanitizer, or any business-layer crate. Its only direct zeph-* dependency is zeph-db; the rest are infrastructure crates (tokio, tracing, metrics, bytes, blake3, serde, uuid). The concrete payload cipher lives in zeph-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::derive uses BLAKE3 derive_key with a fixed context string and length-delimited (injective) input, so an attacker-controlled fingerprint cannot collide with a different (execution_id, step_id) pair.

[!NOTE] Schema ownership (INV-14). zeph-durable owns no .sql files and no sqlx::migrate!. The four durable_* tables (durable_executions, durable_journal, durable_promises, durable_timers) live as numbered migrations in zeph-db/migrations/{sqlite,postgres}/ and are applied via zeph_db::run_migrations against a dedicated durable.db pool.

Installation

This crate is an internal workspace member of Zeph. To use it from another workspace crate:

[dependencies]
zeph-durable = { path = "../zeph-durable" }
# or with the postgres backend:
zeph-durable = { path = "../zeph-durable", default-features = false, features = ["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] sqlite and postgres are mutually exclusive (enforced by zeph-db). Building with --all-features is intentionally unsupported — use --features full or --features full,postgres.

Usage

Idempotency keys are deterministic for a given (execution, step, fingerprint) and domain-separated from any other BLAKE3 use:

use zeph_durable::{ExecutionId, IdempotencyKey, StepId};

let execution = ExecutionId::new(); // fresh, time-ordered UUIDv7

let key = IdempotencyKey::derive(execution, StepId::new(0), b"tool:read_file");
assert_eq!(
    key,
    IdempotencyKey::derive(execution, StepId::new(0), b"tool:read_file"),
);

Configuration deserializes from the [durable] TOML table with every field defaulted to its spec value:

use zeph_durable::DurableConfig;

let cfg: DurableConfig = toml::from_str("").unwrap(); // empty table => all defaults
assert!(!cfg.enabled);
assert_eq!(cfg.journal_ack_timeout_ms, 5_000);
assert_eq!(cfg.max_payload_bytes, 1_048_576);

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 zeph_durable::{DurableContext, EffectIntentSubClass, OnAmbiguous, StepDescriptor};

// Read-only work is idempotent and replays for free.
let preview: String = ctx
    .step(StepDescriptor::idempotent("read_head", b"tool:read:/var/log".to_vec()),
          |_handle| async { Ok(read_first_line().await?) })
    .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(
        StepDescriptor::exactly_once_guarded(
            "llm_call",
            EffectIntentSubClass::CostBearingOrBoundaryIdempotent,
            Some(OnAmbiguous::Skip),
            b"llm:gpt:summarize".to_vec(),
        )?,
        |handle| async move { Ok(call_provider(handle.idempotency_key()).await?) },
    )
    .await?;

MSRV

Rust 1.96 (Edition 2024, resolver 3).

License

MIT — see LICENSE.