Checkpointing
Every run is event-sourced: the session is persisted as an append-only log of SessionEvents through the Checkpointer contract. That buys you durable sessions (resume after a restart), and time travel — roll a session back to any earlier checkpoint, undoing both the conversation and the file changes the agent made. Persistence is on by default via fileCheckpointer; swap in the SQLite backend when several processes must share sessions.
Use it
With the default file backend there is nothing to configure — createLiteAgent persists every session, and LiteAgent owns a current one:
Session management on LiteAgent:
Set sessions: false to disable persistence entirely (session methods then reject).
Time travel
Checkpoints are rewind anchors — one per user prompt. restore(id, seq) rolls a session back to just before a checkpoint: it reverts snapshotted files and/or truncates the conversation, then sets the current session to id.
Time travel works because the file tools snapshot every file before modifying it: restore replays those snapshots to undo changes on disk, then truncates the event log.
Switching to the SQLite backend
The default fileCheckpointer serializes session mutations with file locks across async encoding. SQLite adds database transactions and efficient concurrent reads. The @lite-agent/checkpoint-sqlite package provides sqliteCheckpointer — a SQLite (WAL) backend for single-host, multi-process setups: a server or worker pool where several processes resume and append the same sessions, with optimistic concurrency instead of silent clobbering.
Depends on better-sqlite3 — a native module that compiles on install. @lite-agent/core is required at runtime.
Pass the checkpointer to createLiteAgent (or query); it overrides the default file store:
Options
SqliteCheckpointerOptions:
Concurrency model
- WAL journaling —
journal_mode = WALis set on open: concurrent readers never block, and there is a single writer at a time. BEGIN IMMEDIATEwrites —appendandtruncatetake the write lock up front, so a competing writer waits onbusy_timeoutinstead of failing withSQLITE_BUSY_SNAPSHOT, then reads a fresh head and conflicts cleanly.- Atomic seq allocation — each
appendis one transaction that reads the session head and insertsseq = head + 1…n; the database serializes concurrent appends, so seqs never interleave. - Reads never lock —
read({ sinceSeq })is a pure forward scan; it is also the primitive a server layer can use to tail events over SSE.
Conflict handling
When two processes hold the same session, the second writer's append fails fast with CheckpointConflictError — optimistic concurrency, surfaced never swallowed:
Multi-host concurrency (networked FS, distributed writers) is out of scope for SQLite. The Checkpointer interface is backend-agnostic, so a future Postgres backend can cover that without kernel changes.
Checkpointer contract
SqliteCheckpointer implements the full core Checkpointer interface and passes core's checkpointerConformance test suite — the same suite the default fileCheckpointer runs against:
Operations: checkIntegrity(): { ok, detail } runs PRAGMA quick_check on demand; close() closes the database handle (call it on shutdown — a closed checkpointer cannot be reused). The database carries a user_version schema marker: opening a file written by a newer, incompatible version throws immediately instead of misreading the data.
See also
- Observability — recording the same event stream for audit and debugging.
- Background tasks —
background_completedevents land in the same session log. - Core strategies — the
Checkpointerstrategy interface.
Progress and safe recovery
compact(instructions?, { signal }?) emits live compaction events both through its generator and agent.subscribe(). Consume one delivery path in a UI to avoid duplicates. phase is start, progress, done, error, or cancelled; stage identifies measurement, archiving, normalization, summarization, projection, or persistence. Archive progress reports completed/total items. Token counts are measurements/estimates, not a fabricated percentage. A no-op compaction can finish without reducing the count.
listCheckpoints(id) returns { seq, prompt, ts, files, unavailableFiles }. New anchors identify user input explicitly, including block-form prompts. Runtime reminders, repair messages and background completions are not user checkpoints. Old logs use a conservative compatibility heuristic.
restore(id, seq, { files?, conversation?, signal? }) validates the target and preflights every file before applying changes. It rejects unfinished tool-call boundaries, unavailable/truncated snapshots, unsafe paths and detected out-of-band file changes. If an in-process write/truncate fails, it attempts to restore the pre-operation file contents. This is not a filesystem/database transaction that survives arbitrary process or machine failure.
The result is { restoredFiles, conversationRestored }; checkpoint_restore subscription events expose start/progress/done/error. Checkpointing covers recorded file-tool changes. Shell commands, external services and arbitrary custom-tool side effects are not automatically reversible. Use { files: false } explicitly for conversation-only recovery when a file snapshot is unavailable. File mutation hashes are recorded for new snapshots; old snapshots cannot provide equivalent external-edit detection.
Compaction and restore require an idle agent, including its background work. They hold a maintenance lock, so concurrent runs cannot write during recovery. The file backend atomically replaces a truncated log; memory, file and SQLite backends check an optional expected head before truncating. SQLite remains the backend for cross-process coordination.
The CLI supports /compact [instructions], /checkpoints, and /restore <seq> [--conversation-only]. Ctrl-C cancels manual compaction.
Built-in checkpoint events (including file snapshots) support storage.codec. Codec failures never trigger tail repair or plaintext fallback. SQLite and other injected backends manage their own encoding.