@deepseek-ai/dsh-session-persistence 0.1.2-rc.1 → 0.1.3-alpha.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,9 +1,105 @@
1
- /** Stable failures exposed by the session-persistence service. */
1
+ /**
2
+ * Stable failures exposed by the session-persistence service and its handles,
3
+ * including the format refusals shared by every backend: a stored log this
4
+ * build cannot faithfully interpret is refused, never misread, and the
5
+ * refusal points at the raw artifact when the backend keeps one per session.
6
+ * @module @deepseek-ai/dsh-session-persistence/errors
7
+ */
2
8
  import type { SessionId } from '@deepseek-ai/dsh-session';
3
- /** The requested Session identity has no materialized durable log. */
9
+ /** The requested Session identity has no durable log visible to this caller. */
4
10
  export declare class SessionPersistenceNotFoundError extends Error {
5
11
  readonly sessionId: SessionId;
6
12
  /** @param sessionId - absent durable Session identity. */
7
13
  constructor(sessionId: SessionId);
8
14
  }
15
+ /** `create` targeted a Session identity that already exists in this backend. */
16
+ export declare class SessionAlreadyExistsError extends Error {
17
+ readonly sessionId: SessionId;
18
+ /** @param sessionId - the occupied durable Session identity. */
19
+ constructor(sessionId: SessionId);
20
+ }
21
+ /** A write open found the session already bound to an active write handle. */
22
+ export declare class SessionAlreadyOwnedError extends Error {
23
+ readonly sessionId: SessionId;
24
+ /** @param sessionId - the session whose write ownership is taken. */
25
+ constructor(sessionId: SessionId);
26
+ }
27
+ /** A mutation (`append`/`flush`) was called on a read handle. */
28
+ export declare class SessionReadOnlyError extends Error {
29
+ readonly sessionId: SessionId;
30
+ /**
31
+ * @param sessionId - the session the read handle observes.
32
+ * @param operation - the refused mutating operation name.
33
+ */
34
+ constructor(sessionId: SessionId, operation: string);
35
+ }
36
+ /**
37
+ * A write handle's ownership is permanently gone: its lease expired, a renewal
38
+ * failed, or the durable ownership record no longer names this handle. The
39
+ * handle never re-acquires ownership — close it and reopen for write.
40
+ *
41
+ * Declared for the cross-process lease layer; the shipped in-process backends
42
+ * never throw it yet.
43
+ */
44
+ export declare class SessionOwnershipLostError extends Error {
45
+ readonly sessionId: SessionId;
46
+ /** @param sessionId - the session whose write ownership this handle lost. */
47
+ constructor(sessionId: SessionId);
48
+ }
49
+ /** An operation was called on a handle after `close()` was called. */
50
+ export declare class SessionHandleClosedError extends Error {
51
+ readonly sessionId: SessionId;
52
+ /**
53
+ * @param sessionId - the session the closed handle addressed.
54
+ * @param operation - the refused operation name.
55
+ */
56
+ constructor(sessionId: SessionId, operation: string);
57
+ }
58
+ /**
59
+ * A backend-resolved, per-session local artifact location. Carried only by
60
+ * refusal diagnostics ({@link SessionFormatUnsupportedError}) so a user can
61
+ * find the raw log a build refused to interpret; it is not a consumer-facing
62
+ * query — log access goes through a session handle's `read`.
63
+ */
64
+ export interface SessionLocation {
65
+ /** Backend-specific artifact kind, for example `jsonl`. */
66
+ readonly kind: string;
67
+ /** Absolute path to this session's backend-owned artifact. */
68
+ readonly path: string;
69
+ }
70
+ /** Durable session contents failed validation after a successful backend read. */
71
+ export declare class SessionPersistenceCorruptionError extends Error {
72
+ /**
73
+ * @param message - stable corruption context.
74
+ * @param options - original validation failure.
75
+ */
76
+ constructor(message: string, options: ErrorOptions);
77
+ }
78
+ /**
79
+ * The stored log is intact but this runtime cannot faithfully interpret it:
80
+ * the header carries an unsupported format version, or an event's type is
81
+ * unknown to this build. Distinct from {@link SessionPersistenceCorruptionError}
82
+ * — nothing is damaged; the raw log remains readable at {@link location} when
83
+ * the backend keeps one artifact per session.
84
+ */
85
+ export declare class SessionFormatUnsupportedError extends Error {
86
+ readonly location?: SessionLocation | undefined;
87
+ /**
88
+ * @param message - stable reason the log cannot be interpreted, already
89
+ * including the raw-log path when one exists.
90
+ * @param location - the backend's artifact location, when one exists.
91
+ */
92
+ constructor(message: string, location?: SessionLocation | undefined);
93
+ }
94
+ /**
95
+ * Direction-aware refusal text for a stored session whose format version this
96
+ * build does not read. Shared by load-time checks and by backends that must
97
+ * refuse BEFORE decoding version-dependent structure (a future format may not
98
+ * satisfy this build's structural checks at all, and the user must see
99
+ * "upgrade the harness", never "corrupt").
100
+ * @param id - the stored session id, for message context.
101
+ * @param version - the stored format version.
102
+ * @returns the stable refusal text, without a raw-log path suffix.
103
+ */
104
+ export declare function sessionFormatVersionRefusal(id: string, version: number): string;
9
105
  //# sourceMappingURL=errors.d.ts.map
