Session persistence
lite-agent persists every session as an append-only, event-sourced log behind the Checkpointer contract. Because the log — not the message array — is the source of truth, you get crash recovery, optimistic concurrency, and time travel (fork a session from any point) for free, and the storage backend is a swappable strategy: in-memory for tests, a file store in the SDK by default, or SQLite when several processes share sessions.
Enabling persistence
Pass any Checkpointer to createAgent (or to the SDK's query / createLiteAgent, which default to a file-based store):
Checkpointer primitives
The canonical persisted unit is a SessionEvent (user, assistant, tool_started, tool_result, file_snapshot, artifact_verified, permission_decision, summary, context_view), stored as a StoredEvent with a monotonic seq and parentSeq link.
Passing expectedHead to append gives optimistic concurrency — a mismatch throws CheckpointConflictError. Because the log is the source of truth, truncate + replay is time travel: fork a session from any point.
Building blocks exported from @lite-agent/core:
memoryCheckpointer()— the in-memory implementation, for tests and ephemeral runs.foldEvents(events)— rebuilds the conversation from a log: consecutivetool_resultevents coalesce into one user message (reproducing the kernel's turn shape), and asummaryevent resets the transcript.storeEvents(sessionId, fromSeq, events)— stamps rawSessionEvents intoStoredEvents withseq/parentSeq/ts; the building block for writing your own backend.legacyStoreAdapter(store)— wraps a legacy whole-arrayStoreas aCheckpointer, so existing storage keeps working.
Validate any custom backend against the checkpointerConformance suite — the same suite the built-in backends run against.
The SQLite backend
@lite-agent/checkpoint-sqlite is a durable Checkpointer for single-host, multi-process setups. Two backends ship today:
Choose the SQLite backend when:
- Multiple processes on one host (HTTP server, job workers, parallel CLI runs) share sessions.
- You need optimistic concurrency: a stale writer gets a clean
CheckpointConflictErrorinstead of silently clobbering the log. - You want one queryable database file instead of a directory of JSONL transcripts.
Multi-host concurrency (networked FS, distributed writers) is out of scope for SQLite. The Checkpointer interface is backend-agnostic, so a future @lite-agent/checkpoint-postgres can cover that without kernel changes.
Depends on better-sqlite3 — a native module that compiles on install. @lite-agent/core is required at runtime.
Using the SQLite checkpointer
Pass it to createLiteAgent (or query); it overrides the default file store:
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.
Contract behavior
SqliteCheckpointer implements the full core Checkpointer interface:
The backend passes core's checkpointerConformance test suite — the same suite the default fileCheckpointer runs against.
Options
The database carries a user_version schema marker. Opening a file written by a newer, incompatible version of this package throws immediately instead of misreading the data.
Conflict handling
When two processes hold the same session, the second writer's append fails fast:
This is optimistic concurrency: the conflict is surfaced, never swallowed, so two clients can't silently interleave into one session. The caller decides whether to reload — or, with session time travel, to fork from an earlier point.
Time travel
truncate is what makes session restore possible on this backend: LiteAgent.restore(id, toSeq, { conversation: true }) truncates the log back to a checkpoint. It runs as a single immediate transaction, so a rewind can't interleave with a concurrent append.
Operations
checkIntegrity(): { ok: boolean; detail: string }— runsPRAGMA quick_checkon demand (same check asintegrityCheckOnOpen).close(): void— closes the database handle. Call it on shutdown; a closed checkpointer cannot be reused.
See also
- Testing utilities —
checkpointerConformancefor validating custom backends. - Context compaction — the ContextEngine's durable event log builds on the same primitives.
- Strict local assembly — wires the SQLite checkpointer in with hardened settings.
- The nine strategies — the legacy
Storeseam andlegacyStoreAdapter.