Skip to main content

LocalBackend

Struct LocalBackend 

Source
pub struct LocalBackend { /* private fields */ }
Expand description

The always-compiled durable backend that journals to a dedicated durable.db.

Construct it from a zeph_db::DbPool (or open one with LocalBackend::open), then attach an optional PayloadCipher and HMAC key with the builder methods. Call LocalBackend::init once before use to apply the schema migrations.

§Examples

use zeph_durable::LocalBackend;

// 1 MiB payload ceiling, matching the spec default.
let backend = LocalBackend::open("durable.db", 1_048_576).await?;
backend.init().await?;

Implementations§

Source§

impl LocalBackend

Source

pub fn new(pool: DbPool, max_payload_bytes: u64) -> Self

Wrap an existing zeph_db::DbPool as a local backend with the given payload ceiling.

Call LocalBackend::init before any journal operation to apply the schema. Attach a cipher and HMAC key with with_cipher and with_hmac_key.

Source

pub async fn open( path: &str, max_payload_bytes: u64, ) -> Result<Self, DurableError>

Open (or create) a backend on a dedicated durable.db file (or :memory:).

Connecting also applies the schema migrations, so a freshly opened backend is ready to use; init may still be called and is idempotent.

On the SQLite backend, also derives the lock directory used by open_execution_exclusive from path (a sibling <path>.locks/ directory), unless path is :memory:. The Postgres backend never derives one — path there is a connection URL (which may embed credentials), not a filesystem path.

§Errors

Returns DurableError::Storage if the pool cannot be opened or migrations fail.

Source

pub fn with_cipher(self, cipher: Arc<dyn PayloadCipher>) -> Self

Inject the AEAD payload cipher used to seal and open payload-bearing entries.

Source

pub fn with_hmac_key(self, key: [u8; 32]) -> Self

Configure the keyed-BLAKE3 HMAC key stamped over control entries on shared-database deployments, and used to verify them again on every read (INV-8).

Source

pub fn pool(&self) -> &DbPool

Borrow the underlying pool (for tests and adapters that need direct access).

Source

pub async fn init(&self) -> Result<(), DurableError>

Apply the durable schema migrations to the backing pool.

Idempotent: safe to call repeatedly. The schema is owned by zeph-db, not this crate.

§Errors

Returns DurableError::Storage if a migration fails.

Source

pub async fn list_executions( &self, status: Option<&str>, kind: Option<&str>, limit: i64, ) -> Result<Vec<ExecutionSummary>, DurableError>

List execution summaries for operability surfaces (the zeph durable CLI and TUI).

Returns at most limit executions, newest first, optionally filtered by status and kind (each is matched against the raw column tag; None disables that filter). Only execution-level metadata is read — never payload bytes or resolver tokens (INV-5). The per-execution step count is the number of journal entries recorded for it.

Span: durable.backend.list.

§Errors

Returns DurableError::Storage if the query fails, or DurableError::Decode if a stored id or status cannot be reconstructed (schema corruption — the status column is CHECK-constrained, so this is a fail-closed guard rather than a routine path).

Source

pub async fn read_execution_redacted( &self, id: ExecutionId, ) -> Result<Vec<RedactedEntry>, DurableError>

Read one execution’s journal entries as redaction-safe metadata, without decrypting payloads.

Unlike read_execution, this never touches the cipher, so it works against a journal whose AEAD key is unavailable and never exposes plaintext (INV-5). It backs the default (redacted) zeph durable show/inspect output. Entries are returned in append order.

Span: durable.backend.read_redacted.

§Errors

Returns DurableError::Storage if the query fails.

Source

pub async fn count_prunable( &self, policy: &RetentionPolicy, ) -> Result<u64, DurableError>

Count terminal executions a prune sweep would delete under policy.

Read-only: backs zeph durable prune --dry-run. It applies the same TTL cutoffs as the delete path, so the count is exactly what a real sweep would remove now.

§Errors

Returns DurableError::Storage if the query fails.

Source

pub async fn count_orphans( &self, policy: &RetentionPolicy, ) -> Result<u64, DurableError>

