DSHarness Systems Dissection Pinned baseline 47f943859b · 36 verified / 0 in progress / 36 chapters
中文
Sessions and Persistence·Chapter 11

Persistence and Database Design

JSONL, SQLite, generic Storage, and data ownership

VerifiedUpstream 47f943859bScope: Analyze session persistence, SQLite schemas and migrations, JSONL, KV storage, domain forms, attachments, and workspaces.

Conclusion: persistence is a portfolio of ownership contracts, not “the database”

DeepSeek Harness has three deliberately separate persistence families. SessionPersistence owns the authoritative event log and its non-replayable header. The generic Storage/Domain stack owns host-side records whose meaning comes from a consumer-owned schema. The attachment store owns immutable binary objects while Session events retain only content-addressed references. Workspace then composes Domain state with Session headers; it does not own either project directories or Session logs.

This separation is the chapter's most important result. JSONL and SQLite are alternative physical providers for the same Session contract, but the generic JSON and SQLite backends are not Session databases. Likewise, a Workspace account is not a foreign-key ownership claim over a transcript, and an attachment reference is not the object itself.

1. The ownership map prevents accidental cascades

OwnerDurable dataAuthoritative forExplicitly does not own
SessionPersistenceSessionHeader plus contiguous SessionEventsModel-visible history, replay, resume, and fork factsWorkspace labels, binary image bytes, project files
Storage backendOpaque JSON KV unitsAtomic, durable media operationsRecord schemas, write ordering, domain meaning
DomainSchema-validated records and optional global stateIn-memory current view, serialized writes, change eventsSession-event semantics or cross-domain transactions
Attachment storeImmutable content-addressed bytesObject integrity and verified image metadataMessage ordering and reference lifetime
WorkspaceCanonical path, title, ordered candidate ids, registry order/archive stateHost-side grouping and display orderThe directory, its files, or the referenced Session logs
Session authority

The Service Definition says that backends store existing SessionEvents as the event-sourced log and keep non-replayable SessionHeader metadata separately. Its API distinguishes raw artifact location, lazy creation, durable append, mutating load, non-mutating inspect, physical suffix read, and lightweight listing.PERSIST-SEAM-API

2. SessionPersistence is a behavioral seam, not a CRUD repository

create(header)              // reserve identity; may remain purely in memory
append(id, contiguousBatch) // resolve only after durability
inspect(id)                 // balanced logical view; no physical repair
load(id)                    // commit repair when cold, then return a balanced view
prepare(id)                 // reserve the exact unpublished Session for resume
readFrom(id, seq)           // detached, non-mutating stored suffix
list / listSnapshots        // materialized logs only

The seam makes crash semantics part of the interface. A complete, valid interrupted final turn is preserved and deterministically closed, while a provider returns a contiguous prefix plus an opaque marker for the suffix it classifies as unrecoverable. That abstraction is broader than a literally partial physical record; Sections 6 and 8 audit the concrete classification. A created-but-never-appended Session may leave no artifact at all. SQLite therefore cannot be substituted by implementing ordinary create/read/update/delete calls—the provider must implement append continuity, identity, repair, and quiescent shutdown.

Backend obligations

The shared backend contract requires atomic header-plus-first-batch materialization, an opaque torn-tail marker, source-qualified revisions, and durability before appendBatch resolves. It explicitly permits repair to be non-atomic so a file backend can truncate and append closers in two synced steps.PERSIST-BACKEND-CONTRACT

3. The coordinator is the real portability layer

1

Snapshot admission

Headers and event batches are losslessly JSON-snapshotted before they wait on a chain.

2

Serialize by id

Every operation for one Session ID joins the same promise chain.

3

Validate and commit

Format and seq continuity are checked before the provider's one durable primitive.

4

Publish state

The in-memory cursor advances and prepared views invalidate only after durability.

PersistenceCoordinator owns lazy identity collision checks, write-behind controllers, per-ID serialization, prepared-session caching, live adoption, crash-repair sequencing, revision retries, and disposal. Providers contribute media primitives rather than reimplementing this state machine.

Shared protocol

The coordinator keeps backend state by Session ID, lifecycle/write-behind state by exact live Session, retirement drains, a four-phase preparation pool, and one promise chain per ID. A committed append advances materialized and cursor only after appendBatch returns.PERSIST-COORDINATOR

Read faces

