The persistence seam is now create/open/stat/list returning per-session
SessionHandles (read/append/flush/close); every log read and write flows
through the owning handle. The seam package exports only the service and
handle contracts, consumer-visible errors, and pure durable-data
validation helpers; each backend owns its complete storage runtime, and
the shared contract suites pin equivalent observable behavior. The
backend routes published sessions' live events by id into the active
write handle; agent-loop only acquires, seeds, and closes the handle.
Resume appends interruptedTurnClosers through its write handle;
session-query owns the revision-keyed cold cache. Legacy-only surfaces
are removed in the same swap: locate/readRaw/supportsRawArtifacts, the
legacy event-shape read migration, zstd torn-frame salvage,
DSH_SESSION_JSONL, and hook transcript_path population; a torn final
zstd frame is discarded whole; the session-list cold blank probe returns
on stat metadata (eventCount derived from the last physical row,
sizeBytes). The WebUI ZIP export serializes the logical log from a read
handle, so both backends export identically.
Refs #3245
- document the exported SqliteStore prefix/suffix loaders (verify-export-jsdoc)
- share createStoredEventRead from session-persistence so the standalone
SQLite store stops duplicating the service helper (duplication gate)
- route replaceStored header upserts through writeRow (duplication gate)
- move replacement/conflict test SQL into closed test resources so the
SQLite SQL resource boundary test passes
The product model has no cross-process writer exclusion (the coordinator
serializes per-session operations in-process; the README documents one
live writer per session), so the wx-created .lock sibling only guarded
byte-level races while adding two failure modes: a crash leaves a stale
lock that permanently wedges that log's appends/repairs/replacements, and
a post-commit lock cleanup failure makes a committed append look failed,
so the retained write-behind batch retries into duplicate seqs.
Remove withLogLock and keep replaceStored's revision compare-and-swap at
the commit boundary (recheck immediately before the atomic rename).
buildStepIndex rejected the whole decoder at initialization whenever any
registered step could not reach the current version, so one retired old
upgrader blocked every session, including later versions whose path to
the current version is complete. planSteps already refuses a specific
stored version when a needed step is missing; initialization now checks
only step legality and duplicates.
The abstract capability flag forces every backend to state whether it owns per-session raw artifacts, but TypeScript cannot express that a true flag requires replacing the concrete unsupported default. Without an implementer-facing obligation, a backend could advertise support and then fail with a contradictory unsupported diagnostic on first use.\n\nDocument the required pairing at the capability declaration. Keep readRaw concrete so backends that correctly report false inherit one fail-loud implementation instead of duplicating rejection code.
SessionPersistence.readRaw previously used undefined for two unrelated states: a supported backend could not find the requested session, or the backend had no per-session artifact concept at all. The export endpoint consequently reported an existing SQLite-backed session as HTTP 404, which falsely diagnosed storage capability as session absence.
Make raw-artifact support an explicit backend capability. Unsupported backends now fail their inherited readRaw path loudly and the host answers 501 before reading, while undefined retains the single meaning of an absent artifact on a supporting backend. First-party backends, test providers, generated API catalogs, bilingual persistence docs, and export error contracts now state that distinction; focused tests cover both the 501 and the inherited rejection.
Rescope nothing new: drop the needless async from the inherited readRaw
default (reject explicitly on abort), fix the void arrow shorthand in
downloadBlob, and share one revision-stable file-read loop between readRaw
and readPrefix in the JSONL backend.
Review round: the JSONL backend now refuses a foreign header version straight
from the raw header line, before validating today's header shape or decoding
any event row, so a structurally different future format reports the upgrade
direction instead of corruption (shared message builder
sessionFormatVersionRefusal). HMR live-prefix adoption runs the unknown-type
guard like the other read paths. The appendCore comment now states why the
unknown-type guard is read-side only, the loadStoredFrom JSDoc and README pin
the seek-vs-sequential refusal-scope divergence, and the generated catalog
preamble lists the ignorable envelope field.
Old runtimes meeting a newer session format now fail loud instead of
misreading: version refusal names the direction (newer: upgrade the
harness; older: no upgrade path) and points at the raw JSONL log, and an
event type outside the generated known vocabulary refuses resume unless
its envelope carries the new ignorable: true marker (default: required,
so a forgotten marker over-refuses instead of silently resuming a gutted
session). gen-persistence-catalog now also emits
KNOWN_SESSION_EVENT_TYPES; SQLite stores the marker in a dedicated
column (SCHEMA_VERSION 15). The versioning design (monotonic integer,
n->n+1 upgrader chain, migrate-on-continue) is recorded in the
session-log-version-mechanism Agent Note.
Machine-produced by `pnpm run rescope-vendor --apply` plus the regeneration it
prints: `pnpm install` for the lockfile, `pnpm run gen-third-party-notices`,
`verify-translation-pairing --write` for the touched bilingual pairs,
`gen-doc-graphs`, and one typert snapshot whose ids embed character offsets.
`pnpm run rescope-vendor --check` verifies the result.
Renames nine vendored packages (cordis, cosmokit, schemastery and the six
@cordisjs plugins) and every reference that resolves them: manifest names and
dependency keys, module specifiers including declare-module merges, cordis.yml
plugin names, tsconfig paths, every Markdown fence, and `docs/` prose.
Directory names, upstream versions, and dependency ranges are unchanged, so
vendor/README.md still reads as an upstream snapshot; its manifest table gains
an upstream-name column so THIRD_PARTY_NOTICES keeps MIT attribution pointed
at each fork's origin.
The tutorial tier follows the rename end to end: its yaml fences named plugins
the Loader can no longer resolve, its `ts ignore-check` fences disagreed with
the compiled fences beside them, and its prose quoted both. The contracts that
told readers to keep upstream names — the root convention and the vendoring
cookbook's tree comment and manifest invariant — now say to rescope instead.
Two rules read `@deepseek-ai/` as "another workspace plugin": the client bundle
purity gate now names the vendored libraries a browser bundle inlines, and the
files where a bare `cordis` is an agent-preset id keep that product data.
The inherited readRaw default now rejects like the async backend overrides
instead of throwing synchronously, and its arms plus the JSONL override's
retry loop, zero-frame, and corrupt-header branches get dedicated tests.
The persistence contract gains a concrete readRaw default (undefined for
backends without a per-session artifact) and the JSONL backend overrides it
with the decode of its physical zstd frames, so a consumer can read the
stored artifact text verbatim — the session-log export depends on it.