@@ -0,0 +1,107 @@
1
+ /**
2
+ * The per-session storage handle: one open channel onto a stored session's
3
+ * append-only event log, returned by `SessionPersistence.create`/`open`.
4
+ * @module @deepseek-ai/dsh-session-persistence/handle
5
+ */
6
+ import type { SessionEvent, SessionHeader, SessionId, SessionLogOffset, SessionSeedEventState } from '@deepseek-ai/dsh-session';
7
+ /**
8
+ * Log access granted by an open. `write` is read-write: the session's single
9
+ * mutator, which also reads its own log. `read` only observes — it never
10
+ * takes ownership and works while another handle or process holds `write`.
11
+ */
12
+ export type SessionAccess = 'read' | 'write';
13
+ /** Options for {@link SessionHandle.read}. */
14
+ export interface SessionHandleReadOptions {
15
+ /** Optional cancellation for backend read work. */
16
+ readonly signal?: AbortSignal;
17
+ }
18
+ /** One persistence event slice returned by {@link SessionHandle.read}. */
19
+ export interface SessionHandleReadResult {
20
+ /**
21
+ * Whether event values are exclusively owned or shared only after deep
22
+ * freezing. Slicing preserves the producer's state even when no events remain.
23
+ */
24
+ readonly eventState: SessionSeedEventState;
25
+ /** Event values in a caller-owned outer array. */
26
+ readonly events: readonly SessionEvent[];
27
+ }
28
+ /** Options for {@link SessionHandle.append}. */
29
+ export interface SessionHandleAppendOptions {
30
+ /** Optional cancellation observed before the write starts. */
31
+ readonly signal?: AbortSignal;
32
+ }
33
+ /** Options for {@link SessionHandle.flush}. */
34
+ export interface SessionHandleFlushOptions {
35
+ /** Optional cancellation observed before the barrier starts. */
36
+ readonly signal?: AbortSignal;
37
+ }
38
+ /**
39
+ * One open channel onto a stored session. A handle is single-owner state, not
40
+ * a shared service: `read` never backtracks below what this handle already
41
+ * observed, a `write` handle reads its own successful appends, and `close()`
42
+ * is the one teardown (idempotent, uncancellable; `Symbol.asyncDispose`
43
+ * delegates to it). Every operation on a closed handle rejects with
44
+ * `SessionHandleClosedError`.
45
+ *
46
+ * Freshness across handles: once an `append` or `flush` resolves on a write
47
+ * handle, every read STARTED afterwards on the same backend instance — on any
48
+ * handle, or through `stat`/`list` — observes at least that prefix.
49
+ * Reads concurrent with a mutation carry no ordering promise beyond the valid
50
+ * contiguous prefix.
51
+ */
52
+ export interface SessionHandle extends AsyncDisposable {
53
+ /** The stored session this handle addresses. */
54
+ readonly id: SessionId;
55
+ /** The immutable stored header, fixed at `create`/`open`. */
56
+ readonly header: SessionHeader;
57
+ /**
58
+ * Exact fork-inherited prefix length stored with the log; `0` when
59
+ * `header.isSeeded` is false. Storage metadata paired with the header for
60
+ * every body read, never part of the replayable event log.
61
+ */
62
+ readonly inheritedEventCount: SessionLogOffset;
63
+ /** Whether this handle may mutate the log. */
64
+ readonly access: SessionAccess;
65
+ /**
66
+ * Read a slice of the valid contiguous logical log. The slice is a legal log
67
+ * prefix segment: a torn physical tail is never returned, and repeated reads
68
+ * on this handle never observe an older state than a prior read.
69
+ * @param offset - first logical event seq to include; defaults to `0`.
70
+ * @param length - maximum number of events to return; defaults to the rest
71
+ * of the log. An offset at or past the end returns an empty list.
72
+ * @param options - optional cancellation.
73
+ * @returns the caller-owned outer slice plus the ownership state of its event values.
74
+ */
75
+ read(offset?: number, length?: number, options?: SessionHandleReadOptions): Promise<SessionHandleReadResult>;
76
+ /**
77
+ * Append a contiguous batch continuing the current logical end. The first
78
+ * event's `seq` MUST equal the stored next-seq; committed events are never
79
+ * rewritten. Persistence is best-effort: on resolution the batch is
80
+ * accepted, ordered, and visible to reads on this backend instance, but
81
+ * only a resolved {@link flush} promises it survives a crash — a backend
82
+ * may buffer or batch physical writes behind append. Rejects with
83
+ * `SessionReadOnlyError` on a read handle and `SessionOwnershipLostError`
84
+ * when write ownership is gone.
85
+ * @param events - the contiguous batch, in seq order.
86
+ * @param options - optional cancellation observed before the write starts.
87
+ */
88
+ append(events: readonly SessionEvent[], options?: SessionHandleAppendOptions): Promise<void>;
89
+ /**
90
+ * The durability barrier — the one operation that promises storage: on
91
+ * resolution every acknowledged append is durable and the session is
92
+ * materialized for other processes; an empty created session becomes
93
+ * durably listable here. Callers that must survive a crash flush; a backend
94
+ * whose `append` already persists on resolution treats this as
95
+ * materialize-if-needed. Rejects with `SessionReadOnlyError` on a read
96
+ * handle.
97
+ * @param options - optional cancellation observed before the barrier starts.
98
+ */
99
+ flush(options?: SessionHandleFlushOptions): Promise<void>;
100
+ /**
101
+ * Release the handle: a read handle frees local resources; a write handle
102
+ * completes pending durability and releases write ownership. Idempotent,
103
+ * asynchronous, and deliberately not cancellable.
104
+ */
105
+ close(): Promise<void>;
106
+ }
107
+ //# sourceMappingURL=handle.d.ts.map
@@ -1,235 +1,158 @@
1
1
  /**
2
2
  * Durable session-persistence Service Definition (`ctx.sessionPersistence`). Backends store
3
3
  * {@link SessionEvent}s as the event-sourced log and carry non-replayable
4
- * {@link SessionHeader} metadata separately.
4
+ * {@link SessionHeader} metadata separately; callers address one stored
5
+ * session through a {@link SessionHandle} obtained from `create`/`open`.
5
6
  * @module @deepseek-ai/dsh-session-persistence
6
7
  */