inspect may reuse an immutable prepared graph without repair; load and prepare reserve and commit repair; readFrom bypasses that cache and never repairs. These are deliberately different ownership operations.PERSIST-READ-FACES

4. Write-behind separates hot-path admission from a durable quiescent point

session/event → structuredClone → pending queue
                 first item arms one fixed deadline
deadline → stable batch → backend append
failure  → batch restored at queue head; automatic retries pause
flush    → cancel timer → join active write → retry and drain to empty

Later events do not extend the current batching deadline. Events admitted during one write form a follow-up batch. A background failure is reported while the exact batch remains retained; explicit flush is the retry and quiescence barrier. Backend disposal drains controllers before it closes media.

Failure-safe queue

SessionWriteBehind copies admitted events, shares concurrent flush callers through one barrier, and restores a failed batch in front of newer pending events. The queue never silently acknowledges a failed durable write.PERSIST-WRITE-BEHIND

5. JSONL is a logical event stream with two physical encodings

PropertyRaw .jsonlDefault .jsonl.zstd
HeaderFirst newline-terminated recordOne checksummed header frame
Append batchNew JSONL linesOne independent checksummed frame
Chunk packingEligible delta runs may become lossless packed rows; load expands them back to events
Suffix seekNone: readFrom still scans the physical prefix, then returns only the suffix
Raw exportReturns the exact logical JSONL text, preserving packed rows and serialization

The configured root is required, then resolved once in the constructor so later process.cwd() changes cannot split one backend. Session IDs are injectively escaped before becoming path segments. Header identity and the derived cwd/id path are checked before repair or append, and a root mixing compression suffixes or the retired flat layout is rejected rather than guessed.

File identity

The provider resolves its root once, exposes an absolute location, and deliberately omits seek-capable loadStoredFrom. The format uses a dedicated header record followed by verbatim or packed storage rows, with path-safe encoding for unvalidated branded Session IDs.PERSIST-JSONL-ROOTPERSIST-JSONL-FORMAT

6. JSONL durability is strong, but its repair is intentionally two-stage

OperationDurability boundaryFailure behavior
First appendSynced temporary file; no-overwrite publish; parent-directory sync on POSIXSame-ID publication collision rejects instead of replacing a log
Later appendAppend encoded batch and fsync fileWrite/sync failure truncates back to the previous byte length
RepairTruncate and fsync, then append recovered records plus synthetic closers and fsyncA crash between the two steps can leave a shorter but still recoverable open tail

Raw mode tolerates only an incomplete final record or a defect strictly after the last committed turn/end. Zstandard mode rejects a broken complete frame, but can salvage complete JSONL records emitted from a structurally incomplete final frame. It truncates at that frame's start, then rewrites only recovered records and closers—not the earlier committed prefix.

Physical repair scope

The reader returns a byte-offset marker plus any complete events recovered from an incomplete final frame. commitRepair first fsyncs truncation, then appends recovered events and closers through the normal synced append path. The coordinator contract makes this non-atomic sequence legal.PERSIST-JSONL-REPAIR

Classification is broader than byte tearing

The raw scanner also accepts complete newline-terminated rows. If such a row is invalid JSON, has an invalid storage shape, or expands to a sequence gap, the scanner records the issue but throws only if a later decoded row closes the Turn. Otherwise finish() returns the byte offset before the defect, so repair may discard a physically complete row and all later rows in the final open Turn. The policy protects every closed Turn, but it cannot prove that this suffix was never durably committed.PERSIST-JSONL-SCANPERSIST-JSONL-REPAIR

7. Session SQLite maps the same log contract onto strict rows

TableRoleIdentity/durability detail
persistence_stateSingleton store UUIDPrevents equal local counters from comparing across stores
sessionsHeader columns, incarnation UUID, revision counterRow existence is lazy materialization
eventsOne row per event, primary key (session_id, seq)Foreign-key cascade and strict columns

The database carries both application_id and user_version. A pristine database is initialized and stamped; a nonempty unversioned database, a foreign application identity, or any version other than SCHEMA_VERSION = 15 refuses. Despite the word “version,” there is no migration path in this pre-release implementation.

Schema ownership

Initialization holds BEGIN IMMEDIATE, checks schema ownership, creates three STRICT tables, assigns the store identity, and stamps application/schema versions before applying the requested journal mode. Existing incompatible media is rejected rather than altered into a guessed shape.PERSIST-SQLITE-SCHEMA

