• English
  • 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:

    import { createLiteAgent } from "@lite-agent/sdk";
    import { anthropic } from "@lite-agent/provider";
    
    const agent = createLiteAgent({
      model: anthropic(),
      modelName: "claude-sonnet-4-6",
      workdir: process.cwd(),
    });
    
    await agent.send("Refactor src/auth.ts to use async/await.");
    const result = await agent.send("Now add tests for it."); // same session, full context

    Session management on LiteAgent:

    MethodDescription
    send(input, opts?)Run one turn to completion in the current session; resolves to LiteAgentResult.
    sessionIdThe id run/send use when opts.sessionId is not passed.
    resume(id)Switch the current session to an existing id (unknown ids start empty).
    clear()Rotate to a new empty session; returns the new id. The old transcript is kept.
    listSessions()List persisted sessions ({ id, mtime }, most-recent first).
    deleteSession(id)Delete a persisted session transcript.
    compact(instructions?)Manually compact the current session; resolves to { before, after } token counts.

    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.

    const sessions = await agent.listSessions();
    agent.resume(sessions[0].id);            // continue the most recent session
    
    const checkpoints = await agent.listCheckpoints(agent.sessionId);
    await agent.restore(agent.sessionId, checkpoints[2].seq); // undo everything after that prompt
    MethodDescription
    listCheckpoints(id)List rewind anchors (one per user prompt) for a session, oldest first.
    restore(id, seq, opts?)Roll back to just before a checkpoint. opts.files (default true) reverts snapshotted files; opts.conversation (default true) truncates the conversation.

    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.

    BackendPackageConcurrencyUse when
    fileCheckpointer (default)@lite-agent/sdkPer-session file lockLocal dev, CLI tools, one agent process per project
    sqliteCheckpointer@lite-agent/checkpoint-sqliteMany processes, one hostA server or worker pool sharing sessions on one machine
    pnpm add @lite-agent/checkpoint-sqlite
    Info

    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:

    import { createLiteAgent } from "@lite-agent/sdk";
    import { anthropic } from "@lite-agent/provider";
    import { sqliteCheckpointer } from "@lite-agent/checkpoint-sqlite";
    
    const checkpointer = sqliteCheckpointer({ file: "./sessions.db" }); // or ":memory:"
    
    const agent = createLiteAgent({
      model: anthropic(),
      modelName: "claude-sonnet-4-6",
      workdir: process.cwd(),
      checkpointer,
    });
    
    await agent.send("Hello!");
    // Sessions persist to sessions.db, shared across processes on this host.
    
    checkpointer.close(); // when you're done

    Options

    SqliteCheckpointerOptions:

    OptionTypeDefaultDescription
    filestring— (required)Path to the SQLite database file. Use ":memory:" for an ephemeral DB (per-process, nothing is shared).
    synchronous"normal" | "full""normal"SQLite durability level. "normal" is safe under WAL; "full" survives OS-level crashes at a write-latency cost.
    busyTimeoutMsnumber5000How long a competing writer waits for the write lock before erroring. Clamped to >= 0.
    integrityCheckOnOpenbooleanfalseRun PRAGMA quick_check at startup; throws if the database is corrupt.

    Concurrency model

    • WAL journaling — journal_mode = WAL is set on open: concurrent readers never block, and there is a single writer at a time.
    • BEGIN IMMEDIATE writes — append and truncate take the write lock up front, so a competing writer waits on busy_timeout instead of failing with SQLITE_BUSY_SNAPSHOT, then reads a fresh head and conflicts cleanly.
    • Atomic seq allocation — each append is one transaction that reads the session head and inserts seq = 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:

    import { CheckpointConflictError } from "@lite-agent/core";
    
    try {
      await checkpointer.append(sessionId, events, knownHead);
    } catch (err) {
      if (err instanceof CheckpointConflictError) {
        // err.sessionId, err.expected, err.actual
        // Another client advanced the log: reload (re-read + fold) and retry.
      }
    }
    Warning

    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:

    MethodBehavior
    append(sessionId, events, expectedHead?)Single immediate transaction. If expectedHead is given and differs from the current head, throws CheckpointConflictError. Returns the new head seq.
    read(sessionId, { sinceSeq? })Replays StoredEvents in seq order, optionally after sinceSeq (exclusive).
    head(sessionId)Current head seq; 0 when the session is empty or unknown.
    list()All sessions as { id, mtime }, most-recent activity first.
    delete(sessionId)Removes the session's events and its sessions-table row.
    truncate(sessionId, toSeq)Drops every event with seq > toSeq and moves the head back. Powers time travel: LiteAgent.restore(id, toSeq, { conversation: true }) truncates the log back to a checkpoint, as a single immediate transaction that can't interleave with a concurrent append.

    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

    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.