7
8
  import { Context, Service } from '@deepseek-ai/cordis';
8
- import { SessionPreparation, SessionLogOffset } from '@deepseek-ai/dsh-session';
9
- import type { Session, SessionEvent, SessionId, SessionHeader } from '@deepseek-ai/dsh-session';
9
+ import type { SessionEvent, SessionHeader, SessionId, SessionLogOffset } from '@deepseek-ai/dsh-session';
10
+ import type { SessionHandle, SessionAccess } from './handle.ts';
10
11
  import type { SessionPersistenceRevision } from './revision.ts';
11
12
  export type { SessionHeader } from '@deepseek-ai/dsh-session';
12
13
  export { SessionPersistenceRevision } from './revision.ts';
13
- export { SessionPersistenceNotFoundError } from './errors.ts';
14
- /** Lightweight immutable source identity returned without loading a full log. */
14
+ export type { SessionAccess, SessionHandle, SessionHandleAppendOptions, SessionHandleFlushOptions, SessionHandleReadOptions, SessionHandleReadResult, } from './handle.ts';
15
+ export { SessionAlreadyExistsError, SessionAlreadyOwnedError, SessionFormatUnsupportedError, SessionHandleClosedError, SessionOwnershipLostError, SessionPersistenceCorruptionError, SessionPersistenceNotFoundError, SessionReadOnlyError, sessionFormatVersionRefusal, } from './errors.ts';
16
+ export type { SessionLocation } from './errors.ts';
17
+ export { assertContiguous, assertStoredId, assertVersion, materializeAppendBatch, materializeCreateHeader, validateStoredEvents, } from './storage-contract.ts';
18
+ /**
19
+ * Lightweight stored-session observation returned by {@link SessionPersistence.stat}
20
+ * and {@link SessionPersistence.list} without reading the full event log.
21
+ */
15
22
  export interface SessionPersistenceSnapshot {
16
- /** Detached metadata for one materialized session. */
17
- header: SessionHeader;
18
- /** Opaque source-qualified token that changes whenever this stored log changes. */
19
- revision: SessionPersistenceRevision;
23
+ /** Detached metadata for one stored session. */
24
+ readonly header: SessionHeader;
25
+ /** Opaque change token; see {@link SessionPersistence.stat}. */
26
+ readonly revision: SessionPersistenceRevision;
27
+ /** Logical event count, when the backend can provide it cheaply from metadata; otherwise absent. */
28
+ readonly eventCount?: number;
29
+ /** Physical artifact byte size, when the backend can provide it cheaply (JSONL); otherwise absent. */
30
+ readonly sizeBytes?: number;
20
31
  }