8. SQLite append and repair gain transaction atomicity and real suffix reads

First materialization and every event in its batch commit in one transaction. Later append batches do the same and increment the Session revision once. Load scans a transactionally consistent full row set, identifies the first invalid tail seq, then repairs by deleting seq >= tornMarker, inserting deterministic closers, and incrementing the revision in one transaction.

Row repair

scanRows rejects an unparsable row or sequence gap at or before the last valid turn/end, but treats a later defect as a torn tail. commitRepair atomically deletes only that suffix and inserts closers; no valid committed row before the marker is rewritten.PERSIST-SQLITE-SCANPERSIST-SQLITE-REPAIR

The same policy exists at row granularity

A SQLite row can be transactionally present yet contain bad JSON or the wrong sequence. When that defect lies after the last valid turn/end, scanRows labels the row and every later row a never-committed torn tail, and mutating load can delete them. This is a reasonable availability choice, but “after the last closed Turn” is a semantic recovery rule—not physical evidence that the rows were torn.PERSIST-SQLITE-SCANPERSIST-SQLITE-REPAIR

Seek semantics

loadStoredFrom executes WHERE session_id = ? AND seq >= ? ORDER BY seq and scans only that selected region. It is explicitly non-mutating: a bad selected tail is shortened in the returned view, not repaired.PERSIST-SQLITE-SUFFIX

9. The documented WAL default has a direct-constructor trap

Static finding

The plugin schema defaults journalMode to wal, but SqliteSessionPersistence's constructor supplies fallback values only for cache size and batch delay. It casts the still-optional journalMode to required and passes it onward; openDatabase calls journalMode.toUpperCase(). Therefore new SqliteSessionPersistence(ctx, { path }) fails asynchronously instead of using WAL. The direct-constructor test avoids the defect by explicitly passing journalMode: 'wal'.PERSIST-SESSION-SQLITE-CONSTRUCTORPERSIST-SESSION-SQLITE-CONSTRUCTOR-TESTPERSIST-SQLITE-SCHEMA

The same shape appears in the generic SQLite storage backend: Schemastery promises a WAL default, while its public constructor casts an omitted optional value and the open path dereferences it. Loader-normalized plugin construction is safe; direct programmatic construction without journalMode is not.

Repeated boundary leak

SqliteStorageBackend({ path }) passes an undefined journal mode to a later toUpperCase() for the same reason. Defaults that are part of a public config contract need constructor-side normalization or a constructor type that requires already-normalized config.PERSIST-STORAGE-SQLITE-CONSTRUCTORPERSIST-STORAGE-SQLITE-OPEN

10. Generic Storage deliberately knows less than SessionPersistence

Storage hub
  ├─ backend registry: name → one medium and optional facets
  └─ mounted forms: domain today

KV backend contract
  open(unit descriptor) → loadAll / putRecord / deleteRecord / setGlobal / close

Domain form
  spec + zod schemas + route → authoritative memory + one write chain + change events

KV values are opaque JSON to the backend. A unit guarantees each single call is atomic and durable, but explicitly does not serialize concurrent calls. The Domain layer supplies that ordering, validates data when opening, applies backend durability before changing its in-memory map, and emits domain/changed only after both agree.

Layer boundary

The backend contract assigns media ownership and per-call durability to KvUnit, while leaving schemas and concurrent ordering to callers. A backend unregister operation does not close the medium; the provider plugin owns that lifecycle.PERSIST-STORAGE-SEAMPERSIST-STORAGE-REGISTRY

Domain ordering

One per-domain promise chain makes update functions observe the value at their queue slot. A rejected backend write leaves memory untouched; a successful write updates memory before emitting a contained notification.PERSIST-DOMAIN-ORDER

The generic SQLite provider gives each unit a row in units, an optional row in unit_globals, and one STRICT u_<unit>_<table> table per declared table. Its physical user_version is 1; any other stamped layout refuses instead of migrating. Each KV mutation is one prepared statement, so the provider supplies per-call atomicity while Domain still supplies write order.PERSIST-STORAGE-SQLITE-SCHEMAPERSIST-STORAGE-SQLITE-UNIT

11. JSON KV publication is carefully durable, but relative roots remain dynamic

Every JSON KV mutation republishes the entire human-readable unit file. The writer creates a same-directory owner-only temporary file, writes and fsyncs it, atomically renames it over the target, then fsyncs the parent directory on POSIX. Windows receives atomic replacement through libuv but no explicit write-through flag. There is no cross-process writer lock.

