@deepseek-ai/dsh-session-persistence-jsonl 0.1.2-alpha.5 → 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.
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Cross-process write-ownership lock for one session's artifact directory,
3
+ * held for the whole life of a write handle. The arbiter is the kernel:
4
+ * POSIX takes a non-blocking `flock(2)` (through fs-ext) on `session.lock`
5
+ * beside the log, and Windows holds a named kernel semaphore derived from
6
+ * that path — never a file lock or handle, so readers, searches, and
7
+ * directory removal proceed freely while the lock is held. Contention maps
8
+ * to `SessionAlreadyOwnedError`; the kernel releases the lock when the
9
+ * holder's descriptor or last object handle closes, including on any process
10
+ * death, so a crashed holder never blocks a successor. A live but wedged
11
+ * holder keeps the lock until its process exits: there is deliberately no
12
+ * expiry that could expropriate a stalled writer whose resumed appends would
13
+ * tear the log.
14
+ * A POSIX lock names an inode, not a path, so after locking the holder
15
+ * verifies the locked inode is still the file at the lock path and retries
16
+ * otherwise: an unlinked-and-recreated lock file carries a fresh inode, and
17
+ * a lock on the orphaned one proves nothing. Removing a live session's lock
18
+ * file therefore forfeits exclusion on POSIX (nothing in the harness does
19
+ * so); Windows has no lock file at all. Readers never touch the lock.
20
+ * The lock is acquired at write-open of an existing artifact and, for a
21
+ * created session, only right before its first materializing write — an
22
+ * unmaterialized session has no filesystem footprint. Release never removes
23
+ * the POSIX lock file: every acquired lock belongs to a materialized or
24
+ * materializing session, and the surviving file keeps the stable inode later
25
+ * lockers verify against. The browser worker deployment stubs fs-ext to
26
+ * immediate success: it is single-process, so the in-process write claim
27
+ * already excludes every writer.
28
+ * @module @deepseek-ai/dsh-session-persistence-jsonl/lease
29
+ */
30
+ import type { SessionId } from '@deepseek-ai/dsh-session';
31
+ /** Base name of the kernel lock file inside a session's directory. */
32
+ export declare const LEASE_FILENAME = "session.lock";
33
+ /**
34
+ * One held write lock. Constructed only by {@link SessionWriteLease.acquire};
35
+ * `release` closes the descriptor or handle, which is what releases the lock.
36
+ */
37
+ export declare class SessionWriteLease {
38
+ private readonly held;
39
+ private released;
40
+ private constructor();
41
+ /**
42
+ * Acquire the session directory's kernel write lock.
43
+ * @param dir - the session's artifact directory (created if absent).
44
+ * @param id - the session the lock guards, for error identities.
45
+ * @returns the held lock.
46
+ * @throws {SessionAlreadyOwnedError} while another holder keeps the lock.
47
+ */
48
+ static acquire(dir: string, id: SessionId): Promise<SessionWriteLease>;
49
+ /**
50
+ * Release the kernel lock by closing its descriptor or handle. The POSIX
51
+ * lock file is never removed: every acquired lock belongs to a
52
+ * materialized or materializing session, and keeping the file preserves
53
+ * the stable inode later lockers verify against. Idempotent.
54
+ */
55
+ release(): Promise<void>;
56
+ }
57
+ //# sourceMappingURL=lease.d.ts.map
@@ -0,0 +1,15 @@
1
+ /** Isolated verification for a staged or competing current JSONL generation. */
2
+ import type { JsonlCompression } from './format.ts';
3
+ import type { JsonlExpectedPrefix, JsonlVerifiedGeneration } from './generation.ts';
4
+ /**
5
+ * Verify one current generation in a fresh Worker Thread.
6
+ * @param path - staged or competing current-generation path.
7
+ * @param compression - configured physical encoding.
8
+ * @param expectedId - Session id expected in the decoded header.
9
+ * @param expectedEventCount - exact logical event count expected after decoding.
10
+ * @param expectedPrefix - verified physical prefix; an append tail may be present and is not validated.
11
+ * @param signal - optional cancellation for scheduler wait and Worker execution.
12
+ * @returns stable physical identity and digest observed by the worker.
13
+ */
14
+ export declare function verifyCurrentGenerationInWorker(path: string, compression: JsonlCompression, expectedId: string, expectedEventCount: number, expectedPrefix?: JsonlExpectedPrefix, signal?: AbortSignal): Promise<JsonlVerifiedGeneration>;
15
+ //# sourceMappingURL=migration-verifier.d.ts.map
@@ -0,0 +1,249 @@
1
+ /**
2
+ * The JSONL provider's session storage runtime: its concrete write/read
3
+ * handle with a per-handle mutation chain and a routed live write-behind
4
+ * buffer, the in-process bookkeeping that enforces one active writer per
5
+ * session id, and the backend's live event routing and teardown. Deliberately
6
+ * provider-local: the persistence seam exposes only the service and handle
7
+ * contracts, and the shared contract suites pin equivalent observable
8
+ * behavior across providers.
9
+ * @module
10
+ */
11
+ import type { Context } from '@deepseek-ai/cordis';
12
+ import type { SessionEvent, SessionHeader, SessionId, SessionLogOffset } from '@deepseek-ai/dsh-session';
13
+ import { SessionPersistenceRevision } from '@deepseek-ai/dsh-session-persistence';
14
+ import type { SessionAccess, SessionHandle, SessionHandleAppendOptions, SessionHandleFlushOptions, SessionHandleReadOptions, SessionHandleReadResult } from '@deepseek-ai/dsh-session-persistence';
15
+ import type { SessionWriteLease } from './lease.ts';
16
+ /** Maximum intentional wait before a routed live session batch starts writing. */
17
+ export declare const LIVE_WRITE_BATCH_MAX_DELAY_MS = 200;
18
+ /** The file-storage primitives the handle drives on its owning service. */
19
+ export interface JsonlHandleStorage {
20
+ /** Append encoded lines; `isMaterialized` selects create-vs-extend publication. */
21
+ persistBatch(header: SessionHeader, events: readonly SessionEvent[], isMaterialized: boolean, inheritedEventCount: SessionLogOffset): Promise<void>;
22
+ /** Materialize the header-only artifact for an explicitly flushed empty session. */
23
+ persistHeader(header: SessionHeader, inheritedEventCount: SessionLogOffset): Promise<void>;
24
+ /** Truncate a torn physical tail before the first new append lands. */
25
+ truncateTornTail(header: SessionHeader, truncateTo: number): Promise<void>;
26
+ /** Resolve the current-generation artifact path, or `undefined` when absent. */
27
+ resolveCurrentLog(id: SessionId, signal?: AbortSignal): Promise<string | undefined>;
28
+ /** Read and validate the stored log at `path`, including its established event aliasing state. */
29
+ readStoredLog(path: string, expectedId: SessionId, signal?: AbortSignal): Promise<SessionHandleReadResult>;
30
+ /** Whether the id is still a created-but-unmaterialized session here. */
31
+ hasPendingSession(id: SessionId): boolean;
32
+ /** Acquire the session's cross-process write lock in its artifact directory. */
33
+ acquireWriteLease(header: SessionHeader): Promise<SessionWriteLease>;
34
+ /** Drop the handle's bookkeeping on close. */
35
+ releaseHandle(handle: JsonlSessionHandle, materialized: boolean): void;
36
+ }
37
+ /** Mutable per-handle log state; a write handle is its session's single mutator. */
38
+ export interface StorageHandleState {
39
+ /** The stored next-seq (the logical end this handle knows). */
40
+ cursor: number;
41
+ /** Whether the session has a durable artifact yet. */
42
+ materialized: boolean;
43
+ /** Torn-tail truncation point, consumed by the first new append. */
44
+ tornTruncateTo?: number | undefined;
45
+ /** Complete events recovered from the torn final frame; the first mutation rewrites them durably. */
46
+ recoveredTail?: SessionEvent[] | undefined;
47
+ /** Exact fork-inherited prefix length stored with the log; `0` when unseeded. */
48
+ inheritedEventCount: SessionLogOffset;
49
+ /** The validated stored prefix from a write open, served to reads until the first append. */
50
+ primed?: SessionHandleReadResult | undefined;
51
+ }
52
+ /**
53
+ * The JSONL session handle. Mutations serialize on a per-handle promise
54
+ * chain; reads re-scan the artifact on demand and never observe a shorter log
55
+ * than a prior read on this handle. Routed live events buffer in a bounded
56
+ * window and drain through the same chain as explicit appends.
57
+ */
58
+ export declare class JsonlSessionHandle implements SessionHandle {
59
+ private readonly storage;
60
+ readonly id: SessionId;
61
+ readonly header: SessionHeader;
62
+ readonly access: SessionAccess;
63
+ private readonly state;
64
+ /** The cross-process write lock; a create handle acquires it lazily at first materialization. */
65
+ private lease?;
66
+ private chain;
67
+ private closing;
68
+ private observedLength;
69
+ /** Routed live events awaiting their batching deadline (persistence-owned copies). */
70
+ private buffered;
71
+ private batchTimer;
72
+ /** Set when a drain failed; the automatic timer stays quiet until the next drain. */
73
+ private drainPaused;
74
+ private draining;
75
+ constructor(storage: JsonlHandleStorage, id: SessionId, header: SessionHeader, access: SessionAccess, state: StorageHandleState,
76
+ /** The cross-process write lock; a create handle acquires it lazily at first materialization. */
77
+ lease?: SessionWriteLease | undefined);
78
+ /** Exact fork-inherited prefix length stored with this session's log. */
79
+ get inheritedEventCount(): SessionLogOffset;
80
+ /**
81
+ * Read a slice of the valid contiguous logical log; see the seam contract.
82
+ * @param offset - first logical seq to include (default 0).
83
+ * @param length - maximum events returned (default: the rest).
84
+ * @param options - optional cancellation.
85
+ * @returns a slice carrying the aliasing state established by its producer.
86
+ */
87
+ read(offset?: number, length?: number, options?: SessionHandleReadOptions): Promise<SessionHandleReadResult>;
88
+ /** Read one slice from the prepared historical prefix retained by this handle. */
89
+ private readPrimed;
90
+ /** Read one current physical generation and enforce this handle's monotonic view. */
91
+ private readCurrent;
92
+ /**
93
+ * Durably append a contiguous batch; see the seam contract.
94
+ * @param events - the contiguous batch in seq order.
95
+ * @param options - optional cancellation observed before the write starts.
96
+ */
97
+ append(events: readonly SessionEvent[], options?: SessionHandleAppendOptions): Promise<void>;
98
+ /**
99
+ * Durability barrier; materializes the artifact when nothing has been
100
+ * appended yet, so an explicitly flushed empty session survives this process.
101
+ * @param options - optional cancellation observed before the barrier starts.
102
+ */
103
+ flush(options?: SessionHandleFlushOptions): Promise<void>;
104
+ /**
105
+ * Release the handle; see the seam contract. Idempotent and uncancellable.
106
+ * A write handle first drains its routed live buffer through the still-open
107
+ * storage, so backend teardown loses nothing regardless of which fiber
108
+ * unwinds first; a drain or lock-release failure still frees the in-process
109
+ * claim, then rejects — both failures together reject as one
110
+ * `AggregateError`.
111
+ * @returns settlement of the release.
112
+ */
113
+ close(): Promise<void>;
114
+ /** `await using` support: delegates to {@link close}. */
115
+ [Symbol.asyncDispose](): Promise<void>;
116
+ /**
117
+ * Buffer one published live session event and arm the bounded batching
118
+ * window when it is idle. The routing installer is the only caller.
119
+ * @param event - the live event, retained as a persistence-owned copy.
120
+ * @param reportBackgroundFailure - observes a deadline-driven drain failure
121
+ * (the events stay buffered; the next {@link drainLive} retries loudly).
122
+ */
123
+ enqueueLive(event: SessionEvent, reportBackgroundFailure: (error: unknown) => void): void;
124
+ /**
125
+ * Durably drain the routed live buffer through the mutation chain;
126
+ * concurrent callers join one drain, and a failure retains the batch in
127
+ * order so `session/flush` can retry and reject loudly.
128
+ */
129
+ drainLive(): Promise<void>;
130
+ private drainBuffered;
131
+ /** The shared durable-append body: contiguity, ownership, torn-tail repair, storage write, state advance. */
132
+ private persistContiguous;
133
+ /**
134
+ * Hold the cross-process write lock before this session's first durable
135
+ * write. An open write handle holds it from construction; a create handle
136
+ * acquires it here — immediately before the first log bytes publish — and
137
+ * keeps it through close even when materialization then fails, so a
138
+ * materializing session stays exclusively owned across retries.
139
+ */
140
+ private ensureLease;
141
+ /** Serialize one operation onto the chain without the closed-handle refusal (drain-from-close). */
142
+ private enqueueChain;
143
+ /** Serialize one public mutating operation onto this handle's chain. */
144
+ private run;
145
+ private assertOpen;
146
+ }
147
+ /** One created-but-unmaterialized session tracked in this process only. */
148
+ export interface PendingSession {
149
+ readonly header: SessionHeader;
150
+ readonly revision: SessionPersistenceRevision;
151
+ /** Exact fork-inherited prefix length supplied at create. */
152
+ readonly inheritedEventCount: SessionLogOffset;
153
+ }
154
+ /**
155
+ * The JSONL backend's in-process bookkeeping: the single active writer per
156
+ * session id (doubling as the live event router), the open-handle set the
157
+ * teardown sweep closes, and the created-but-unmaterialized sessions this
158
+ * process can already observe.
159
+ */
160
+ export declare class JsonlBackendTracker {
161
+ private readonly name;
162
+ /** Every open handle; teardown closes what remains. */
163
+ readonly openHandles: Set<SessionHandle>;
164
+ /** `null` marks a claim whose handle is still being constructed. */
165
+ private readonly writers;
166
+ private readonly pending;
167
+ private counter;
168
+ /** @param name - backend label used in in-memory revision tokens and teardown errors. */
169
+ constructor(name: string);
170
+ /**
171
+ * Claim write ownership and record the created session as pending, making
172
+ * it observable to this process before it materializes. Before
173
+ * materialization this registration is the only guard — session ids do not
174
+ * collide across processes, and no durable artifact exists for another
175
+ * process to open; the handle takes the cross-process lock at its first
176
+ * materializing write.
177
+ * @param header - the validated detached header.
178
+ * @param inheritedEventCount - the exact fork-inherited prefix length.
179
+ * @throws {SessionAlreadyExistsError} when a concurrent create or an open
180
+ * write handle holds the id — for create, the duplicate is the fact.
181
+ */
182
+ registerCreated(header: SessionHeader, inheritedEventCount: SessionLogOffset): void;
183
+ /**
184
+ * Claim write ownership for an existing session.
185
+ * @param id - the session to claim.
186
+ * @throws {SessionAlreadyOwnedError} when an active write handle exists.
187
+ */
188
+ claimWrite(id: SessionId): void;
189
+ /**
190
+ * Roll a failed write open back.
191
+ * @param id - the session whose claim is dropped.
192
+ */
193
+ releaseClaim(id: SessionId): void;
194
+ /**
195
+ * The pending entry for a created-but-unmaterialized session, if any.
196
+ * @param id - the session to look up.
197
+ * @returns the pending header and in-memory revision.
198
+ */
199
+ pendingOf(id: SessionId): PendingSession | undefined;
200
+ /**
201
+ * Whether this process still tracks a created-but-unmaterialized session.
202
+ * @param id - the session to test.
203
+ * @returns true while the pending entry exists.
204
+ */
205
+ hasPending(id: SessionId): boolean;
206
+ /**
207
+ * Iterate the pending sessions for listing.
208
+ * @returns the pending entries, keyed by session id.
209
+ */
210
+ pendingEntries(): IterableIterator<[SessionId, PendingSession]>;
211
+ /**
212
+ * Drop a pending entry once the session materialized durably.
213
+ * @param id - the session that reached durable storage.
214
+ */
215
+ materialized(id: SessionId): void;
216
+ /**
217
+ * Track one open handle for teardown and, for a write handle, bind it as
218
+ * the session's live event route.
219
+ * @param handle - the just-constructed handle.
220
+ * @returns the same handle, for construction-site chaining.
221
+ */
222
+ adopt(handle: JsonlSessionHandle): JsonlSessionHandle;
223
+ /**
224
+ * Release one handle's bookkeeping on close. A write handle drops its
225
+ * ownership claim; a creator that never materialized leaves nothing behind —
226
+ * the session never existed.
227
+ * @param handle - the closing handle.
228
+ * @param materialized - whether the session reached durable storage.
229
+ */
230
+ release(handle: JsonlSessionHandle, materialized: boolean): void;
231
+ /**
232
+ * Drain and flush every active write handle — the service-wide durability
233
+ * barrier behind `SessionPersistence.flush`.
234
+ * @throws {AggregateError} naming each session whose flush failed; the
235
+ * remaining handles still flush.
236
+ */
237
+ flushAll(): Promise<void>;
238
+ /**
239
+ * Install the backend's live session routing and teardown. Persistence
240
+ * enforces one active write handle per id, so the listeners route published
241
+ * sessions' events by id; the teardown effect closes every open handle —
242
+ * close drains the routed buffer — and aggregates failures. This provider
243
+ * owns no separate storage connection, so closing handles is the complete
244
+ * teardown. Registrations are effects of the current fiber.
245
+ * @param ctx - the backend's context.
246
+ */
247
+ install(ctx: Context): void;
248
+ }
249
+ //# sourceMappingURL=storage.d.ts.map
@@ -0,0 +1,8 @@
1
+ import { type JsonlGenerationRuntime, type JsonlGenerationRuntimeOverrides } from '../generation.ts';
2
+ /**
3
+ * Create generation operations with deterministic I/O and race seams for tests.
4
+ * @param overrides - deterministic filesystem, platform, and race dependencies.
5
+ * @returns bound generation operations.
6
+ */
7
+ export declare function createJsonlGenerationTestRuntime(overrides?: JsonlGenerationRuntimeOverrides): JsonlGenerationRuntime;
8
+ //# sourceMappingURL=generation.d.ts.map
@@ -18,6 +18,24 @@
18
18
  * @param replacement - the final path, which must not already exist.