21
- /** Logical Session header paired with its exact inherited cut for body-bearing storage operations. */
32
+ /** Options for {@link SessionPersistence.create}. */
33
+ export interface SessionPersistenceCreateOptions {
34
+ /** Optional cancellation observed before backend work starts. */
35
+ readonly signal?: AbortSignal;
36
+ /**
37
+ * Exact fork-inherited prefix length. Required when `header.isSeeded` is
38
+ * true and must be omitted (or `0`) otherwise; the backend refuses a
39
+ * mismatch at create.
40
+ */
41
+ readonly inheritedEventCount?: SessionLogOffset;
42
+ }
43
+ /**
44
+ * Logical Session header paired with its exact inherited cut for body-bearing
45
+ * storage operations. `isSeeded` marks fork lineage on the header; the
46
+ * numeric cut travels beside it, never inside the replayable event log.
47
+ */
22
48
  export interface SessionStorageMetadata {
23
49
  /** Validated immutable Session header. */
24
50
  readonly meta: SessionHeader;
25
51
  /** Number of leading events inherited from the Session's fork parent. */
26
52
  readonly inheritedEventCount: SessionLogOffset;
27
53
  }
28
- /** Immutable logical session prepared from persistence or a live owner. */
54
+ /** Immutable logical session read: storage metadata plus the complete validated event log. */
29
55
  export interface SessionInspection extends SessionStorageMetadata {
30
- /** Validated contiguous logical event log. */
56
+ /** Contiguous validated events from seq 0. */
31
57
  readonly events: readonly SessionEvent[];
32
58
  }
