DSH plugin persistence: SessionPersistence and recovery
DSH plugin session persistence starts from one premise: the append-only SessionEvent log is the source of truth, and persistence only lands it without rewriting semantics. So the seam has no "persistence-only events" — just ctx.sessionPersistence with create/open/stat/list, and per-session SessionHandle values carrying all log access plus single-writer ownership. That premise explains three behaviors that look odd at first: why a concurrent write open is rejected, why a crash does not "fix" the log, and why metadata lives in a separate SessionHeader.
DSH plugin SessionPersistence and JSONL: the log is the source
Bottom line: four methods, all access through handles; JSONL is only the default provider and swapping it keeps the contract.
- Four methods:
create,open,stat,list, all over existingSessionEventvalues. - Handle duties:
read,append,flush,close. - Creation visibility: a session is observable in-process once
createcompletes; the backend may defer physical materialization to the first append or flush. - Cross-process visibility: other processes see only materialized sessions — a session never materialized before a crash never existed.
- Bring your own backend: implement the contract; no new event types are needed.
DSH plugin SessionHandle and single-writer ownership
Bottom line: a write handle is a cross-process lease, so only one writer exists per session and a second write open fails.
- Ownership: the active session's write handle is owned by its lifecycle owner.
- Reject rather than queue: a concurrent
open(id, 'write')fails withSessionAlreadyOwnedError, so repair never races an active turn. - Read-only opens:
open(id, 'read')returns validated continuous prefix slices, never a torn tail, and repeated reads never observe older state. - No prepared-session cache: persistence caches nothing; session-query keeps its own cold-read cache keyed by
stat().revision. - Do not query with diagnostics:
SessionLocationexists only for refusal diagnostics, pointing at the raw log a build refused to interpret.
DSH plugin flush checkpoint: the write-behind window
Bottom line: events notify synchronously while persistence batches; session/flush drains to quiescence and session/disposed does the final drain.
- Synchronous notification:
session/eventis synchronous; the backend routes by session id into a bounded write-behind window of the active write handle, never blocking the producer. - Window rule: the first pending event opens a fixed batching window; later events join without resetting the deadline, and one durable append runs at expiry.
- Events during the write get their own deadline and form the next batch.
- Flush semantics: cancel the wait and drain to quiescence — the loop uses it as the ordering and error checkpoint before the next turn.
- Failure path: rejected background writes keep events in order, pause the automatic path, and report through the logger; the next explicit flush retries and rejects loudly.
- Shutdown:
session/disposedperforms the final drain and closes the handle;close()drains routed buffers through the still-open store.
DSH plugin crash recovery: persistence does not repair, resume does
Bottom line: no truncation, no repair; persistence guarantees physical continuity, and resume appends the missing closers through the same handle.
- The crash shape: the log ends with an open
turn/startand noturn/end. - Persistence does not repair: truncating would discard real events already appended during a long turn.
- Only torn tails are dropped: complete records recovered from a torn append (JSONL partially decodes torn Zstandard frames) are rewritten before the handle's first new append.
- Repair is the reader's job: resume reads the stored log through the write handle, computes
interruptedTurnClosers— missing tool errors, any unclosedstep/end, and a syntheticturn/end { reason: { kind: 'interrupted' } }— and appends them as a normal batch. interruptedis the exception: it is the onlyTurnEndReasonthe loop never emits.- Read-only observers never write back: session-query balances interrupted cold logs in memory only.
DSH plugin SessionHeader and format refusal
Bottom line: version, cwd, lineage and preset live in the header; unreadable logs get a dedicated refusal, not a corruption label.
- Header fields:
version,id,createdAt,cwd,parentSession,isSeeded,origin,delegationDepth,agentPreset. - Outside the vocabulary: the header never enters
SessionEventMaporderiveMessages(); the logical header rides onsession.header. - Why persist delegationDepth: a runtime-only depth would reset to top level after restart, losing the recursion budget.
- Two distinct errors:
SessionFormatUnsupportedErrormeans this build cannot interpret the log (data is not corrupt);SessionPersistenceCorruptionErrormeans it is. - Generation rules:
statandlistclassify the highest canonical generation; a higher future generation refuses even when older readable ones exist. - Recovery scope: current-format recovery preserves installed extensions and
ignorable: trueunknown events; historical v0/v1/v2 migrations reject unknown types even with the ignorable marker.
DSH plugin persistence troubleshooting
Bottom line: separate cannot write, cannot read and cannot interpret — ownership, visibility and format generation.
- Cannot write — another handle owns the write lease (
SessionAlreadyOwnedError). - Other processes cannot read — the session may not be materialized; trigger an append or flush.
- Cannot interpret — distinguish
SessionFormatUnsupportedErrorfrom corruption. - Incomplete history after a crash — expected; resume appends the closers, so do not edit the log by hand.
- Assuming flush means fully persisted — it guarantees the drain and error surface, not backend completion latency.
- Repair status, stated honestly: handle semantics, the write-behind window, crash recovery and format refusal follow the official Session persistence page; confirm signatures against its Cordis API and your version.
Sources: Session persistence, Session, dsh-session-persistence-jsonl.
FAQ
Yes: DSH plugin SessionPersistence is a capability seam where an abstract service exposes create/open/stat/list over existing SessionEvent values. A provider outside the repository can implement the same contract, and JSONL is just the shipped default.
A DSH plugin write handle represents single-writer ownership, guarded by a cross-process write lease, so a second open(id, 'write') fails with SessionAlreadyOwnedError. That keeps crash repair from racing an active turn.
No: DSH plugin persistence deliberately does not truncate or repair, returning only a physically valid continuous log. Repair is the reader's job, where resume appends missing tool errors, unclosed step endings and a synthetic interrupted turn end through the same handle.
DSH plugin session/flush cancels the batching wait and drains to quiescence, so when it returns data is persisted and errors have surfaced. The loop uses it as the ordering and error-observation checkpoint before claiming the next turn.
DSH plugin keeps metadata separate from the event log: SessionHeader carries the format version, id, creation time, cwd, fork lineage, isSeeded, delegationDepth and agentPreset. It never enters SessionEventMap and never reaches deriveMessages().
Related Terms
- SessionPersistence
- SessionPersistence is the DSH plugin session persistence seam, exposing create, open, stat and list over existing SessionEvent values with no parallel persistence event types. It only lands the log; it does not change log semantics.— https://deepseek-harness.github.io/deepseek-harness/reference/subsystems/persistence
- SessionHandle
- SessionHandle is an open channel to one stored session, offering read, append, flush and close plus single-writer ownership. All log access flows through handles rather than id-addressed service methods.— https://deepseek-harness.github.io/deepseek-harness/reference/subsystems/persistence
- session/flush
- session/flush is the persistence drain checkpoint: it cancels the batching wait, writes buffered events through, and surfaces errors. The loop calls it before claiming the next turn.— https://deepseek-harness.github.io/deepseek-harness/reference/subsystems/persistence
- SessionHeader
- SessionHeader is immutable metadata stored separately from the event log, containing the format version, id, creation time, cwd, fork lineage and agent preset. It never enters the event vocabulary or message derivation.— https://deepseek-harness.github.io/deepseek-harness/reference/subsystems/persistence
Sources
- DeepSeek Harness docs — Session persistence (SessionHandle, flush, crash recovery, SessionHeader)· deepseek-harness.github.io
- DeepSeek Harness docs — Session (SessionEventMap and TurnEndReason)· deepseek-harness.github.io
- dsh-session-persistence-jsonl (the shipped JSONL provider)· deepseek-ai (GitHub)