Count crash-orphaned executions a sweep_orphans sweep would abort under policy (#6254).

Read-only: backs zeph durable prune --dry-run. Mirrors the real sweep’s staleness scan and INV-15 flock liveness check (acquiring and immediately releasing each candidate’s ExecutionLock, exactly as the real sweep does, so the count reflects genuinely unowned rows rather than staleness alone) — but never mutates status. Returns 0 when the sweep is disabled (stale_running_after_secs == 0) or this backend has no lock_dir.

§Errors

Returns DurableError::Storage if the query fails.

Source

pub async fn open_execution( &self, id: ExecutionId, kind: ExecutionKind, ) -> Result<bool, DurableError>

Ensure a durable_executions row exists for id, returning whether this is a resume.

Inserts a fresh running row for a new execution (returning false) or detects an existing row for a resumed one (returning true). The journal’s foreign key requires this row before any entry is appended, so callers open the execution first.

Reopening a row previously finalized as completed, failed, or aborted un-finalizes it: status resets to running and finalized_at clears (INV-16, #6254). A caller reopening an execution is, by definition, still using it, so the retention sweep (gated on finalized_at) must not consider it prunable while it does — without this, a long-lived execution finalized at one process’s graceful shutdown and legitimately resumed by a later process (e.g. a per-conversation AgentTurn execution) would keep a stale finalized_at and could be pruned out from under its still-active journal. aborted rows are included because the crash-orphan sweep (INV-17) makes aborted the common outcome of a resumable crash: a resumed execution whose row keeps finalized_at set is prunable out from under the active resume — the exact hazard this un-finalize prevents for completed/failed. This is also strictly safer for the pre-existing divergence-recovery case, which reopens an aborted row on purpose: it now also protects that fresh re-drive from prune.

The un-finalize is attempted as a single guarded UPDATE (no preceding SELECT) so there is no read-then-write window against a concurrent prune sweep (#6251 critic S1): if the row was deleted by prune between an earlier observation and this call, the UPDATE simply matches zero rows rather than silently resurrecting a half-deleted row. A zero-row UPDATE falls back to checking whether the row exists at all (already running/aborted, or genuinely gone) before deciding between reporting a resume or inserting a fresh execution — so this never reports is_resume = true for a row that turned out not to exist.

Span: durable.backend.open.

§Errors

Returns DurableError::Storage if the lookup, reset, or insert fails.

Source

pub async fn open_execution_exclusive( &self, id: ExecutionId, kind: ExecutionKind, ) -> Result<(bool, Option<ExecutionLock>), DurableError>

Like open_execution, but additionally takes a non-blocking, exclusive, process-scoped advisory lock on id before touching the row (INV-15, #6122).

Closes the race two processes deriving the same ExecutionId (e.g. two CLI instances pointed at the same memory.sqlite_path and the same ConversationId) would otherwise hit in open_execution’s unsynchronized SELECT-then-INSERT: both could observe “no existing row”, both insert, and both then drive next_step from 0 against the same journal, corrupting it. The lock is acquired first, so the loser never reaches the row check at all.

Returns (is_resume, lock). The caller MUST hold lock for as long as it drives the execution — dropping it releases the lock and allows another process to open the same id. lock is None when this backend has no on-disk lock directory (a :memory: database, a backend built via LocalBackend::new, or a Postgres deployment), in which case process exclusivity is not enforced — the caller degrades the same way it already does for open_execution’s other failure modes.

§Errors

Returns DurableError::ExecutionLocked if another process already holds id’s lock, or any error open_execution can return.

Trait Implementations§

Source§

impl Debug for LocalBackend

Source§

fn fmt(&self, f: &mut Formatter<'_>) -> Result

Redacts the cipher and HMAC key — never print key material or a cipher handle.

Source§

impl ExecutionBackend for LocalBackend

Source§

fn capabilities(&self) -> BackendCapabilities

Return this backend’s stable capability description.
Source§

async fn lookup_committed_result( &self, id: ExecutionId, idem_key: IdempotencyKey, ) -> Result<Option<JournalEntry>, DurableError>

Look up a committed StepResult anywhere in an execution by its IdempotencyKey. Read more
Source§

impl Journal for LocalBackend

Source§

async fn sweep_orphans( &self, policy: &RetentionPolicy, ) -> Result<u64, DurableError>

Crash-orphan reclamation (INV-17, #6254). See Journal::sweep_orphans for the contract.

Source§

async fn append(&self, entry: JournalEntry) -> Result<JournalSeq, DurableError>

Append an entry and return its database-assigned global sequence number. Read more
Source§

async fn read_execution( &self, id: ExecutionId, ) -> Result<Vec<JournalEntry>, DurableError>

Read every entry of an execution in append order. Read more
Source§

async fn read_execution_range( &self, id: ExecutionId, from_step_id: u32, limit: usize, ) -> Result<Vec<JournalEntry>, DurableError>

Read up to limit entries of an execution starting at from_step_id. Read more
Source§

async fn finalize( &self, id: ExecutionId, status: ExecutionStatus, ) -> Result<(), DurableError>

Transition an execution to a terminal status. Read more
Source§

async fn prune(&self, policy: &RetentionPolicy) -> Result<u64, DurableError>

Prune terminal executions according to policy and return the number of rows deleted. Read more

Auto Trait Implementations§

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T> Instrument for T

Source§

fn instrument(self, span: Span) -> Instrumented<Self>

Instruments this type with the provided Span, returning an Instrumented wrapper. Read more
Source§

fn in_current_span(self) -> Instrumented<Self>

Instruments this type with the current Span, returning an Instrumented wrapper. Read more
Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T> IntoEither for T

Source§

fn into_either(self, into_left: bool) -> Either<Self, Self>

Converts self into a Left variant of Either<Self, Self> if into_left is true. Converts self into a Right variant of Either<Self, Self> otherwise. Read more
Source§

fn into_either_with<F>(self, into_left: F) -> Either<Self, Self>
where F: FnOnce(&Self) -> bool,

Converts self into a Left variant of Either<Self, Self> if into_left(&self) returns true. Converts self into a Right variant of Either<Self, Self> otherwise. Read more
Source§

impl<T> Same for T

Source§

type Output = T

Should always be Self
Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = Infallible

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, <T as TryFrom<U>>::Error>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.
Source§

impl<T> WithSubscriber for T

Source§

fn with_subscriber<S>(self, subscriber: S) -> WithDispatch<Self>
where S: Into<Dispatch>,

Attaches the provided Subscriber to this type, returning a WithDispatch wrapper. Read more
Source§

fn with_current_subscriber(self) -> WithDispatch<Self>

Attaches the current default Subscriber to this type, returning a WithDispatch wrapper. Read more