33
- /** Detached logical suffix returned by one explicit stored-log offset read. */
34
- export interface SessionEventSuffix extends SessionStorageMetadata {
35
- /** First requested log offset; {@link events} contains only seqs at or after it. */
36
- readonly fromSeq: SessionLogOffset;
37
- /** Valid contiguous stored events at or after {@link fromSeq}; not a complete Session log when the offset is nonzero. */
38
- readonly events: readonly SessionEvent[];
59
+ /** Options for {@link SessionPersistence.open}. */
60
+ export interface SessionPersistenceOpenOptions {
61
+ /** Optional cancellation observed before backend work starts. */
62
+ readonly signal?: AbortSignal;
39
63
  }
40
- /** A borrowed exact Session source returned from a cold materialization or concurrent live owner. */
41
- export type BorrowedSessionSource = Disposable & ({
42
- /** A reusable unpublished Session is pinned until this observation is disposed. */
43
- readonly source: 'prepared';
44
- /** Immutable header and logical event prefix observed together. */
45
- readonly inspection: SessionInspection;
46
- /** Durable revision represented by the prepared source. */
47
- readonly revision: SessionPersistenceRevision;
48
- /** Exact unpublished Session retained for a later {@link prepare}. */
49
- readonly preparedSession: Session;
50
- } | {
51
- /** A live Session won source resolution while the persistence read was starting. */
52
- readonly source: 'live';
53
- /** Immutable live header and event prefix observed together. */
54
- readonly inspection: SessionInspection;
55
- });
56
- /** A backend's own raw artifact text for one session, verbatim. */
57
- export interface SessionRawArtifact extends SessionStorageMetadata {
58
- /** The artifact's base filename on disk, without any physical encoding suffix. */
59
- readonly filename: string;
60
- /** The artifact's full text content, decoded from the backend's physical encoding. */
61
- readonly content: string;
64
+ /** Options for {@link SessionPersistence.stat}. */
65
+ export interface SessionPersistenceStatOptions {
66
+ /** Optional cancellation for backend metadata reads. */
67
+ readonly signal?: AbortSignal;
68
+ }
69
+ /** Options for {@link SessionPersistence.list}. */
70
+ export interface SessionPersistenceListOptions {
71
+ /** Optional cancellation for backend listing work. */
72
+ readonly signal?: AbortSignal;
62
73
  }
63
- export { DEFAULT_PREPARED_SESSION_CACHE_SIZE, DEFAULT_WRITE_BATCH_MAX_DELAY_MS, MAX_WRITE_BATCH_DELAY_MS, PersistenceCoordinator, SessionFormatUnsupportedError, SessionPersistenceCorruptionError, sessionFormatVersionRefusal, } from './coordinator.ts';
64
- export type { PersistenceBackend, PersistenceCoordinatorOptions, StoredPrefix, StoredSuffix, } from './coordinator.ts';
65
74
  declare module '@deepseek-ai/cordis' {
66
75
  interface Context {
67
76
  sessionPersistence: SessionPersistence;
68
77
  }
69
78
  }
70
79
  /**
71
- * A backend-resolved, per-session local artifact location. The path is an
72
- * absolute target path and can name an artifact that has not materialized yet.
73
- * Consumers must treat it as a location hint, never as an authorization token.
74
- */
75
- export interface SessionLocation {
76
- /** Backend-specific artifact kind, for example `jsonl`. */
77
- readonly kind: string;
78
- /** Absolute path to this session's backend-owned artifact. */
79
- readonly path: string;
80
- }
81
- /**
82
- * Durable append-only session storage. Implementations preserve contiguous,
83
- * losslessly JSON-serializable events; {@link append} resolves only after
84
- * durability, and {@link load} balances a complete interrupted tail without
85
- * rewriting committed events.
80
+ * Durable append-only session storage addressed through per-session handles.
81
+ *
82
+ * Storage semantics shared by every backend: events are contiguous from seq 0
83
+ * and never rewritten; a torn physical tail is never returned to a reader and
84
+ * is truncated by the write path before its first append; reads validate
85
+ * current-format records only and refuse unknown vocabulary fail-closed.
86
+ * `append` persists best-effort; `flush` — per handle or service-wide — is
87
+ * the durability barrier.
88
+ *
89
+ * Visibility: a created session is observable through `stat`/`list`/`open`
90
+ * in this process from the moment `create` resolves, even while a backend
91
+ * defers physical materialization (a pure optimization); other processes see
92
+ * the session only once it materializes, and a session that never
93
+ * materialized before a crash never existed. `SessionHandle.flush` forces
94
+ * materialization.
95
+ *
96
+ * Freshness: once an `append` or `flush` resolves, reads started afterwards
97
+ * on this backend instance observe at least that prefix.
86
98
  */
87
99
  export declare abstract class SessionPersistence extends Service {
88
100
  constructor(ctx: Context);
89
101
  /**
90
- * Resolve this backend's independent local artifact for a session without
91
- * reading, creating, flushing, or otherwise materializing it. A backend
92
- * that does not own one artifact per Session returns `undefined`.
93
- * @param meta - the immutable session header whose artifact is requested.
94
- * @returns the backend-specific absolute location, when one exists.
95
- */
96
- abstract locate(meta: SessionHeader): SessionLocation | undefined;
97
- /**
98
- * Whether this backend exposes one verbatim raw artifact per session.
99
- * A backend that declares `true` must override {@link readRaw}.
102
+ * Create a new stored session and take its write ownership.
103
+ * @param header - the immutable header (id, version, cwd, lineage) to store.
104
+ * @param options - optional cancellation.
105
+ * @returns a `write` handle owned by the caller; close it to release ownership.
106
+ * @throws {SessionAlreadyExistsError} when the id already exists.
100
107
  */
101
- abstract readonly supportsRawArtifacts: boolean;
108
+ abstract create(header: SessionHeader, options?: SessionPersistenceCreateOptions): Promise<SessionHandle>;
102
109
  /**
103
- * Read a session's backend-owned artifact text verbatim — the exact durable
104
- * bytes the backend wrote (decoded from its physical encoding, e.g. a
105
- * decompressed JSONL). The returned `content` is the raw text, not a
106
- * reconstruction from parsed events, so it preserves backend-specific
107
- * serialization (chunk packing, key order, line breaks). Callers first test
108
- * {@link supportsRawArtifacts}; `undefined` then means only that the requested
109
- * session has no materialized artifact.
110
- * @param _id - the persisted session to read (unused by the default: no
111
- * per-session artifact).
112
- * @param signal - optional cancellation for backend read work.
113
- * @returns the raw artifact plus its parsed header, or `undefined` when the
114
- * session is absent.
115
- * @throws when this backend does not expose per-session raw artifacts.
116
- */
117
- readRaw(_id: SessionId, signal?: AbortSignal): Promise<SessionRawArtifact | undefined>;
118
- /**
119
- * Register a new session's metadata. A backend MAY defer the physical write
120
- * until the first {@link append} (lazy materialization), in which case a
121
- * created-but-never-appended session is absent from {@link list}
122
- * — abandoned sessions leave nothing behind.
123
- * @param meta - the immutable header (id, version, cwd, lineage) to record.
124
- * @param inheritedEventCount - exact fork-inherited prefix length. Required
125
- * for a seeded header and omitted only for an unseeded header.
126
- */
127
- abstract create(meta: SessionHeader, inheritedEventCount?: SessionLogOffset): Promise<void>;
128
- /**
129
- * Ensure a live session has a durable header even when it has no events.
130
- * Ordinary sessions remain lazily materialized; lifecycle frontends call
131
- * this only when an empty session itself is a durable resumable resource.
132
- * @param _session - exact live session whose registered header is materialized.
133
- */
134
- ensureMaterialized(_session: Session): Promise<void>;
135
- /**
136
- * Durably persist a batch of events. Honors the append-only and contiguous-
137
- * seq contracts: the first event's `seq` MUST equal the stored next-seq
138
- * (after `load` has durably closed any interrupted turn). Rejects non-JSON-
139
- * serializable `event.data` with an error naming the offending event type.
140
- * A seeded session's first materializing batch must reach its complete
141
- * inherited prefix.
142
- * @param id - the session the batch belongs to.
143
- * @param events - the contiguous batch to persist, in seq order.
144
- */
145
- abstract append(id: SessionId, events: readonly SessionEvent[]): Promise<void>;
146
- /**
147
- * Prepare the exact unpublished Session used by resume. Implementations may
148
- * reuse object graphs retained by an earlier {@link inspect} after confirming
149
- * their durable revision is still current; disposal releases an unpublished
150
- * reservation. Revision retries require the durable log to remain unchanged
151
- * for one read/check round trip; continuous external writers may delay completion.
152
- * @param id - persisted session to prepare.
153
- * @param signal - optional cancellation for preparation work.
154
- * @returns one owned unpublished Session preparation.
155
- */
156
- prepare(id: SessionId, signal?: AbortSignal): Promise<SessionPreparation>;
157
- /**
158
- * Load an immutable balanced logical view and commit any required cold
159
- * recovery. A complete interrupted final turn is preserved and durably
160
- * closed with missing tool errors plus any open step and turn boundaries;
161
- * only a torn final record is discarded. Unknown versions and corruption in
162
- * the committed prefix reject. Implementations MUST NOT crash-repair an
163
- * identity still bound to a live Session: a balanced live log may return as a
164
- * durable snapshot, while an open live turn rejects. Returned values may be
165
- * shared with immutable live or prepared state and must not be mutated.
166
- * Revision-based implementations may wait for one stable read/check round trip.
167
- * @param id - the persisted session to reload.
168
- * @returns the header and a log ending on a balanced `turn/end`.
169
- */
170
- abstract load(id: SessionId): Promise<SessionInspection>;
171
- /**
172
- * Inspect an immutable logical session without committing recovery or
173
- * publishing it. A cold complete interrupted turn receives synthetic closers
174
- * in memory and a torn physical tail remains untouched. An already-live
175
- * Session instead yields its current immutable snapshot, which may contain an
176
- * open turn and its `session/end-seed` boundary. Coordinator-backed
177
- * implementations retain the exact cold unpublished Session for bounded
178
- * reuse by a later {@link prepare}. A stale ready source is reloaded; a source
179
- * already committing or reserved for resume remains exclusive, and inspection
180
- * may borrow its immutable view. Callers borrow only the immutable header and
181
- * log. Continuous external writers may delay revision convergence.
182
- * @param id - the persisted session to inspect.
183
- * @param signal - optional cancellation for queued and backend read work.
184
- * @returns the validated header and current logical event log.
185
- */
186
- abstract inspect(id: SessionId, signal?: AbortSignal): Promise<SessionInspection>;
187
- /**
188
- * Borrow one exact inspection while retaining any reusable prepared source.
189
- * A cold observation must pin the exact prepared Session that a later
190
- * {@link prepare} reserves. Implementations must not degrade this operation
191
- * to a detached {@link inspect} result.
192
- * @param id - persisted session to observe.
193
- * @param signal - optional cancellation for preparation work.
194
- * @returns a disposable immutable observation.
110
+ * Open an existing stored session.
111
+ *
112
+ * `read` never takes ownership and works while another handle (or process)
113
+ * holds write ownership. `write` atomically claims single-writer ownership;
114
+ * an existing active owner rejects.
115
+ * @param id - the stored session to open.
116
+ * @param access - `read` or `write`.
117
+ * @param options - optional cancellation.
118
+ * @returns the open handle.
119
+ * @throws {SessionPersistenceNotFoundError} when the session does not exist.
120
+ * @throws {SessionAlreadyOwnedError} for `write` when ownership is taken.
195
121
  */
196
- abstract borrowSession(id: SessionId, signal?: AbortSignal): Promise<BorrowedSessionSource>;
122
+ abstract open(id: SessionId, access: SessionAccess, options?: SessionPersistenceOpenOptions): Promise<SessionHandle>;
197
123
  /**
198
- * Read the stored events from `fromSeq` onward — the read-from-seq
199
- * primitive for read models that resume from a watermark (e.g. a persisted
200
- * projection cache folding only the tail past its checkpoint). Unlike
201
- * {@link inspect}, it is a detached physical suffix read: no preparation
202
- * cache, torn-tail truncation, synthetic closers, or coordinator-state
203
- * publication. Only events from the valid contiguous stored prefix are
204
- * returned, so a torn fragment never reaches the caller. `fromSeq` at or
205
- * beyond the stored prefix returns an empty event list (never an error).
206
- * A backend whose medium can seek by seq may read only the suffix;
207
- * sequential media such as JSONL still parse the whole artifact and skip
208
- * forward. The primitive bounds what is returned and refolded, not every
209
- * backend's physical read.
210
- * @param id - the persisted session to read.
211
- * @param fromSeq - first event offset to include.
212
- * @param signal - optional cancellation for queued and backend read work.
213
- * @returns storage metadata, the requested offset, and stored events with `seq >= fromSeq`.
124
+ * Flush every active write handle owned by this service instance in one
125
+ * durability barrier: each handle's routed live events drain durably and
126
+ * its session materializes, exactly as that handle's own
127
+ * `SessionHandle.flush` would. Read handles buffer nothing and are
128
+ * untouched. A handle closed concurrently counts as flushed — close itself
129
+ * drains durably.
130
+ * @returns resolution once every write handle active at the call has flushed.
131
+ * @throws {AggregateError} naming each session whose flush failed; the
132
+ * remaining handles still flush.
214
133
  */
215
- abstract readFrom(id: SessionId, fromSeq: SessionLogOffset, signal?: AbortSignal): Promise<SessionEventSuffix>;
134
+ abstract flush(): Promise<void>;
216
135
  /**
217
- * Lightweight listing from metadata, without a full-log parse.
218
- * @param signal - optional cancellation for backend listing work.
219
- * @returns one header per materialized session.
136
+ * Observe one stored session without reading its event log or taking
137
+ * ownership.
138
+ *
139
+ * The snapshot's `revision` is an opaque change token comparable only
140
+ * against revisions from the same service instance and session id: equal
141
+ * revisions may be treated as an unchanged log; unequal revisions promise
142
+ * nothing. Write-ownership churn does not change a revision. It exists for
143
+ * derived read-model caches keyed off `stat`/`list`; it plays no part in
144
+ * open, read, or resume.
145
+ * @param id - the stored session to observe.
146
+ * @param options - optional cancellation.
147
+ * @returns the snapshot, or `undefined` when the session does not exist.
220
148
  */
221
- abstract list(signal?: AbortSignal): Promise<SessionHeader[]>;
149
+ abstract stat(id: SessionId, options?: SessionPersistenceStatOptions): Promise<SessionPersistenceSnapshot | undefined>;
222
150
  /**
223
- * List materialized sessions with cheap per-log change tokens.
224
- *
225
- * Repeated observations of an unchanged log return the same revision. A
226
- * successful mutating {@link load} repair changes the next listed revision.
227
- * Revisions also distinguish independently backed stores so backend-local
228
- * counters cannot compare equal across different persistence sources.
229
- * @param signal - optional cancellation for backend snapshot-listing work.
230
- * @returns one header and opaque revision per materialized session without loading full logs.
151
+ * List every stored session visible to this process, in no promised order.
152
+ * @param options - optional cancellation.
153
+ * @returns one snapshot per stored session.
231
154
  */
232
- abstract listSnapshots(signal?: AbortSignal): Promise<SessionPersistenceSnapshot[]>;
155
+ abstract list(options?: SessionPersistenceListOptions): Promise<readonly SessionPersistenceSnapshot[]>;
233
156
  }
234
157
  export default SessionPersistence;
235
158
  //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,62 @@
1
+ /**
2
+ * Backend-shared storage validation: the version gate, the fail-closed event
3
+ * vocabulary, append-batch materialization, and contiguity — one place so
4
+ * every backend refuses the same inputs identically.
5
+ * @module @deepseek-ai/dsh-session-persistence/storage-contract
6
+ */
7
+ import type { SessionEvent, SessionHeader, SessionId } from '@deepseek-ai/dsh-session';
8
+ import { type SessionLocation } from './errors.ts';
9
+ /**
10
+ * Refuse stored metadata that is not bound to the requested session id.
11
+ * @param id - the requested session id.
12
+ * @param meta - the stored header.
13
+ */
14
+ export declare function assertStoredId(id: SessionId, meta: SessionHeader): void;
15
+ /**
16
+ * Refuse a stored header whose format version this build does not read.
17
+ * @param meta - the stored header.
18
+ * @param location - the backend's artifact location for the refusal, when one exists.
19
+ */
20
+ export declare function assertVersion(meta: {
21
+ readonly id: SessionId;
22
+ readonly version: number;
23
+ }, location?: SessionLocation): void;
24
+ /**
25
+ * Validate one exclusively owned stored event array in place: adopt each
26
+ * record (validating and freezing it) and refuse any event type this build
27
+ * does not know, unless its writer marked it `ignorable: true` — silently
28
+ * skipping an unknown required event could reconstruct a wrong session (the
29
+ * envelope contract on `SessionEvent.ignorable`). Both newer vocabularies and
30
+ * retired pre-release shapes refuse here; this build ships no migration.
31
+ * @param meta - the stored header the events belong to.
32
+ * @param events - exclusively owned decoded events; validated in place.
33
+ * @param location - the backend's artifact location for refusals, when one exists.
34
+ * @returns the same array, validated and frozen.
35
+ * @throws {SessionFormatUnsupportedError} for unknown event types.
36
+ * @throws {SessionPersistenceCorruptionError} for records that fail validation.
37
+ */
38
+ export declare function validateStoredEvents(meta: SessionHeader, events: SessionEvent[], location?: SessionLocation): SessionEvent[];
39
+ /**
40
+ * Validate and deep-snapshot a header passed to `create` in one traversal.
41
+ * @param header - the caller's header.
42
+ * @returns the detached lossless-JSON header.
43
+ * @throws {TypeError} for non-JSON metadata or an invalid `createdAt`.
44
+ */
45
+ export declare function materializeCreateHeader(header: SessionHeader): SessionHeader;
46
+ /**
47
+ * Validate and deep-snapshot one append batch in a single traversal, so the
48
+ * checked value is exactly the value persisted (a check followed by a copy
49
+ * could reread accessors into a different record).
50
+ * @param events - the caller's batch.
51
+ * @returns the detached lossless-JSON batch.
52
+ * @throws {TypeError} when any event data is not losslessly JSON-serializable.
53
+ */
54
+ export declare function materializeAppendBatch(events: readonly SessionEvent[]): readonly SessionEvent[];
55
+ /**
56
+ * Refuse a batch that does not contiguously continue the stored log.
57
+ * @param id - the session the batch belongs to.
58
+ * @param events - the batch, in seq order.
59
+ * @param cursor - the stored next-seq.
60
+ */
61
+ export declare function assertContiguous(id: SessionId, events: readonly SessionEvent[], cursor: number): void;
62
+ //# sourceMappingURL=storage-contract.d.ts.map