DSH plugin persistence: SessionPersistence and recovery

Plugin DevelopmentPublished 2026-10-17Author: DeepSeek Plugin Market
DeepSeek HarnessDSH pluginSessionPersistenceJSONLcrash recoverypersistence
How a DSH plugin uses session persistence: handles and single-writer ownership, flush checkpoints, crash repair by resume, and SessionHeader.

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.

  1. Four methods: create, open, stat, list, all over existing SessionEvent values.
  2. Handle duties: read, append, flush, close.
  3. Creation visibility: a session is observable in-process once create completes; the backend may defer physical materialization to the first append or flush.
  4. Cross-process visibility: other processes see only materialized sessions — a session never materialized before a crash never existed.
  5. 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.

  1. Ownership: the active session's write handle is owned by its lifecycle owner.
  2. Reject rather than queue: a concurrent open(id, 'write') fails with SessionAlreadyOwnedError, so repair never races an active turn.
  3. Read-only opens: open(id, 'read') returns validated continuous prefix slices, never a torn tail, and repeated reads never observe older state.
  4. No prepared-session cache: persistence caches nothing; session-query keeps its own cold-read cache keyed by stat().revision.
  5. Do not query with diagnostics: SessionLocation exists 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.

  1. Synchronous notification: session/event is synchronous; the backend routes by session id into a bounded write-behind window of the active write handle, never blocking the producer.
  2. 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.
  3. Events during the write get their own deadline and form the next batch.
  4. Flush semantics: cancel the wait and drain to quiescence — the loop uses it as the ordering and error checkpoint before the next turn.
  5. 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.
  6. Shutdown: session/disposed performs 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.

  1. The crash shape: the log ends with an open turn/start and no turn/end.
  2. Persistence does not repair: truncating would discard real events already appended during a long turn.
  3. 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.
  4. Repair is the reader's job: resume reads the stored log through the write handle, computes interruptedTurnClosers — missing tool errors, any unclosed step/end, and a synthetic turn/end { reason: { kind: 'interrupted' } } — and appends them as a normal batch.
  5. interrupted is the exception: it is the only TurnEndReason the loop never emits.
  6. 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.

  1. Header fields: version, id, createdAt, cwd, parentSession, isSeeded, origin, delegationDepth, agentPreset.
  2. Outside the vocabulary: the header never enters SessionEventMap or deriveMessages(); the logical header rides on session.header.
  3. Why persist delegationDepth: a runtime-only depth would reset to top level after restart, losing the recursion budget.
  4. Two distinct errors: SessionFormatUnsupportedError means this build cannot interpret the log (data is not corrupt); SessionPersistenceCorruptionError means it is.
  5. Generation rules: stat and list classify the highest canonical generation; a higher future generation refuses even when older readable ones exist.
  6. Recovery scope: current-format recovery preserves installed extensions and ignorable: true unknown 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.

  1. Cannot write — another handle owns the write lease (SessionAlreadyOwnedError).
  2. Other processes cannot read — the session may not be materialized; trigger an append or flush.
  3. Cannot interpret — distinguish SessionFormatUnsupportedError from corruption.
  4. Incomplete history after a crash — expected; resume appends the closers, so do not edit the log by hand.
  5. Assuming flush means fully persisted — it guarantees the drain and error surface, not backend completion latency.
  6. 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

Can a DSH plugin use a backend other than JSONL for session persistence?

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.

Why is a second write-mode open of one session rejected in a DSH plugin deployment?

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.

When a session crashes mid-turn and has no turn/end, does DSH plugin persistence repair it?

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.

What does DSH plugin session/flush guarantee, and why is it a loop checkpoint?

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.

Why does DSH plugin keep session metadata outside the log, and what is in SessionHeader?

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