19
19
  */
20
20
  export declare function publishNewFileWin32(existing: string, replacement: string): Promise<void>;
21
+ /**
22
+ * Acquire the session write lock as a named kernel semaphore (count 1) whose
23
+ * name is derived from the canonical lock path. A kernel object never touches
24
+ * the filesystem, so readers, searches, and directory removal proceed freely
25
+ * while the lock is held; a second acquirer's zero-timeout wait times out
26
+ * (`EBUSY`); and when the last handle closes — including on any process
27
+ * death — the object is destroyed, so a successor's create starts fresh.
28
+ * @param path - the lock file path the name is derived from (case-folded:
29
+ * Windows paths are case-insensitive).
30
+ * @returns the open semaphore handle, released via {@link releaseLockHandleWin32}.
31
+ */
32
+ export declare function acquireLockHandleWin32(path: string): Promise<number>;
33
+ /**
34
+ * Release a lock from {@link acquireLockHandleWin32}: restore the semaphore
35
+ * count and close the handle (the object dies with its last handle).
36
+ * @param handle - the open semaphore handle.
37
+ */
38
+ export declare function releaseLockHandleWin32(handle: number): Promise<void>;
21
39
  /**
22
40
  * Create `target` and its missing ancestors with durable Windows namespace
23
41
  * publication. Each missing directory is first created as a random staging
@@ -0,0 +1,3 @@
1
+ /** Worker entry for current-generation physical and logical verification. */
2
+ export {};
3
+ //# sourceMappingURL=worker.d.ts.map