Replace protocol

Temp-file and target share a directory, so rename is the publication point; file sync precedes it and POSIX directory sync follows it. A failed path removes the temporary file and propagates the error.PERSIST-JSON-ATOMIC

Post-publication error ambiguity

rename() happens before the parent-directory fsync. If that fsync rejects, writeAtomic rejects after the target may already expose the new bytes. JsonKvUnit then rolls its state back, and Domain leaves its own memory unchanged because the backend call failed. The caller therefore receives failure while a reopen may observe the new file; crash durability is also uncertain. The backend needs an explicit post-publication policy—such as poisoning/reloading the unit or preserving enough state to reconcile—rather than treating every rejection as pre-publication failure.PERSIST-JSON-ATOMICPERSIST-JSON-UNIT-ROLLBACKPERSIST-DOMAIN-ORDER

Static root gap

The config correctly requires an explicit root, but its schema accepts relative strings and JsonStorageBackend stores the string verbatim. mkdir(root) and join(root, unit.json) run later, so a process-wide cwd change can redirect later unit opens. The Session JSONL provider avoids this exact class by resolving its root once in its constructor.PERSIST-JSON-ROOT-GAPPERSIST-JSONL-ROOT

12. Domain close has a one-way failure state

The happy path is sound: close() immediately rejects new writes, drains the settled write-chain tail, closes the backend unit, marks reads closed, then releases the facility's name reservation. Repeated close calls share one promise.

Static lifecycle gap

If unit.close() rejects, disposing remains true, closed remains false, onClosed() never frees the name, and disposal permanently retains the rejected promise. The resulting handle rejects all new writes but still permits reads; reopening the name also remains impossible.PERSIST-DOMAIN-CLOSE-GAP

13. Attachments use commit-before-reference and content-addressed ownership

1

Admission

Limit encoded bytes, fully decode the raster, verify format, dimensions, and pixel count.

2

Address

Hash exact encoded bytes into an opaque sha256: attachment id.

3

Publish

Sync a private staging file, publish with an exclusive hard link, and sync directory ancestors.

4

Reference

Only then may a Session event contain id, media type, byte length, dimensions, and sanitized display name.

Deduplication is safe because an existing object is read and hashed before it is accepted. Reads re-hash bytes, compare stored metadata with the logged reference, and preserve cancellation. Objects are retained indefinitely because resumed and forked Sessions may share them; no Session or Workspace deletion implicitly garbage-collects them.PERSIST-ATTACH-LIMITS

Binary ownership

The attachment seam exposes immutable references rather than paths or provider URLs. The local provider stores owner-private objects beneath a versioned root and publishes a durable reference only after content and namespace durability checks.PERSIST-ATTACH-SEAMPERSIST-ATTACH-STORE

14. Attachment read-time decoding is documented incorrectly

Documentation drift

The local-backend README says both write admission and reads fully decode the raster. Production source says the opposite: admission calls image.raw().toBuffer() for a full decode, while a digest-verified read calls probeImage(), which only asks Sharp for header metadata. The code's rationale is to avoid replay-time pixel amplification because the digest proves these are the exact bytes admitted earlier.PERSIST-ATTACH-DECODE-DOCPERSIST-ATTACH-DECODE-SOURCEPERSIST-ATTACH-DECODE-IMPL

The implementation still verifies integrity and reference metadata; the drift is about computational work and malformed-payload revalidation, not about skipping digest verification. Documentation should say “full decode at admission, header probe after digest verification on read.”

15. Workspace is a recoverable two-write registry over Domain state

The workspace domain (version 2) stores one record per Workspace—canonical path, title, ordered candidate Session IDs, and timestamps—plus global initialization, Workspace order, archive IDs, and an optional pending create/delete marker. First startup lists only Session headers, canonicalizes valid cwd directories, groups them, and writes the initialized marker last.PERSIST-WORKSPACE-INITPERSIST-WORKSPACE-BOOTSTRAP

Create/delete cannot be one generic cross-record transaction, so the registry writes a pending marker before the record/order pair can diverge. Startup completes only the named mutation; unexplained order/table, duplicate-path, or duplicate-session ownership still fails loud. Workspace deletion removes the registration and account only, never the directory, files, live Session, persisted log, or attachment.PERSIST-WORKSPACE-MUTATIONPERSIST-WORKSPACE-OWNERSHIP

