6.9 KiB
Session Persistence
The durability seam for the event log. session.md describes the in-memory Session — the append-only SessionEvent log that is the source of truth. This page describes how that log is made durable: the abstract SessionPersistence service, its backends, the flush checkpoint, crash recovery, and the metadata header that travels alongside the log. The event vocabulary the log carries is enumerated, member by member, in the generated persistence log event catalog.
The seam is a textbook capability seam: one abstract service (dsh-session-persistence, ctx.sessionPersistence) defining locate/create/append/load/list over the existing SessionEvent — no parallel persisted type — and two interchangeable backends that pass the same runPersistenceContract suite. See the session-persistence Agent Note.
The flush checkpoint
session/event is a synchronous notification; persistence plugins buffer it (write-behind) until session/flush. The loop awaits an ordinary turn's checkpoint before claiming the next queue item; synchronous idle inject() schedules its checkpoint without blocking send(), and disposal still drains it. A successful flush durably commits the closed turn as one unit; a rejecting flush is reported through agent/error and the logger — never as a session event past the closed turn — while the backend keeps its buffered events for the next flush.
Crash recovery preserves an interrupted turn
A backend that reloads a log crashed mid-turn finds an open turn/start with no turn/end. It does not truncate — a single turn can be huge in a long-horizon task (many steps, large tool output), and those events were durably appended before the crash. Instead it closes the orphaned turn with a synthetic turn/end { reason: { kind: 'interrupted' } }, keeping the log balanced and the turn-enclosure invariant intact. interrupted is the one TurnEndReason no loop emits (see session.md).
SessionLocation — optional per-session artifact target
SessionPersistence.locate(meta) synchronously resolves a backend-owned independent artifact without reading, creating, or flushing it. JSONL returns its absolute target path; SQLite returns undefined because sessions share one database. A returned path can therefore name a file that does not yet exist or lacks the current unflushed turn; it is a location hint, not authorization or a freshness guarantee.
/**
* A backend-resolved, per-session local artifact location. The path is an
* absolute target path and can name an artifact that has not materialized yet.
* Consumers must treat it as a location hint, never as an authorization token.
*/
interface SessionLocation {
/** Backend-specific artifact kind, for example `jsonl`. */
readonly kind: string
/** Absolute path to this session's backend-owned artifact. */
readonly path: string
}
SessionHeader — metadata beside the log
Per-session metadata travels separately from the event log: format version, cwd, lineage, and the seed boundary are storage concerns, not conversation events, so they stay out of SessionEventMap and never reach deriveMessages(). The header is attached to a Session via session.header.
Source: packages/core/session/src/types.ts
/**
* Immutable validated storage metadata, kept outside the conversation event log.
*/
interface SessionHeader {
/**
* On-disk format version, stamped from {@link SESSION_FORMAT_VERSION} when the
* session is created. A persistence backend rejects any other version on load
* (no migration — see the constant).
*/
readonly version: number
/** The session's id (mirrors the {@link Session}'s id). */
readonly id: SessionId
/** Unix epoch milliseconds when the session was created. */
readonly createdAt: number
/** Absolute working directory the session was created in (if any). */
readonly cwd?: string
/** The session this one was forked from (seed lineage), if any. */
readonly parentSession?: SessionId
/**
* How many leading events were inherited through a seed. Persisting this
* boundary lets resume and replay distinguish parent history from child work.
*/
readonly seedLength?: number
}
CreateSessionOptions — seeding and metadata
Creating a Session through the store takes a seed (replay/fork an existing event log) and meta (the storage-level fields the store folds into a SessionHeader). The store fills in version/id and defaults createdAt; the caller supplies the validated absolute cwd, the parentSession lineage, the seedLength seed boundary, and — only when reconstructing a persisted session — the original createdAt to preserve it.
/**
* Options for creating a {@link Session} via the store. `seed` replays/forks
* an existing event log; `meta` carries the caller-supplied storage fields the
* store folds into a {@link SessionHeader}.
*/
interface CreateSessionOptions {
/** Events to seed the new session with (replay/fork). */
readonly seed?: readonly SessionEvent[]
/**
* Storage metadata read once before publication. `seedLength` is explicit
* because a resumed seed contains the full stored log, not only its inherited prefix.
*/
readonly meta?: {
readonly cwd?: string
readonly parentSession?: SessionId
readonly createdAt?: number
readonly seedLength?: number
}
}
Replay/fork is therefore ctx.sessions.create(id, { seed: seedEvents }); resuming a persisted session into a live agent is ctx.agents.resume({ resumeSessionId }).
The backends
Both implement the same abstract SessionPersistence (locate/create/append/load/list over SessionEvent) and pass runPersistenceContract, proving the seam is genuinely backend-agnostic:
- dsh-session-persistence-jsonl — an append-only logical JSONL log per session, stored as checksummed concatenated Zstandard frames by default or raw lines by configuration, with crash-safe atomic writes, interrupted-turn recovery, and a read/replay path.
- dsh-session-persistence-sqlite —
node:sqlite, one row perSessionEvent. The row shape(session_id, seq, type, time, data, source_event_seqs, surface_op)maps 1:1 onto the event, including optional surface metadata, so there is no parallel persisted schema to keep in sync.
Multiple backends sharing one on-disk session coordinate writes through the shared persistence write-coordinator.