description: "SQLite session persistence for deployments and maintainers choosing, configuring, or debugging the opt-in packed-row backend."
English | 中文
dsh-session-persistence-sqlite keeps every session's durable history in a single SQLite database: sessions survive restarts, and the deployment's whole history becomes one queryable file you can back up, inspect with SQL, and analyze — instead of one artifact per session. Choosing it changes nothing for the agent loop, the model, or replay, because it serves the same logical SessionEvent stream as the JSONL backend; packing, compression, and recovery are storage-internal details. Choose it when a single queryable database fits the deployment; no shipped composition enables it by default. It is a pre-release provider: it rejects database files it does not own instead of migrating them, and its synchronous Node SQLite driver blocks the JavaScript thread during reads and writes. Setup, sizing, and migration guidance come first; the implementation internals live in a collapsible developer section below.
Mount this provider when a composition needs durable sessions backed by SQLite and accepts a process-local, synchronous database driver. The common path is explicit: load the session service, mount the provider, and give it a database path.
Choose this backend when a local deployment benefits from one queryable database instead of many per-session files. Choose the JSONL backend when consumers need a per-session artifact: this provider returns undefined from locate(meta), supports no raw artifacts, and exposes no per-session file. Account for synchronous SQLite and compression work before adopting it for a high-concurrency service.
The packed layout exchanges some SQLite-local latency for a smaller queryable database. The available 501-session comparison measures schema 19 rather than schema 20; that layout used 233.18 MB against the SQLite comparison baseline's 438.31 MB and compressed JSONL's 148.15 MB. Full writes were about 2.3× faster than JSONL and suffix reads remained much faster; complete reads and forks were slightly slower than JSONL. The persistence latency and page-size decision owns the method, complete metrics, and accepted trade-offs.
The disk cost buys a structured, queryable view of session history: external tooling can analyze sessions and events with SQL, decoding physical rows the way this provider does — the groundwork for features such as built-in full-text search.
Load the session service first, then mount the provider with a database path. Use an absolute path when the location must not depend on the process working directory; relative paths resolve from that directory. :memory: is valid for an in-process database whose contents disappear with the process.
- name: '@deepseek-ai/dsh-session'
- name: '@deepseek-ai/dsh-session-persistence-sqlite'
config:
path: /absolute/path/to/sessions.db
| Field | Default | Meaning |
|---|---|---|
path |
required | SQLite database path, or :memory: |
journalMode |
wal |
Durable journal mode: wal, delete, truncate, or persist |
busyTimeoutMs |
5,000 |
Maximum synchronous wait for another connection's lock |
preparedSessionCacheSize |
5 |
Cold session preparations retained for resume reuse |
writeBatchMaxDelayMs |
200 |
Fixed live-event coalescing window, in milliseconds |
The generated configuration catalog is the exhaustive source for every accepted field and its JSDoc.
There is no built-in migration tool: the JSONL and SQLite stores are separate, and nothing copies sessions between them. Because both backends implement the same logical contract, you can carry a session over with the persistence API — read on the JSONL side, write on the SQLite side. One backend serves ctx.sessionPersistence per composition, so run the two halves as separate runs or processes:
// Export — run against the JSONL composition, per session id:
const { meta, events } = await ctx.sessionPersistence.load(id)
// Import — run against the SQLite composition, per exported session:
await ctx.sessionPersistence.create(meta)
await ctx.sessionPersistence.append(id, events)
list() enumerates the materialized sessions to export. The exported events keep contiguous seq values starting at 0, so append accepts them as one ordered batch into a fresh session; load also commits any needed cold repair on the source first, so the exported log is balanced. Treat the migration as a one-time cutover: verify that the imported sessions load, then switch the composition to the SQLite provider. Continuing to write through the old JSONL root afterwards would let the two stores diverge.
A fresh database initializes directly at schema version 20 with 64 KiB pages. Existing files are never retuned: databases with any other version, a foreign application identity, an unversioned non-pristine schema, or unexpected schema objects are rejected before any data is exposed or changed. This pre-release provider ships no migration. Every statement and fixed pragma comes from packaged .sql resources in resources/sql/, and runtime values are bound as SQLite parameters, so package code never assembles query text.
Each connection disables SQLite trusted schemas and memory-mapped I/O, verifies the requested journal mode, and pins synchronous=FULL so a resolved append remains durable across an OS crash or power loss. On POSIX, the database parent directory and file must belong to the current user, the parent must not be group/world-writable, and the file must grant no group or world permissions; Windows additionally rejects symbolic links and non-regular files, while ACL restriction stays the deployment's job. Path and ownership failures reject plugin initialization; Node's SQLite driver loads lazily on the first persistence operation. Ordinary create stays lazy until the first append, while ensureMaterialized writes a session metadata row with no event rows.
Read these pages when the package-level contract is not enough. They move from the shared persistence model to exhaustive configuration and the decision evidence behind the physical layout.
Nothing specific to SQLite. Resume restores the same logical events and derived messages as the JSONL backend; physical packed tags never reach prompts, tools, replay, or live session/event delivery.
Zero live-request tokens. Resume pays only for the retained logical history and the current request envelope.
Physical packing does not mutate request prefixes. Provider cache reuse depends on the reconstructed history, current envelope, and model route exactly as with other persistence backends.
These limits define when the provider is a poor fit or needs special operational care. They are current package constraints, not a general SQLite comparison or a task backlog.
busyTimeoutMs.events.type (text-chunks, reasoning-chunks, tool-call-chunks) is not a logical event type; supported consumers read through this provider.