Stored shape

The zod spec projects one workspaces table and one global state slot through Domain storage. Runtime validation separately checks order completeness, unique paths, and one-Workspace-per-session accounting.PERSIST-WORKSPACE-SCHEMAPERSIST-WORKSPACE-INVARIANTS

16. Workspace title, schema, and refresh contracts have visible gaps

FindingWhat source doesConsequence
Stale title parametercreate(path, title?) remains public, but a source TODO says its last production caller was deletedREADME/API surface advertises a branch that shipped consumers no longer exercise
Loose scalar schemasWorkspace id, path, title, Session ids, and timestamps are plain z.string()Empty/whitespace title, non-UUID ids, non-ISO timestamps, and non-canonical stored paths pass the schema
Partial compensationStartup separately checks duplicate order ids, missing/orphan rows, duplicate paths, and duplicate Session accountingCross-record integrity is stronger than the zod shape, but scalar semantics remain unchecked
No public full refreshOnly startup clears/replaces the header index; uncached lookup calls list() and incrementally indexes returned headersAn externally deleted cached Session is not removed by that incremental pass; restart is the only guaranteed full purge
Cached fast pathreadSessionHeader returns a cached header without re-listing or re-statting cwdExternal cwd damage is observed only when another path happens to re-index that header or on restart
Title and schema gap

The implementation accepts an optional raw title at create and any string at setTitle; the durable record schema imposes no nonblank or timestamp/UUID/path refinements. The source itself marks the create-time title parameter for removal.PERSIST-WORKSPACE-TITLE-GAPPERSIST-WORKSPACE-TITLE-SETTERPERSIST-WORKSPACE-SCHEMA

Refresh gap and README overstatement

The README says external deletion or cwd damage appears after the “next refresh or restart,” but production has no public full-refresh operation. Runtime misses call incremental indexHeaders without clearing absent ids, while cached hits do no refresh at all. Restart is the only explicit full replacement path.PERSIST-WORKSPACE-REFRESHPERSIST-WORKSPACE-REFRESH-DOC

17. A useful Codex comparison: similar media, different authority topology

For Codex users, the filenames can look deceptively familiar. At official public Codex commit 66919805ea080053d1933b6b43afeb0d8bf70c91, the rollout module says JSONL Session rollouts are persisted for replay or later inspection, and its recorder writes canonical rollout items to JSONL. The public config labels sqlite_home as the directory for the SQLite state DB; the App Server's thread/list contract says its default path may scan JSONL rollouts to repair metadata, while useStateDbOnly opts out.

Architectural difference

DeepSeek Harness makes JSONL and SQLite interchangeable authoritative Session-log providers behind one behavioral seam. Its generic SQLite state, Workspace records, and attachment objects remain separate ownership domains. The transferable lesson is not “JSONL versus SQLite”; it is to state which medium can reconstruct which facts, which medium is merely an index or projection, and which repairs are allowed to mutate authority.

18. Audit result: strong contracts, with seven concrete follow-ups

  • Normalize SQLite defaults at constructor boundaries. Fix both direct-constructor journalMode paths or require a normalized config type.
  • Name the torn-tail policy precisely. Document that both providers may discard physically complete defective records after the last closed Turn; consider preserving diagnostics or requiring explicit repair when physical tearing is not established.
  • Resolve JSON KV post-publication failures. Keep Domain, unit memory, and the published target in a defined state when directory sync fails after rename.
  • Freeze JSON KV root identity. Resolve an explicit relative root once, matching Session JSONL behavior.
  • Specify failed Domain closure. Decide retry and name-release semantics so a close error cannot strand a readable-but-unwritable handle.
  • Correct attachment documentation. State full decode on admission and digest-plus-header verification on read.
  • Tighten or document Workspace boundaries. Remove the dead create-time title branch, validate user-facing title/timestamps/ids as intended, and expose or stop promising a runtime full refresh.

The system's central design remains coherent: durable facts have explicit owners and providers share one Session contract. Recovery is conservative through the last closed Turn, while the final open tail follows a broader policy that should be named explicitly. The static gaps cluster around repair classification, post-publication failure, configuration, lifecycle, documentation, and cache refresh—not around confusion over which component owns the event log.

My Learning Notes

Autosaved only in this browser. Nothing is uploaded or committed. Export Markdown whenever you want to keep a copy.