synwire-checkpoint-sqlite: SQLite Checkpoint Backend

synwire-checkpoint-sqlite provides SqliteSaver, a production-ready BaseCheckpointSaver implementation backed by SQLite. It persists graph execution checkpoints to a local database file with security-conscious defaults and configurable size limits.

Why SQLite?

Checkpointing needs durable, ordered storage with transactional guarantees. SQLite provides all of this in a single file with no external daemon, no network round-trips, and no configuration beyond a file path. For local agent workloads --- where checkpoints are written by one process and read back by the same process or a restart of it --- SQLite is the simplest backend that is also correct.

WAL (Write-Ahead Logging) mode is used for concurrency. Multiple readers can proceed in parallel with a single writer, which matches the typical checkpoint access pattern: frequent reads during graph traversal, occasional writes at step boundaries.

Key type: SqliteSaver

#![allow(unused)]
fn main() {
use std::path::Path;
use synwire_checkpoint_sqlite::saver::SqliteSaver;

// Default: 16 MiB max checkpoint size
let saver = SqliteSaver::new(Path::new("/tmp/checkpoints.db"))?;

// Custom size limit
let saver = SqliteSaver::with_max_size(Path::new("/tmp/checkpoints.db"), 4 * 1024 * 1024)?;
}

SqliteSaver implements BaseCheckpointSaver with three operations:

MethodBehaviour
putSerializes the checkpoint to JSON, enforces max_checkpoint_size, records the parent chain, and inserts via INSERT OR REPLACE
get_tupleRetrieves the latest checkpoint for a thread (or a specific checkpoint by ID)
listReturns checkpoints for a thread in reverse chronological order, with optional limit

Security: file permissions

On Unix systems, SqliteSaver::new creates the database file with mode 0600 (owner read/write only) before handing it to SQLite. This prevents other users on a shared machine from reading checkpoint data, which may contain agent conversation history, tool call results, or application state.

Size limits

The max_checkpoint_size parameter (default 16 MiB) is enforced on every put. If a serialized checkpoint exceeds the limit, put returns CheckpointError::StateTooLarge without writing to the database. This prevents runaway state growth from consuming disk space, which can happen when an agent accumulates large tool outputs across many steps.

Connection pooling

SqliteSaver uses r2d2 with r2d2_sqlite for connection pooling. The pool is configured with a maximum of 4 connections, which is sufficient for the single-writer/multiple-reader pattern. The pool is wrapped in an Arc so SqliteSaver is cheaply cloneable.

Schema

The database has a single table:

CREATE TABLE IF NOT EXISTS checkpoints (
    thread_id              TEXT NOT NULL,
    checkpoint_id          TEXT NOT NULL,
    data                   BLOB NOT NULL,
    metadata               TEXT NOT NULL,
    parent_checkpoint_id   TEXT,
    PRIMARY KEY (thread_id, checkpoint_id)
);

Checkpoints are stored as JSON-serialized blobs. Metadata is stored as a separate JSON text column to allow querying without deserializing the full checkpoint.

Dependencies

CrateRole
synwire-checkpointBaseCheckpointSaver trait and checkpoint types
synwire-coreBoxFuture for async trait methods
rusqliteSQLite bindings
r2d2 / r2d2_sqliteConnection pooling
serde_jsonCheckpoint serialization
thiserrorError types

Ecosystem position

SqliteSaver is the recommended checkpoint backend for single-machine deployments. For distributed systems where multiple processes need to share checkpoint state, a networked backend (Redis, PostgreSQL) would be implemented as a separate crate, using the same BaseCheckpointSaver trait and validated by the same conformance suite.

synwire-checkpoint            (trait)
    |
    +-- synwire-checkpoint-sqlite   (this crate: SqliteSaver)
    +-- synwire-checkpoint-conformance (test suite)

See also