Files
deepseek-harness/packages/session-persistence/session-persistence-jsonl
Hypatia May 828c3f85c9 fix review findings: skip collided SCHEMA_VERSION 3; reject marker-less surface events
P1: both merge parents shipped SCHEMA_VERSION=3 for different layouts (surface
columns vs seed_length), so an on-disk 3 was ambiguous and wrongly accepted.
Bump to 4 (merged layout) so the version check rejects both sibling v3s.

P2: a surface-eligible event with no surfaceOp lands in the log but vanishes
from deriveMessages() (surface is the sole derivation path). The typed append
overload enforces the marker only when the type arg is a literal; it collapses
to optional when widened to the union (a caller iterating raw events). Guard at
runtime in both append() and the seed constructor — no backward-compat for
surface-less logs. Shared seed fixtures carry surfaceOp explicitly and the
appendLog helper forwards it verbatim (no synthesized default). Exports
isSurfaceEligibleType. Regression tests for all three, each verified to fail
on the unfixed code.

Gates: typecheck, test (1115), snapshot (14), doc-sync, lint, build, hygiene green.
2026-06-24 17:45:48 +08:00
..

@deepseek-ai/dsh-session-persistence-jsonl

The JSONL durable session-persistence backend — a concrete SessionPersistence (the dsh-session-persistence seam). One append-only .jsonl event log per session.

On-disk layout

<root>/
  cwd-<sha256(cwd)[:12]>/        # per-project bucket (or _no-cwd/ when no cwd)
    <encoded-id>.jsonl           # header line + one SessionEvent per line (verbatim)
  • The first .jsonl line is the immutable SessionHeader tagged { type: 'session', version, id, cwd?, createdAt, parentSession? }; every subsequent line is one SessionEvent JSON, verbatim including assistant/chunk so seq stays contiguous (events[i].seq === i).
  • Session ids are unvalidated branded strings, so they are percent-encoded to a single safe path segment before use (no traversal, no collision).

Config

Key Type Notes
root string (required) Root directory for all session files. No default — a process.cwd() default would scatter files as the process's cwd changes (bash calls, subprocesses).

Durability and crash semantics

  • Lazy materialization. create(meta) writes nothing; the .jsonl (header + first batch) is written atomically (temp-write + fsync + rename) on the first append. A created-but-never-appended session leaves nothing on disk and is absent from list.
  • Append-only. Committed events (at or below a flushed turn/end) are never rewritten. Subsequent appends are line appends at EOF + fsync.
  • Crash recovery — close, don't truncate. A crash can leave a log whose final turn never closed (real events after the last turn/end). load PRESERVES those events (a turn can be huge — they are real work) and closes the orphaned turn by durably appending synthetic boundary events: an error tool/result for every tool-call the crash left unanswered (the loop logs the assistant message before running the tools, so a mid-tool crash leaves dangling calls — and deriveMessages() would replay an assistant tool-call with no result, which providers reject), then a step/end if a step was open, then turn/end {kind:'interrupted'}, returning a balanced log. Only a never-fully-written torn tail fragment (a final line with no newline / unparseable) is ftruncated away before the closers are written. See session persistence.
  • Contiguous-seq. load rejects a mid-log parse error or seq gap (unloadable); append rejects a batch whose first seq does not continue the stored log, and rejects non-JSON-serializable event.data naming the offending event type.
  • Format version. Only the current SESSION_FORMAT_VERSION (v0) is supported; load rejects any other version. While the harness is unreleased the on-disk format is pre-release/unstable: a breaking format change is absorbed at v0 (no bump until the first tagged release) and non-current logs are rejected — there is no migration (no persisted user data to preserve).

Write path

The plugin generalizes the example session-jsonl.ts: it subscribes to session/created (capture the header; persist a fork's seed once), session/event (snapshot each event when buffering — the live session.events object is mutable), and session/flush/dispose (drain the write-behind buffer through append). A per-session write cursor means a resumed session never re-appends already-stored events. Existing live sessions are seeded on plugin apply (HMR does not replay session/created). All backend operations for one session are serialized, and disposal awaits quiescence (every init + final drain) before returning, so no write lands after teardown.