description: "The shipped JSONL session-persistence backend for deployments and maintainers choosing, configuring, or debugging per-session durable logs with optional Zstandard compression."
English | 中文
dsh-session-persistence-jsonl stores each session in its own append-only JSONL log — checksummed Zstandard frames by default, raw newline-delimited lines when compression is disabled. It serves the same logical SessionEvent stream as any persistence backend, so choosing it changes nothing for the agent loop, the model, or replay; compression, packing, and crash recovery are storage-internal details. Choose it when consumers need a per-session artifact on disk: locate(meta) returns the transcript path, and the logs are readable as plain lines when compression: 'none' is selected. A root directory is the one required configuration; durability, lazy materialization, and interrupted-turn recovery come with the backend.
Mount this backend when a composition needs durable sessions backed by per-session files. The common path is explicit: load the session service, mount the backend, and give it a root directory.
Choose this backend when consumers benefit from one artifact per session — navigation, external tooling, or a raw line-readable log. It is the sole first-party Session-persistence provider. The backend keeps sessions under a deployment-controlled root: project-local, shared, temporary, or centralized.
- name: '@deepseek-ai/dsh-session'
- name: '@deepseek-ai/dsh-session-persistence-jsonl'
config:
root: /absolute/path/to/session-logs
root is required and has no default: a process.cwd() default would scatter session files as the process's cwd changes. An existing root must be a readable directory; an absent root is created on first materialization.
| Field | Default | Meaning |
|---|---|---|
root |
required | Root directory for all session files |
packChunks |
true |
Write eligible assistant/chunk runs as packed rows; false keeps one event per line for diagnostics |
compression |
'zstd' |
Physical encoding: 'zstd' checksummed frames, or 'none' newline-delimited UTF-8 text |
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.
Each session gets a session-owned directory under a readable project directory; the first logical line of the log is the immutable SessionHeader, followed by one storage record per logical event (or one packed chunk row per eligible run). Storage records use the lossless provenance representation described below:
<root>/
--<normalized-cwd>--/ # readable project directory (or _no-cwd/)
<encoded-id>/ # session-owned directory
session.jsonl.zstd # default: checksummed header frame + append frames
session.jsonl # only with compression: 'none'
Session ids are injectively escaped to one safe path segment before use (no traversal, no collision). The normalized cwd keeps the project directory readable for navigation; cwd strings that normalize alike share a project directory while session ids still select distinct session directories. locate(meta) returns { kind: 'jsonl', path } for the fixed transcript inside the resolved directories, performing no filesystem I/O.
A session is materialized lazily: create(meta) writes nothing, and the first append writes and fsyncs the encoded header and first batch through a no-overwrite publish — so a created-but-never-appended session leaves nothing on disk unless a lifecycle consumer calls ensureMaterialized, which publishes one header frame without an event. Flushed events are never rewritten; each subsequent batch appends lines or one compressed frame, and a caught write or sync failure rolls the file back to its prior length. After a crash, load preserves an interrupted final turn: it keeps the complete decoded records of an incomplete last frame, truncates from that frame's start, and re-encodes the records with the synthetic tool, step, and turn closers required by the shared persistence contract. Only a never-fully-written torn tail is discarded; checksum, decompression, or structural failure in the committed prefix rejects as corruption.
inspect(id) returns an immutable balanced view without committing recovery. readFrom(id, fromSeq) returns stored events at or past a sequence number for watermark consumers; sequential media like JSONL parse the whole artifact and skip forward. With compression: 'none', the log is newline-delimited text an external reader can consume directly; the compressed default must be read through the backend.
Read these pages when the package-level contract is not enough. They move from the shared persistence model to the sibling backend and the physical-format decisions.
JSONL storage contributes no live prompt or schema. Loading restores stored surface history and preserves prior request headers for reconstruction; the new loop composes its current envelope. Recovery balances an assistant request without a durable call with TOOL_NOT_STARTED; a durable call without a result becomes TOOL_OUTCOME_UNKNOWN, which tells the model to retry only read-only or idempotent work and to verify possible side effects or ask the user. Raw assistant/chunk records do not duplicate messages.
Zero live-request tokens. A resumed agent pays for retained history and its current envelope, plus the quoted repair result for each interrupted call.
JSONL storage does not mutate live request prefixes. A resumed loop can reuse provider cache only when its reconstructed history, current envelope, and model route match; crash-repair results append.
These limits define when this backend is a poor fit or needs special operational care. They are current package constraints, not a task backlog.
SESSION_FORMAT_VERSION (v0) load — changing compression requires a separate or fresh root, or selecting raw mode; the pre-release format has no migration.compression: 'none' before writing a fresh root when external line readers are required.root until removed externally; the seam has no deletion API.link() so same-id races fail instead of overwriting a committed log; Windows uses write-through rename without replacement.