docs: trim generated prose
This commit is contained in:
@@ -1,26 +1,6 @@
|
||||
/**
|
||||
* The backend-agnostic write-path orchestration shared by every first-party
|
||||
* {@link SessionPersistence} backend.
|
||||
*
|
||||
* Every durable backend needs the same orchestration: the in-memory bookkeeping
|
||||
* (the per-id state, the write-behind buffers, the per-id serialization chains,
|
||||
* the per-session init promises), the `session/event` → buffer → `session/flush`
|
||||
* drain, lazy materialization, crash-tail repair on load, the four
|
||||
* `session/created` adoption cases (new / HMR-adopt / collision /
|
||||
* ownerless-claim), and dispose-time quiescence. Only the STORAGE primitives are
|
||||
* backend-specific (file bytes for `dsh-session-persistence-jsonl`, `node:sqlite`
|
||||
* rows for `dsh-session-persistence-sqlite`). {@link PersistenceCoordinator} owns
|
||||
* the orchestration; a backend supplies the storage primitives as a small
|
||||
* {@link PersistenceBackend} hook object.
|
||||
*
|
||||
* The abstract {@link SessionPersistence} service's public API is independent of
|
||||
* this: a backend IS a `SessionPersistence` (its four public methods delegate to
|
||||
* a coordinator it composes), so a third-party backend MAY implement the service
|
||||
* directly without using the coordinator at all.
|
||||
*
|
||||
* See the write-coordinator RFC (docs/rfc/implemented/architecture/2026-06-18-shared-persistence-write-coordinator.md)
|
||||
* for the design rationale (composition over inheritance, the opaque torn marker).
|
||||
*
|
||||
* The backend-agnostic write-path orchestration shared by every first-party {@link
|
||||
* SessionPersistence} backend.
|
||||
* @module @deepseek-ai/dsh-session-persistence/coordinator
|
||||
*/
|
||||
|
||||
@@ -30,16 +10,9 @@ import type { Session, SessionEvent, SessionId, SessionHeader } from '@deepseek-
|
||||
import { assertSerializable, seedCoversPrefix } from './index.ts'
|
||||
|
||||
/**
|
||||
* A stored session's durable prefix as read back from a backend: its
|
||||
* {@link SessionHeader}, the preserved (seq-contiguous, parseable) event prefix,
|
||||
* and an OPAQUE `tornMarker` that is present iff a never-committed torn tail must
|
||||
* be truncated before further writes.
|
||||
*
|
||||
* The coordinator NEVER inspects `tornMarker`'s value — it only tests
|
||||
* `!== undefined` (is there a tail to repair?) and passes the value back to
|
||||
* {@link PersistenceBackend.commitRepair}. Each backend chooses its own marker
|
||||
* type: the JSONL backend uses the byte offset to truncate to, the SQLite
|
||||
* backend uses the seq to delete from (both happen to be `number`).
|
||||
* A stored session's durable prefix as read back from a backend: its {@link SessionHeader},
|
||||
* the preserved (seq-contiguous, parseable) event prefix, and an OPAQUE `tornMarker` that is
|
||||
* present iff a never-committed torn tail must be truncated before further writes.
|
||||
*/
|
||||
export interface StoredPrefix<TornMarker = unknown> {
|
||||
meta: SessionHeader
|
||||
@@ -112,16 +85,11 @@ interface SessionState {
|
||||
/** The next seq the backend expects to append (the stored log length). */
|
||||
cursor: number
|
||||
/**
|
||||
* Whether the backend has physically written this session (a JSONL file /
|
||||
* SQLite row exists). `create()` registers state LAZILY — cursor 0,
|
||||
* materialized false, nothing on disk — so an empty session leaves no
|
||||
* artifact and the FIRST `appendBatch` writes the header + its events in ONE
|
||||
* transaction (the "a row exists ⇔ it has events" invariant `list`
|
||||
* relies on; a separate up-front materialize could crash leaving a row with
|
||||
* zero events). The flag is the only signal that distinguishes a session
|
||||
* registered-but-never-written from one durably present, which the reclaim
|
||||
* path needs (an abandoned id with no artifact AND no buffered events is free
|
||||
* to reuse; a materialized one is a real collision).
|
||||
* Whether the backend has physically written this session (a JSONL file / SQLite row
|
||||
* exists). `create()` registers state LAZILY — cursor 0, materialized false, nothing on disk
|
||||
* — so an empty session leaves no artifact and the FIRST `appendBatch` writes the header +
|
||||
* its events in one transaction (the "a row exists ⇔ it has events" invariant `list` relies
|
||||
* on; a separate up-front materialize could crash leaving a row with zero events).
|
||||
*/
|
||||
materialized: boolean
|
||||
/**
|
||||
@@ -224,10 +192,9 @@ export class PersistenceCoordinator<TornMarker = unknown> {
|
||||
// Validate serializability BEFORE cloning so a bad event surfaces the typed
|
||||
// error rather than an opaque DataCloneError from structuredClone.
|
||||
assertSerializable(events)
|
||||
// Deep-snapshot the batch HERE, before the op waits behind the per-session
|
||||
// chain: a caller that mutates a live array (e.g. session.events) — or an
|
||||
// event inside it — before the op runs would otherwise have those changes
|
||||
// persisted. The clone is taken synchronously (at call time).
|
||||
// Deep-snapshot the batch HERE, before the op waits behind the per-session chain: a caller
|
||||
// that mutates a live array (e.g. session.events) — or an event inside it — before the op
|
||||
// runs would otherwise have those changes persisted.
|
||||
const batch = events.map(e => structuredClone(e))
|
||||
return this.serialize(id, () => this.appendCore(id, batch))
|
||||
}
|
||||
@@ -268,11 +235,9 @@ export class PersistenceCoordinator<TornMarker = unknown> {
|
||||
const { meta, events, tornMarker } = stored
|
||||
this.assertVersion(meta)
|
||||
|
||||
// Crash-recovery: if the log ended mid-turn (real, preserved events but no
|
||||
// closing turn/end), close it durably DURING load so disk, the returned log,
|
||||
// and the cursor all agree. The interrupted turn's real events are preserved,
|
||||
// never truncated (a turn can be huge — the session-persistence RFC); only a
|
||||
// never-fully-written torn tail fragment is discarded.
|
||||
// Crash-recovery: if the log ended mid-turn (real, preserved events but no closing
|
||||
// turn/end), close it durably DURING load so disk, the returned log, and the cursor all
|
||||
// agree.
|
||||
const closers = interruptedTurnClosers(events)
|
||||
const balanced = [...events, ...closers]
|
||||
|
||||
@@ -288,12 +253,7 @@ export class PersistenceCoordinator<TornMarker = unknown> {
|
||||
return { meta, events: balanced }
|
||||
}
|
||||
|
||||
// NOTE: there is deliberately no coordinator `list()`. Listing needs none of
|
||||
// the coordinator's orchestration (no per-id serialization, no cursor, no
|
||||
// in-memory state) — it is a pure read of stored metadata. A backend's public
|
||||
// `list()` IS the {@link PersistenceBackend.list} hook (one method); routing it
|
||||
// through the coordinator would only forward to that same hook, so the
|
||||
// coordinator stays out of the listing path entirely.
|
||||
// NOTE: there is deliberately no coordinator `list()`.
|
||||
|
||||
// --- per-id serialization + adoption helpers ---
|
||||
|
||||
@@ -395,9 +355,8 @@ export class PersistenceCoordinator<TornMarker = unknown> {
|
||||
// against later mutation of the live event objects.
|
||||
const seed = session.events.map(e => structuredClone(e))
|
||||
const p = this.onCreated(session, seed)
|
||||
// Attach a no-op rejection handler so a failing init does not surface as an
|
||||
// unhandled rejection if no flush observes `p` before it rejects. The REAL
|
||||
// error is still delivered: flush/dispose await the same `p` from the map.
|
||||
// Attach a no-op rejection handler so a failing init does not surface as an unhandled
|
||||
// rejection if no flush observes `p` before it rejects.
|
||||
p.catch(() => { /* observed by flush/dispose via the stored promise */ })
|
||||
this.inits.set(session, p)
|
||||
return p
|
||||
@@ -418,15 +377,6 @@ export class PersistenceCoordinator<TornMarker = unknown> {
|
||||
|
||||
/**
|
||||
* On session/created: sync the backend's in-memory state to a live Session.
|
||||
*
|
||||
* Cases, by whether this backend tracks the id and whether an artifact exists:
|
||||
* 1. Already tracked → no-op (or claim ownerless state if the seed matches,
|
||||
* or reclaim a truly-abandoned id, else reject as a collision).
|
||||
* 2. Not tracked, an artifact EXISTS at this cwd and is a seq-aligned PREFIX
|
||||
* of the live events → ADOPT it (HMR/reload), persisting any live suffix.
|
||||
* 3. Not tracked, an artifact EXISTS but is NOT a prefix → REJECT (collision).
|
||||
* 4. Not tracked and NO artifact → a genuinely new session: register meta
|
||||
* (lazy) and persist its seed once.
|
||||
*/
|
||||
private async onCreated(session: Session, seed: readonly SessionEvent[]): Promise<void> {
|
||||
const id = session.header.id
|
||||
@@ -436,16 +386,7 @@ export class PersistenceCoordinator<TornMarker = unknown> {
|
||||
/* v8 ignore next -- initFor dedupes per session object; same-object re-entry can't occur */
|
||||
if (tracked.owner === session) return
|
||||
if (tracked.owner === undefined) {
|
||||
// Ownerless state from the public create()/load() API. The FIRST live
|
||||
// session claims it — but ONLY if BOTH the cwd scope and the seed match.
|
||||
// The cwd guard mirrors case-2's cwd-scoped loadLive(): a same-id
|
||||
// ownerless artifact at a DIFFERENT cwd is a collision, not a claim
|
||||
// (claiming it would append the live cwd's events under the stored
|
||||
// header's cwd, the exact cross-cwd corruption the loadLive scope
|
||||
// prevents). The seed guard then ensures the live events reproduce the
|
||||
// persisted prefix (else a fresh, unrelated session reusing the id would
|
||||
// have its seq 0..cursor-1 events filtered as already-written and
|
||||
// grafted on).
|
||||
// Ownerless state from the public create()/load() API.
|
||||
if (tracked.meta.cwd !== session.header.cwd) {
|
||||
throw new Error(`session "${id}" is already persisted at a different cwd (persisted: ${String(tracked.meta.cwd)}, live: ${String(session.header.cwd)}) (id collision)`)
|
||||
}
|
||||
@@ -519,10 +460,8 @@ export class PersistenceCoordinator<TornMarker = unknown> {
|
||||
}
|
||||
|
||||
private async flush(session: Session): Promise<void> {
|
||||
// Wait for the session's init (onCreated) so the state/cursor and any
|
||||
// fork-seed persistence are in place before draining. Awaiting the same
|
||||
// promise initFor stored also surfaces an init failure (e.g. a collision)
|
||||
// here, where the caller of session/flush observes it.
|
||||
// Wait for the session's init (onCreated) so the state/cursor and any fork-seed persistence
|
||||
// are in place before draining.
|
||||
await this.inits.get(session)
|
||||
// Serialize the WHOLE drain (read cursor → append → splice) on the per-session
|
||||
// chain so two concurrent flushes cannot both read the same cursor and
|
||||
@@ -534,10 +473,7 @@ export class PersistenceCoordinator<TornMarker = unknown> {
|
||||
private async drain(session: Session): Promise<void> {
|
||||
const buffer = this.buffers.get(session)
|
||||
if (!buffer?.length) return
|
||||
// Copy WITHOUT removing: the buffer is the only durable-pending copy of these
|
||||
// events. Drain it only AFTER the append commits; events pushed during the
|
||||
// await sit past batch.length and survive the prefix splice, so a
|
||||
// retry/dispose re-drains the rest.
|
||||
// Copy WITHOUT removing: the buffer is the only durable-pending copy of these events.
|
||||
const batch = buffer.slice()
|
||||
const state = this.states.get(session.header.id)
|
||||
// Only append events at or beyond the write cursor (a resumed session's seed
|
||||
|
||||
@@ -1,23 +1,7 @@
|
||||
/**
|
||||
* The durable session-persistence seam (`ctx.sessionPersistence`): an abstract
|
||||
* service defining WHAT a persistence backend does — durably store, reload,
|
||||
* and list sessions — without saying HOW. Implementations subclass
|
||||
* {@link SessionPersistence} and register themselves as the
|
||||
* `sessionPersistence` service; `@deepseek-ai/dsh-session-persistence-jsonl`
|
||||
* (an append-only JSONL log per session) is the first and
|
||||
* `@deepseek-ai/dsh-session-persistence-sqlite` (`node:sqlite`, one row per
|
||||
* event) is a second that validates the seam is backend-agnostic by passing
|
||||
* the same `runPersistenceContract` suite. Further backends swap in an object
|
||||
* store or a remote service without touching the consumers (the write-path
|
||||
* plugin, the agent-loop resume seam).
|
||||
*
|
||||
* The persisted unit IS the existing {@link SessionEvent} — there is no
|
||||
* parallel "persisted message" type the log must be converted to and from
|
||||
* (faithful to the event-sourced model: the log is the single source of
|
||||
* truth). Metadata that is NOT replayable conversation state (format version,
|
||||
* cwd, lineage, seed boundary) travels separately as {@link SessionHeader},
|
||||
* which is owned by `dsh-session` and re-exported here.
|
||||
*
|
||||
* The durable session-persistence seam (`ctx.sessionPersistence`): an abstract service
|
||||
* defining what a persistence backend does — durably store, reload, and list sessions —
|
||||
* without saying how.
|
||||
* @module @deepseek-ai/dsh-session-persistence
|
||||
*/
|
||||
|
||||
@@ -39,12 +23,10 @@ declare module 'cordis' {
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether a live session's seed reproduces a persisted prefix exactly. Backends
|
||||
* use this collision check to distinguish a legitimate resume/HMR rebind from a
|
||||
* different live session reusing an existing session id.
|
||||
* Whether a live session's seed reproduces a persisted prefix exactly. Backends use this
|
||||
* collision check to distinguish a legitimate resume/HMR rebind from a different live session
|
||||
* reusing an existing session id.
|
||||
*
|
||||
* The comparison includes the full event payload, not just seq/type/time, so a
|
||||
* mutated seed cannot be grafted onto a durable log with the same envelope.
|
||||
* @param seed - the live session's creation-time event snapshot.
|
||||
* @param prefix - the persisted prefix the seed must reproduce.
|
||||
* @returns `true` when the prefix fits within the seed and every event matches by JSON text.
|
||||
@@ -72,32 +54,9 @@ export function assertSerializable(events: readonly SessionEvent[]): void {
|
||||
}
|
||||
|
||||
/**
|
||||
* Abstract durable session-persistence service. Subclass, implement the
|
||||
* abstract methods, and load the subclass as a plugin — it registers as
|
||||
* `ctx.sessionPersistence` (one implementation per context; loading a second
|
||||
* throws, cordis' standard duplicate-service behavior).
|
||||
*
|
||||
* Contracts every implementation MUST honor (a DB backend asserts them inside
|
||||
* a transaction; a file backend appends at EOF):
|
||||
*
|
||||
* - **Append-only; a crashed turn is closed, not truncated.** Committed events
|
||||
* — those at or below a flushed `turn/end` — are never rewritten. A crash can
|
||||
* leave an unclosed final turn whose events are real (and possibly large);
|
||||
* {@link load} preserves them and closes the orphaned turn with synthetic
|
||||
* boundary events (see {@link load}). Only a never-fully-written torn tail
|
||||
* fragment is discarded.
|
||||
* - **Contiguous seq.** A persisted log is contiguous: `events[i].seq === i`.
|
||||
* {@link load} rejects a parse error or a `seq` gap in the COMMITTED region
|
||||
* (unloadable); {@link append}'s first event `seq` MUST equal the backend's
|
||||
* stored next-seq (after `load` has balanced any interrupted turn).
|
||||
* - **JSON-serializable data.** `SessionEventMap` is merge-extensible and
|
||||
* `event.data` is typed only as `SessionEventMap[K]`, so {@link append}
|
||||
* REJECTS non-JSON-serializable data with an error naming the offending
|
||||
* event type. A backend snapshots (serializes/clones) each event when it
|
||||
* buffers, since `session.events` hands out the live mutable object.
|
||||
* - **Durability.** {@link append} returns only once the batch is durable
|
||||
* (the file backend fsyncs; a DB commits). {@link create} MAY defer the
|
||||
* physical write until the first {@link append} (lazy materialization).
|
||||
* Abstract durable session-persistence service. Subclass, implement the abstract methods, and
|
||||
* load the subclass as a plugin — it registers as `ctx.sessionPersistence` (one implementation
|
||||
* per context; loading a second throws, cordis' standard duplicate-service behavior).
|
||||
*/
|
||||
export abstract class SessionPersistence extends Service {
|
||||
constructor(ctx: Context) {
|
||||
@@ -125,26 +84,10 @@ export abstract class SessionPersistence extends Service {
|
||||
abstract append(id: SessionId, events: readonly SessionEvent[]): Promise<void>
|
||||
|
||||
/**
|
||||
* Reload a session: its {@link SessionHeader} plus the event log up to the last
|
||||
* durable checkpoint. Returns `meta` AND `events` so the live session is
|
||||
* reconstructed with its `cwd`/lineage, not just its log.
|
||||
* Reload a session: its {@link SessionHeader} plus the event log up to the last durable
|
||||
* checkpoint. Returns `meta` AND `events` so the live session is reconstructed with its
|
||||
* `cwd`/lineage, not just its log.
|
||||
*
|
||||
* The loop only flushes at `turn/end`, so a crash can leave a durable log
|
||||
* whose final turn never closed: real, fully-written events sit after the last
|
||||
* `turn/end`. Those events are PRESERVED — a single turn can be huge in a
|
||||
* long-horizon task, so truncating it would destroy real work — and `load`
|
||||
* CLOSES the orphaned turn by durably appending the minimal synthetic boundary
|
||||
* events: an error `tool/result` for every `tool-call` the crash left
|
||||
* unanswered (so the rehydrated history is a valid provider transcript — a
|
||||
* dangling assistant tool-call is otherwise rejected), then a `step/end` if a
|
||||
* step was open, then a `turn/end` carrying the `{ kind: 'interrupted' }`
|
||||
* reason. The returned `events` therefore end on a balanced `turn/end` and are
|
||||
* immediately usable as a session seed. Only a never-fully-written TORN tail
|
||||
* fragment (a half-written final record) is discarded. Returned events are
|
||||
* contiguous (`events[i].seq === i`); a parse error or a `seq` gap in the
|
||||
* COMMITTED region (at or before the last real `turn/end`) makes the session
|
||||
* unloadable (reject). Rejects an unknown format `version`. See the session-persistence RFC for
|
||||
* the crash-recovery contract.
|
||||
* @param id - the persisted session to reload.
|
||||
* @returns the header plus the event log, ending on a balanced `turn/end` —
|
||||
* immediately usable as a session seed.
|
||||
|
||||
@@ -43,15 +43,8 @@ export function oneTurnLog(): SessionEvent[] {
|
||||
}
|
||||
|
||||
/**
|
||||
* Append a whole event log to a LIVE session, event by event, forwarding the
|
||||
* surface metadata each event already carries. A bare `append(e.type, e.data)`
|
||||
* over a `SessionEvent[]` widens the type argument to the union, where the
|
||||
* typed overload's mandatory-marker rule collapses to optional — and `append`'s
|
||||
* runtime guard then rejects a surface-eligible event with no marker. This
|
||||
* helper forwards the `surfaceOp`/`sourceEventSeqs` VERBATIM from the source
|
||||
* event (it does not synthesize a default), so a well-formed recorded log
|
||||
* round-trips through a live session intact and a fixture that forgot a marker
|
||||
* still trips the guard.
|
||||
* Append a whole event log to a LIVE session, event by event, forwarding the surface metadata
|
||||
* each event already carries.
|
||||
*/
|
||||
export function appendLog(session: Session, events: readonly SessionEvent[]): void {
|
||||
for (const e of events) {
|
||||
@@ -222,10 +215,9 @@ export function runPersistenceContract(name: string, make: () => Promise<Contrac
|
||||
it('append rejects non-JSON-serializable event data, naming the event type', async () => {
|
||||
const { persistence, dispose } = await make()
|
||||
try {
|
||||
// Every value `isJsonValue` rejects must be rejected by the backend, not
|
||||
// just BigInt — otherwise a backend could pass this contract while still
|
||||
// accepting values that corrupt the durable round-trip. Each is a
|
||||
// plugin-added `extra` field on a single user/message (seq 0).
|
||||
// Every value `isJsonValue` rejects must be rejected by the backend, not just BigInt —
|
||||
// otherwise a backend could pass this contract while still accepting values that
|
||||
// corrupt the durable round-trip.
|
||||
const cyclic: Record<string, unknown> = { type: 'text', text: 'x' }
|
||||
cyclic['self'] = cyclic
|
||||
const badValues: unknown[] = [
|
||||
|
||||
@@ -1,28 +1,5 @@
|
||||
/**
|
||||
* Reusable ORCHESTRATION suite for any backend that composes a
|
||||
* {@link PersistenceCoordinator}. Where {@link runPersistenceContract} (in
|
||||
* contract.ts) pins the public read/write SEMANTICS, this suite pins the
|
||||
* coordinator's WRITE-PATH ORCHESTRATION — the behavior that is identical across
|
||||
* every first-party backend because it lives in the shared coordinator, not in
|
||||
* the storage primitives: the `session/created` → `session/event` →
|
||||
* `session/flush` → dispose drain, lazy materialization, fork-seed persistence,
|
||||
* the four `onCreated` adoption cases (new / HMR-adopt / collision /
|
||||
* ownerless-claim), crash-tail repair on load, and dispose-time quiescence.
|
||||
*
|
||||
* A backend imports {@link runCoordinatorContract} and calls it with a
|
||||
* {@link CoordinatorFixture} factory that knows how to (a) mount the REAL
|
||||
* backend plugin on a {@link Context} over a SHARED storage scope (so HMR/reload
|
||||
* tests can dispose one instance and mount another over the same bytes/rows),
|
||||
* and (b) inject a never-committed torn tail for one session
|
||||
* ({@link CoordinatorFixture.corruptTail}) so the through-coordinator torn-tail
|
||||
* repair branch is exercised against real storage. The suite drives everything
|
||||
* through the PUBLIC {@link SessionPersistence} API + the cordis SessionStore
|
||||
* write path — never the storage primitives directly — so it runs unchanged for
|
||||
* every backend (memory / jsonl / sqlite).
|
||||
*
|
||||
* Each scenario lives here once and runs once per backend through the fixture;
|
||||
* the per-backend specs keep ONLY their storage-mechanics tests.
|
||||
*
|
||||
* Reusable ORCHESTRATION suite for any backend that composes a {@link PersistenceCoordinator}.
|
||||
* @module @deepseek-ai/dsh-session-persistence/tests/coordinator-contract
|
||||
*/
|
||||
|
||||
@@ -124,10 +101,9 @@ export function runCoordinatorContract(name: string, makeFixture: () => Promise<
|
||||
})
|
||||
|
||||
it('round-trips the seed boundary (seedLength) through persistence', async () => {
|
||||
// A forked child records how many leading events were inherited via the
|
||||
// seed; the boundary must survive a reload (so a resume/replay can tell the
|
||||
// inherited prefix from the child's own events). Both backends carry it on
|
||||
// the header — JSONL on the header line, SQLite in the seed_length column.
|
||||
// A forked child records how many leading events were inherited via the seed; the
|
||||
// boundary must survive a reload (so a resume/replay can tell the inherited prefix from
|
||||
// the child's own events).
|
||||
const fix = await makeFixture()
|
||||
const { ctx, fiber } = await freshCtx(fix)
|
||||
try {
|
||||
@@ -303,10 +279,8 @@ export function runCoordinatorContract(name: string, makeFixture: () => Promise<
|
||||
session.append('turn/end', { turn: 1, reason: { kind: 'completed' } })
|
||||
await ctx.parallel('session/flush', session)
|
||||
|
||||
// Hot-reload: dispose instance 1, mount instance 2 over the SAME storage
|
||||
// while the session stays live. Instance 2 has an empty states map but the
|
||||
// log is materialized and is a prefix of the live events — it must ADOPT
|
||||
// (not reject). A second turn appended after reload then persists.
|
||||
// Hot-reload: dispose instance 1, mount instance 2 over the same storage while the
|
||||
// session stays live.
|
||||
await backend1.dispose()
|
||||
await fix.mount(ctx)
|
||||
session.append('turn/start', { turn: 2, trigger: { kind: 'message', source: { kind: 'user' } } })
|
||||
@@ -398,9 +372,8 @@ export function runCoordinatorContract(name: string, makeFixture: () => Promise<
|
||||
await first.fiber.dispose()
|
||||
}
|
||||
|
||||
// A FRESH backend + a NEW live session with the same id but NO explicit
|
||||
// resume. onCreated treats it as new; create() rejects because a log already
|
||||
// exists. The rejection surfaces via the init promise (flush awaits it).
|
||||
// A fresh backend + a NEW live session with the same id but NO explicit resume. onCreated
|
||||
// treats it as new; create() rejects because a log already exists.
|
||||
const second = await freshCtx(fix)
|
||||
try {
|
||||
const s2 = second.ctx.sessions.create(SessionId('collide'), { meta: { cwd: WORK } })
|
||||
|
||||
@@ -16,18 +16,8 @@ type MemoryStore = Map<string, { meta: SessionHeader; events: SessionEvent[] }>
|
||||
interface MemoryConfig { store?: MemoryStore }
|
||||
|
||||
/**
|
||||
* A trivial in-memory {@link SessionPersistence} that composes a
|
||||
* {@link PersistenceCoordinator} over a dependency-free `Map`-backed
|
||||
* {@link PersistenceBackend}. It is BOTH the coordinator's reference vehicle
|
||||
* (the simplest possible storage — a `Map<id, {meta, events}>` with no torn
|
||||
* tails, so `tornMarker` is always undefined) and the cover for the abstract
|
||||
* base's constructor + service registration. The real durable backends are
|
||||
* `@deepseek-ai/dsh-session-persistence-jsonl` / `-sqlite`.
|
||||
*
|
||||
* The store can be supplied via config so two backend instances share one Map —
|
||||
* the in-RAM analogue of two backends over the same file/db, which the
|
||||
* coordinator orchestration suite's HMR/reload tests need (a fresh instance with
|
||||
* an empty in-memory states map adopting an already-materialized session).
|
||||
* A trivial in-memory {@link SessionPersistence} that composes a {@link
|
||||
* PersistenceCoordinator} over a dependency-free `Map`-backed {@link PersistenceBackend}.
|
||||
*/
|
||||
class MemoryPersistence extends SessionPersistence implements PersistenceBackend<never> {
|
||||
static inject = ['sessions']
|
||||
@@ -123,11 +113,6 @@ runPersistenceContract('memory', async () => {
|
||||
})
|
||||
|
||||
// Run the shared coordinator orchestration suite against the in-memory backend.
|
||||
// A per-fixture Map is the shared "storage", so two mounted instances see the
|
||||
// same materialized sessions (HMR/reload). `corruptTail` is OMITTED: a Map store
|
||||
// writes atomically in RAM and has no torn tails, so the suite's torn-tail test
|
||||
// self-skips (and asserts the omission). The real torn-tail repair branch is
|
||||
// covered by the jsonl/sqlite fixtures, which CAN inject one.
|
||||
runCoordinatorContract('memory', async (): Promise<CoordinatorFixture> => {
|
||||
const store: MemoryStore = new Map()
|
||||
return {
|
||||
|
||||
Reference in New Issue
Block a user