@morlay/session-rdb 0.0.10 → 0.0.12

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.
package/lib/index.mjs DELETED
@@ -1,2175 +0,0 @@
1
- import "@deepseek-ai/cordis";
2
- import z from "@deepseek-ai/schemastery";
3
- import { settingsNamespace } from "@deepseek-ai/dsh-settings";
4
- import { randomUUID } from "node:crypto";
5
- import { Pool } from "pg";
6
- import { drizzle } from "drizzle-orm/node-postgres";
7
- import { PersistenceCoordinator, SessionPersistence, SessionPersistenceRevision } from "@deepseek-ai/dsh-session-persistence";
8
- import { and, desc, eq, gte, inArray, sql } from "drizzle-orm";
9
- import { check, index, integer, sqliteTable, text, unique } from "drizzle-orm/sqlite-core";
10
- import { bigint, check as check$1, index as index$1, integer as integer$1, pgTable, serial, text as text$1, unique as unique$1 } from "drizzle-orm/pg-core";
11
- import { statSync } from "node:fs";
12
- import { mkdir, open } from "node:fs/promises";
13
- import { dirname, resolve } from "node:path";
14
- import { DatabaseSync } from "node:sqlite";
15
- import { drizzle as drizzle$1 } from "drizzle-orm/node-sqlite";
16
- import { SESSION_FORMAT_VERSION } from "@deepseek-ai/dsh-session";
17
- import { SessionBranch, SessionBranchError, buildTimeline } from "@morlay/session-branch";
18
- //#region src/log.ts
19
- /**
20
- * Reconstruct the {@link SessionHeader} from a `t_sessions` row.
21
- * @param row - the `t_sessions` table row.
22
- * @returns the header, `NULL` columns mapped to omitted optional fields.
23
- */
24
- function rowToMeta(row) {
25
- if (!Number.isSafeInteger(row.fCreatedAt) || row.fCreatedAt < 0) throw new Error("stored session createdAt must be a non-negative safe integer");
26
- return {
27
- version: row.fVersion,
28
- id: row.fSessionId,
29
- createdAt: row.fCreatedAt,
30
- ...row.fCwd !== null ? { cwd: row.fCwd } : {},
31
- ...row.fParentSession !== null ? { parentSession: row.fParentSession } : {},
32
- ...row.fSeedLength !== null ? { seedLength: row.fSeedLength } : {},
33
- ...row.fOrigin !== null ? { origin: row.fOrigin } : {},
34
- ...row.fDelegationDepth === null ? {} : { delegationDepth: row.fDelegationDepth }
35
- };
36
- }
37
- /**
38
- * `t_sessions` 的 INSERT 列值:`SessionHeader` 的持久化字段 + 初始 head 游标
39
- * (空事件 id、seq -1)+ materialization identity(`f_incarnation`)+ revision 0。
40
- * 方言无关的纯映射——SQLite 与 PostgreSQL 后端的 `upsertSession` 共用,列名
41
- * 与 `src/entities/sessions.ts` 对齐(改一处即两方言生效)。`f_id` serial 由
42
- * 数据库生成,不在映射内。
43
- */
44
- function sessionInsertRow(meta, incarnation) {
45
- return {
46
- fSessionId: meta.id,
47
- fHeadEventId: "",
48
- fHeadSequence: -1,
49
- fVersion: meta.version,
50
- fCreatedAt: meta.createdAt,
51
- fCwd: meta.cwd ?? null,
52
- fParentSession: meta.parentSession ?? null,
53
- fSeedLength: meta.seedLength ?? null,
54
- fOrigin: meta.origin ?? null,
55
- fDelegationDepth: meta.delegationDepth ?? null,
56
- fIncarnation: incarnation,
57
- fRevision: 0
58
- };
59
- }
60
- /**
61
- * `t_sessions` 的 ON CONFLICT 更新列值:只刷新 header 列,保留 head 游标
62
- * (`f_head_event_id`/`f_head_sequence`)与 materialization identity
63
- * (`f_incarnation`/`f_revision`)。方言无关,两后端共用(见
64
- * {@link sessionInsertRow})。
65
- */
66
- function sessionConflictRow(meta) {
67
- return {
68
- fVersion: meta.version,
69
- fCreatedAt: meta.createdAt,
70
- fCwd: meta.cwd ?? null,
71
- fParentSession: meta.parentSession ?? null,
72
- fSeedLength: meta.seedLength ?? null,
73
- fOrigin: meta.origin ?? null,
74
- fDelegationDepth: meta.delegationDepth ?? null
75
- };
76
- }
77
- /**
78
- * Remap a stored {@link SurfaceOp} from upstream seqs to persisted seqs. An
79
- * `append` op carries no seqs; a positional `replace`'s `start`/`end` name
80
- * surface nodes by UPSTREAM seq and must follow {@link SessionEvent.sourceEventSeqs}
81
- * through the same upstream→persisted map when delta filtering re-numbered the
82
- * log — otherwise the replacement range is looked up against DENSE seqs and the
83
- * surface fold rejects the log ("start seq N not found in surface").
84
- * @param op - the stored surface op.
85
- * @param remap - upstream→persisted seq mapping (identity when absent).
86
- * @returns the remapped surface op.
87
- */
88
- function remapSurfaceOp(op, remap) {
89
- if (op === "append") return op;
90
- return {
91
- op: "replace",
92
- start: remap(op.start),
93
- end: remap(op.end)
94
- };
95
- }
96
- /**
97
- * The compact metering events (`compaction/summary`, `compaction/prune`) carry the
98
- * token-meter's shadow-price claim in `data.shadowedRange`: the inclusive
99
- * surface-node seqs of the range the IMMEDIATELY following surface `replace`
100
- * shadows. The range names surface nodes by UPSTREAM seq, so it must follow
101
- * the replace's `surfaceOp` through the same upstream→persisted map — the
102
- * fold compares claim and replacement ranges for exact equality, and an
103
- * un-remapped claim (upstream) next to a remapped replacement range (dense)
104
- * makes replay fail loud ("token surface: replace ... has no adjacent shadow
105
- * price").
106
- * @param range - the stored shadowed range (upstream seqs).
107
- * @param remap - upstream→persisted seq mapping (identity when absent).
108
- * @returns the remapped shadowed range.
109
- */
110
- function remapShadowedRange(range, remap) {
111
- return {
112
- start: remap(range.start),
113
- end: remap(range.end)
114
- };
115
- }
116
- /**
117
- * Reconstruct a {@link SessionEvent} from a joined row. The emitted event
118
- * carries the DENSE persisted seq (`row.fSequence`); `sourceEventSeqs` entries,
119
- * a positional `replace` {@link SurfaceOp}'s range, and the compact metering
120
- * events' `shadowedRange` are resolved onto persisted seqs through `seqMap`
121
- * when the log was delta-filtered.
122
- *
123
- * Coordinate resolution is "the stored f_sequence wins, when it is a valid
124
- * earlier reference": after resume the upstream live log IS the dense log,
125
- * so events written since (checkpoint replaces, provenance) cite DENSE seqs.
126
- * A cited seq that exists in the dense space, precedes this event, and is
127
- * either a SURFACE node or has no upstream counterpart is kept as-is. A seq
128
- * outside the dense space (or a dense seq that is a non-surface event with an
129
- * upstream counterpart — an upstream-cited seq that happens to collide with a
130
- * later dense seq) is treated as an UPSTREAM reference and remapped through
131
- * the upstream→persisted map, again only when the result precedes this event.
132
- * Anything else is unresolvable and dropped (never kept verbatim — after
133
- * rewind re-uses the upstream space, keeping an upstream value would mix
134
- * coordinates and fail the Session seed validation).
135
- * @param row - the joined `t_session_events` + `t_events` row.
136
- * @param seqMap - upstream→persisted seq map (optional).
137
- * @param denseTypeMap - every persisted f_sequence → event type for the
138
- * session (optional; always passed together with `seqMap`).
139
- * @returns the reconstructed event; throws when a JSON column fails to parse
140
- * ({@link scanRows} treats that as a hole, not corruption, in the tail).
141
- */
142
- function rowToEvent(row, seqMap, denseTypeMap) {
143
- const remap = (seq) => resolveProvenanceSeq(seq, row.fSequence, seqMap, denseTypeMap) ?? seq;
144
- const strictRemap = (seq) => resolveProvenanceSeq(seq, row.fSequence, seqMap, denseTypeMap);
145
- const surfaceFields = {
146
- ...row.fSourceEventSeqs !== null ? (() => {
147
- const refs = remapProvenance(JSON.parse(row.fSourceEventSeqs), strictRemap);
148
- return refs.length > 0 ? { sourceEventSeqs: refs } : {};
149
- })() : {},
150
- ...row.fSurfaceOp !== null ? { surfaceOp: remapSurfaceOp(JSON.parse(row.fSurfaceOp), remap) } : {}
151
- };
152
- const data = JSON.parse(row.fData);
153
- if (row.fKind === "compaction/summary" || row.fKind === "compaction/prune") {
154
- const metering = data;
155
- if (metering.shadowedRange !== void 0) metering.shadowedRange = remapShadowedRange(metering.shadowedRange, remap);
156
- }
157
- return {
158
- type: row.fKind,
159
- seq: row.fSequence,
160
- time: row.fCreatedAt,
161
- data,
162
- ...surfaceFields
163
- };
164
- }
165
- /**
166
- * Resolve one cited seq onto the DENSE persisted seq space.
167
- *
168
- * "The stored f_sequence wins, when it is a valid earlier reference": after
169
- * resume the upstream live log IS the dense log, so events written since
170
- * (checkpoint replaces, provenance) cite DENSE seqs. A cited seq that exists
171
- * in the dense space, precedes `eventSeq`, and is either a SURFACE node or has
172
- * no upstream counterpart is kept as-is. A seq outside the dense space (or a
173
- * dense seq that is a non-surface event with an upstream counterpart — an
174
- * upstream-cited seq that happens to collide with a later dense seq) is
175
- * treated as an UPSTREAM reference and remapped through the upstream→persisted
176
- * map, again only when the result precedes `eventSeq`. Anything else is
177
- * unresolvable (`undefined`): provenance entries are dropped rather than kept
178
- * verbatim, because after rewind re-uses the upstream space an upstream value
179
- * would mix coordinates and fail the Session seed validation.
180
- * @param seq - the cited seq (upstream or dense).
181
- * @param eventSeq - the citing event's dense seq.
182
- * @param seqMap - upstream→persisted seq map (optional).
183
- * @param denseTypeMap - every persisted f_sequence → event type (optional;
184
- * always passed together with `seqMap`).
185
- * @returns the dense seq to cite, or undefined when unresolvable.
186
- */
187
- function resolveProvenanceSeq(seq, eventSeq, seqMap, denseTypeMap) {
188
- if (seqMap === void 0 && denseTypeMap === void 0) return seq;
189
- const kind = denseTypeMap?.get(seq);
190
- if (kind !== void 0 && seq < eventSeq && (SURFACE_EVENT_TYPES.has(kind) || !seqMap?.has(seq))) return seq;
191
- const dense = seqMap?.get(seq);
192
- if (dense !== void 0 && dense < eventSeq) return dense;
193
- }
194
- /**
195
- * Build the upstream→persisted seq map for one session's persisted events
196
- * (only meaningful when delta filtering re-numbered the log).
197
- *
198
- * A session re-opened by resume (or forked) persists the seed segment and the
199
- * new segment in ONE log: the seed rows keep the PARENT session's upstream seqs
200
- * while the resumed rows carry the child session's own upstream seqs, which
201
- * renumber from the seed boundary and therefore OVERLAP the parent space. The
202
- * FIRST mapping wins so a seed-segment event's provenance reference resolves to
203
- * the seed-space row it actually derived from (rows are ordered by persisted
204
- * seq, so the seed segment always precedes the resumed one); the resumed
205
- * segment's references are unique within their own space unless they point at a
206
- * shared value, which only the parent could have produced first.
207
- * @param rows - one session's seq rows (upstream + persisted), ordered by seq
208
- * ascending (only the two seq columns are needed).
209
- * @returns map from `f_original_seq` to `f_sequence` (first occurrence wins).
210
- */
211
- function buildSeqMap(rows) {
212
- const map = /* @__PURE__ */ new Map();
213
- for (const row of rows) if (!map.has(row.fOriginalSeq)) map.set(row.fOriginalSeq, row.fSequence);
214
- return map;
215
- }
216
- /**
217
- * Normalize a stored provenance list into the persisted seq space.
218
- *
219
- * `sourceEventSeqs` references events by UPSTREAM seq; on read each entry is
220
- * remapped to the DENSE persisted seq through the upstream→persisted map. An
221
- * entry whose event never got a persisted row (a dropped delta) or no longer
222
- * has one cannot be remapped. Keeping such an entry verbatim would mix
223
- * upstream and dense coordinates in one list — after rewind re-uses the
224
- * upstream seq space, a stale reference can even collide with a live dense
225
- * seq — which the upstream Session seed validation rejects (duplicates /
226
- * non-monotonic / "must reference earlier events"). Unmappable entries are
227
- * dropped. Because the upstream space is reused, the remapped entries can
228
- * also arrive out of order or duplicated; the result is sorted and
229
- * de-duplicated so it is strictly increasing, as the upstream provenance
230
- * contract requires. The output's SET (the only thing surface-replace
231
- * shadowing checks) is unchanged by normalization.
232
- * @param refs - the event's `sourceEventSeqs` (upstream seqs).
233
- * @param remap - upstream→persisted seq mapping; undefined for an entry
234
- * whose event has no persisted row.
235
- * @returns the normalized, strictly increasing persisted seqs (possibly empty).
236
- */
237
- function remapProvenance(refs, remap) {
238
- const mapped = [];
239
- for (const ref of refs) {
240
- const dense = remap(ref);
241
- if (dense !== void 0) mapped.push(dense);
242
- }
243
- mapped.sort((a, b) => a - b);
244
- const result = [];
245
- for (const seq of mapped) if (result.length === 0 || result[result.length - 1] !== seq) result.push(seq);
246
- return result;
247
- }
248
- /**
249
- * The event types that produce model-visible surface nodes (mirror of the
250
- * upstream `SURFACE_EVENT_TYPES`); only these can appear in a replace's
251
- * shadowed range, so only their seqs may be merged into provenance.
252
- */
253
- const SURFACE_EVENT_TYPES = /* @__PURE__ */ new Set([
254
- "user/message",
255
- "assistant/message",
256
- "tool/result"
257
- ]);
258
- /**
259
- * Make a surface `replace` event's provenance cover its replacement range.
260
- *
261
- * The upstream Session seed validation requires a replace's `sourceEventSeqs`
262
- * to include every surface node it shadows (`assertProvenance`'s shadowed
263
- * check). Rewind re-uses the upstream seq space: a replace that survives a
264
- * truncation (e.g. a checkpoint `user/message` before the new boundary) has
265
- * its range remapped onto DENSE seqs, while its provenance references were
266
- * written in upstream seqs — references to rows rewind deleted are dropped on
267
- * read, so the surviving provenance no longer covers the range's dense nodes.
268
- * Merging every persisted SURFACE node seq inside the (dense) range into the
269
- * provenance makes the log loadable again; extra references are legal
270
- * provenance and never affect the fold (the fold consumes only the surfaceOp
271
- * range). Only events the map could resolve exist in the dense range, so the
272
- * merged list is strictly increasing after sort/dedup.
273
- * @param events - a session's dense event list (contiguous seqs), read in
274
- * full; mutated in place.
275
- */
276
- function normalizeSurfaceReplaceProvenance(events) {
277
- for (const event of events) {
278
- const raw = event;
279
- const op = raw.surfaceOp;
280
- if (typeof op !== "object" || op === null || op.op !== "replace") continue;
281
- const { start, end } = op;
282
- const refs = new Set(raw.sourceEventSeqs ?? []);
283
- for (const candidate of events) if (candidate.seq >= start && candidate.seq <= end && SURFACE_EVENT_TYPES.has(candidate.type)) refs.add(candidate.seq);
284
- raw.sourceEventSeqs = [...refs].sort((a, b) => a - b);
285
- }
286
- }
287
- /**
288
- * Prune `sourceEventSeqs` references that cannot be remapped on read.
289
- *
290
- * `sourceEventSeqs` references events by UPSTREAM seq; on read the references
291
- * are remapped to the DENSE persisted seq through {@link buildSeqMap}. A
292
- * reference whose event never got a persisted row (a dropped delta, or a seq
293
- * that never existed) has no map entry and would replay as a `source >=
294
- * current seq` provenance violation, so the write path prunes it. A fully
295
- * pruned list is stored as null (no provenance) by the serializer.
296
- *
297
- * The `keep` predicate is the CALLER's view of resolvability: the write path
298
- * knows which upstream seqs THIS INSTANCE dropped (`WriteGuard.pruneRefs` —
299
- * per-instance knowledge, which must not prune references to rows another
300
- * instance persisted, e.g. a resume seed segment), while the one-shot repair
301
- * script knows which upstream seqs exist on DISK (full-database view). The
302
- * filter itself is shared so the rule lives in one place.
303
- * @param refs - the event's `sourceEventSeqs` (upstream seqs).
304
- * @param keep - true for a seq whose referenced event is resolvable.
305
- * @returns the pruned list.
306
- */
307
- function pruneSourceEventSeqs(refs, keep) {
308
- return refs.filter(keep);
309
- }
310
- /**
311
- * Find the preserved prefix of ordered event rows. Fully written rows in an
312
- * interrupted final turn remain in the prefix. The first unparsable row or seq
313
- * gap after the last `turn/end` marks a tolerated torn tail; the same hole in
314
- * the committed region rejects.
315
- *
316
- * @param rows - one session's event rows, ordered by persisted seq ascending.
317
- * @param base - the persisted seq the first row is expected to carry; `0` for
318
- * a whole log, the requested `fromSeq` for a suffix read (`loadStoredFrom`).
319
- * @param seqMap - upstream→persisted seq map forwarded to {@link rowToEvent}.
320
- * @param denseTypeMap - every persisted f_sequence → type, forwarded to
321
- * {@link rowToEvent} (kept together with `seqMap`).
322
- * @returns the preserved event prefix, plus `tornFrom` — the persisted seq the
323
- * physical delete starts at — when a torn tail exists.
324
- */
325
- function scanRows(rows, base = 0, seqMap, denseTypeMap) {
326
- const parsed = rows.map((row) => {
327
- try {
328
- return {
329
- ok: true,
330
- event: rowToEvent(row, seqMap, denseTypeMap)
331
- };
332
- } catch {
333
- return { ok: false };
334
- }
335
- });
336
- let lastTurnEnd = -1;
337
- for (let i = parsed.length - 1; i >= 0; i--) if (parsed[i]?.ok && rows[i]?.fKind === "turn/end") {
338
- lastTurnEnd = i;
339
- break;
340
- }
341
- const preserved = [];
342
- for (let i = 0; i < rows.length; i++) {
343
- const p = parsed[i];
344
- if (!p?.ok || p.event === void 0) {
345
- if (i <= lastTurnEnd) throw new Error(`corrupt session log: unparsable committed event at seq ${rows[i]?.fSequence}`);
346
- break;
347
- }
348
- if (p.event.seq !== base + i) {
349
- if (i <= lastTurnEnd) throw new Error(`corrupt session log: seq gap in committed region (expected ${base + i}, got ${p.event.seq})`);
350
- break;
351
- }
352
- preserved.push(p.event);
353
- }
354
- return preserved.length < rows.length ? {
355
- preserved,
356
- tornFrom: base + preserved.length
357
- } : { preserved };
358
- }
359
- //#endregion
360
- //#region src/write-guard.ts
361
- /**
362
- * The write-authority state machine for one backend instance. Not part of the
363
- * {@link Backend} seam: it guards the orchestration layer's own invariants and
364
- * lives entirely in memory.
365
- */
366
- var WriteGuard = class {
367
- /**
368
- * Last CONFIRMED dense head per session — the head this instance itself
369
- * wrote or observed via `loadStored`. `-1` records a confirmed absence (no
370
- * row). `undefined` (absent from the map) means this instance never read or
371
- * wrote the session.
372
- */
373
- headSeqs = /* @__PURE__ */ new Map();
374
- /**
375
- * Upstream seqs of delta events dropped per session. Mirrors `headSeqs` in
376
- * shape: the concurrent-writer guarantee limits each session to one writer,
377
- * so this instance is the only authority for its dropped seqs.
378
- */
379
- filteredSeqs = /* @__PURE__ */ new Map();
380
- /**
381
- * Record a head this instance actually observed or wrote.
382
- * @param id - the session id.
383
- * @param head - the confirmed dense head, or `-1` for a confirmed absence
384
- * (a fresh session this instance has read about — a later append to a
385
- * session that meanwhile got a row must reject).
386
- */
387
- confirmHead(id, head) {
388
- this.headSeqs.set(id, head);
389
- }
390
- /**
391
- * Fail loud when the on-disk head no longer matches this instance's last
392
- * confirmed head for the session. `undefined` (never read/written here) is
393
- * only acceptable for a session with NO row: a row written by someone else
394
- * means this instance's coordinator cursor is not the log's authority.
395
- * @param id - the session id.
396
- * @param storedHead - the on-disk head cursor, read inside the append
397
- * transaction before any re-numbering happens.
398
- */
399
- assertNoConcurrentWriter(id, storedHead) {
400
- const known = this.headSeqs.get(id);
401
- if (known === void 0) {
402
- if (storedHead !== -1) throw new Error(`session "${id}" has a persisted log this instance has not read; another writer may own it — load the session first`);
403
- return;
404
- }
405
- if (known !== storedHead) throw new Error(`session "${id}" was modified by another writer (stored head ${storedHead}, this instance last confirmed head ${known}); concurrent writers on one session are not supported`);
406
- }
407
- /**
408
- * Record the upstream seqs of events dropped for a session (delta events and
409
- * ignorable events), so a later batch's `assistant/message` can prune
410
- * `sourceEventSeqs` references to events that never got a persisted row.
411
- * @param id - the session id.
412
- * @param seqs - the dropped events' upstream seqs (pure-delta batches included).
413
- */
414
- noteDropped(id, seqs) {
415
- const known = this.filteredSeqs.get(id) ?? /* @__PURE__ */ new Set();
416
- for (const seq of seqs) known.add(seq);
417
- this.filteredSeqs.set(id, known);
418
- }
419
- /**
420
- * Prune `sourceEventSeqs` references that hit this session's dropped-delta
421
- * seq set. `undefined`-like state (no dropped seqs recorded for the session)
422
- * leaves the list untouched, matching the write path's "no known drops →
423
- * keep verbatim" semantics (repair closers, which never carry provenance,
424
- * call through the identity path).
425
- *
426
- * The predicate is THIS INSTANCE's view (see
427
- * {@link pruneSourceEventSeqs}): only seqs it knows were dropped are
428
- * pruned — references to rows persisted by another instance (e.g. a resume
429
- * seed segment) must survive, so the disk-wide view used by the one-shot
430
- * repair script is not applicable here.
431
- * @param id - the session id.
432
- * @param refs - the event's `sourceEventSeqs` (upstream seqs).
433
- * @returns the pruned list; identical content when nothing was dropped.
434
- */
435
- pruneRefs(id, refs) {
436
- const dropped = this.filteredSeqs.get(id);
437
- if (dropped === void 0 || dropped.size === 0) return [...refs];
438
- return pruneSourceEventSeqs(refs, (seq) => !dropped.has(seq));
439
- }
440
- };
441
- //#endregion
442
- //#region src/entities/types.ts
443
- /** 把物理列名转成 drizzle 表对象的属性名(`f_session_id` → `fSessionId`)。 */
444
- function toProperty(name) {
445
- return name.replace(/_([a-z])/g, (_match, char) => char.toUpperCase());
446
- }
447
- //#endregion
448
- //#region src/adapters/to-sqlite.ts
449
- /**
450
- * 从方言无关的表描述生成 SQLite 的 drizzle `sqliteTable` 对象。查询的类型
451
- * 安全由调用方的手写行接口(`backend.ts` 的 `SessionRow` / `EventRow` /
452
- * `EventInsert`)与显式投影兜底——drizzle 无法从运行时构建的列映射保留
453
- * 精确的列类型。
454
- *
455
- * @module @morlay/session-rdb/entities/to-sqlite
456
- */
457
- function buildColumn$1(c, tables) {
458
- let col;
459
- switch (c.type) {
460
- case "text":
461
- col = text(c.name);
462
- break;
463
- case "serial":
464
- col = integer(c.name).primaryKey({ autoIncrement: true });
465
- break;
466
- case "integer":
467
- case "bigint": {
468
- const built = integer(c.name);
469
- col = c.primaryKey ? built.primaryKey() : built;
470
- break;
471
- }
472
- }
473
- if (c.notNull) col = col.notNull();
474
- if (c.default !== void 0) col = col.default(c.default);
475
- if (c.unique) col = col.unique();
476
- if (c.references) {
477
- const { table, column, onDelete } = c.references;
478
- col = col.references(() => tables[table][toProperty(column)], { onDelete });
479
- }
480
- return col;
481
- }
482
- /** 由表描述构建 SQLite drizzle 表对象(按传入顺序;外键目标须先构建)。 */
483
- function toSqliteSchema(defs) {
484
- const tables = {};
485
- for (const def of defs) {
486
- const columns = {};
487
- for (const c of def.columns) columns[toProperty(c.name)] = buildColumn$1(c, tables);
488
- const extra = (self) => [
489
- ...(def.checks ?? []).map((c) => check(c.name, sql.raw(c.expression))),
490
- ...(def.uniques ?? []).map((u) => unique(u.name).on(...u.columns.map((name) => self[toProperty(name)]))),
491
- ...(def.indexes ?? []).map((i) => index(i.name).on(...i.columns.map((name) => self[toProperty(name)])))
492
- ];
493
- tables[def.name] = sqliteTable(def.name, columns, extra);
494
- }
495
- return tables;
496
- }
497
- //#endregion
498
- //#region src/adapters/to-postgres.ts
499
- /**
500
- * 从方言无关的表描述生成 PostgreSQL 的 drizzle `pgTable` 对象。查询的类型
501
- * 安全由调用方的手写行接口与显式投影兜底(同 `to-sqlite.ts`)。
502
- *
503
- * @module @morlay/session-rdb/entities/to-postgres
504
- */
505
- function buildColumn(c, tables) {
506
- let col;
507
- switch (c.type) {
508
- case "text":
509
- col = text$1(c.name);
510
- break;
511
- case "serial":
512
- col = serial(c.name).primaryKey();
513
- break;
514
- case "integer": {
515
- const built = integer$1(c.name);
516
- col = c.primaryKey ? built.primaryKey() : built;
517
- break;
518
- }
519
- case "bigint": {
520
- const built = bigint(c.name, { mode: "number" });
521
- col = c.primaryKey ? built.primaryKey() : built;
522
- break;
523
- }
524
- }
525
- if (c.notNull) col = col.notNull();
526
- if (c.default !== void 0) col = col.default(c.default);
527
- if (c.unique) col = col.unique();
528
- if (c.references) {
529
- const { table, column, onDelete } = c.references;
530
- col = col.references(() => tables[table][toProperty(column)], { onDelete });
531
- }
532
- return col;
533
- }
534
- /** 由表描述构建 PostgreSQL drizzle 表对象(按传入顺序;外键目标须先构建)。 */
535
- function toPostgresSchema(defs) {
536
- const tables = {};
537
- for (const def of defs) {
538
- const columns = {};
539
- for (const c of def.columns) columns[toProperty(c.name)] = buildColumn(c, tables);
540
- const extra = (self) => [
541
- ...(def.checks ?? []).map((c) => check$1(c.name, sql.raw(c.expression))),
542
- ...(def.uniques ?? []).map((u) => unique$1(u.name).on(...u.columns.map((name) => self[toProperty(name)]))),
543
- ...(def.indexes ?? []).map((i) => index$1(i.name).on(...i.columns.map((name) => self[toProperty(name)])))
544
- ];
545
- tables[def.name] = pgTable(def.name, columns, extra);
546
- }
547
- return tables;
548
- }
549
- //#endregion
550
- //#region src/adapters/ddl.ts
551
- function sqlType(dialect, type) {
552
- switch (type) {
553
- case "serial": return dialect === "sqlite" ? "INTEGER" : "SERIAL";
554
- case "integer": return "INTEGER";
555
- case "bigint": return dialect === "sqlite" ? "INTEGER" : "BIGINT";
556
- case "text": return "TEXT";
557
- }
558
- }
559
- function literal(value) {
560
- return typeof value === "string" ? `'${value.replace(/'/g, "''")}'` : String(value);
561
- }
562
- function quote(name) {
563
- return `"${name}"`;
564
- }
565
- function columnSql(dialect, c) {
566
- let sql = `${quote(c.name)} ${sqlType(dialect, c.type)}`;
567
- if (c.primaryKey) sql += " PRIMARY KEY";
568
- if (c.type === "serial" && dialect === "sqlite") sql += " AUTOINCREMENT";
569
- if (c.notNull) sql += " NOT NULL";
570
- if (c.default !== void 0) sql += ` DEFAULT ${literal(c.default)}`;
571
- if (c.unique) sql += " UNIQUE";
572
- if (c.references) {
573
- sql += ` REFERENCES ${quote(c.references.table)}(${quote(c.references.column)})`;
574
- if (c.references.onDelete) sql += ` ON DELETE ${c.references.onDelete.toUpperCase()}`;
575
- }
576
- return sql;
577
- }
578
- /** 一张表的 `CREATE TABLE IF NOT EXISTS`(表级约束内联在列清单末尾)。 */
579
- function createTableSql(dialect, def) {
580
- const parts = def.columns.map((c) => columnSql(dialect, c));
581
- for (const ck of def.checks ?? []) parts.push(`CHECK (${ck.expression})`);
582
- for (const u of def.uniques ?? []) parts.push(`UNIQUE (${u.columns.map(quote).join(", ")})`);
583
- const strict = dialect === "sqlite" ? " STRICT" : "";
584
- return `CREATE TABLE IF NOT EXISTS ${quote(def.name)} (\n ${parts.join(",\n ")}\n)${strict}`;
585
- }
586
- /** 一张表的独立索引语句(两方言索引 DDL 相同,无需 dialect 参数)。 */
587
- function createIndexSql(def, name) {
588
- const idx = def.indexes?.find((i) => i.name === name);
589
- if (idx === void 0) throw new Error(`unknown index "${name}" on table "${def.name}"`);
590
- return `CREATE INDEX IF NOT EXISTS ${quote(idx.name)} ON ${quote(def.name)}(${idx.columns.map(quote).join(", ")})`;
591
- }
592
- /** 一组表的全部建表语句(每表一条 CREATE TABLE + 每条索引)。 */
593
- function createTablesSql(dialect, defs) {
594
- const statements = [];
595
- for (const def of defs) {
596
- statements.push(createTableSql(dialect, def));
597
- for (const idx of def.indexes ?? []) statements.push(createIndexSql(def, idx.name));
598
- }
599
- return statements;
600
- }
601
- //#endregion
602
- //#region src/entities/persistence-state.ts
603
- /**
604
- * `t_persistence_state` — 单例 store 身份行。`f_singleton` 上的 CHECK 把表
605
- * 钉死为一行(`f_singleton = 1`)。
606
- */
607
- const persistenceState = {
608
- name: "t_persistence_state",
609
- columns: [{
610
- name: "f_singleton",
611
- type: "integer",
612
- primaryKey: true
613
- }, {
614
- name: "f_store_id",
615
- type: "text",
616
- notNull: true
617
- }],
618
- checks: [{
619
- name: "ck_persistence_state_singleton",
620
- expression: "f_singleton = 1"
621
- }]
622
- };
623
- //#endregion
624
- //#region src/entities/schema-meta.ts
625
- /**
626
- * `t_schema_meta` — PostgreSQL 的 schema 版本 / 应用身份载体(SQLite 用
627
- * `PRAGMA user_version` / `application_id`,PG 无等价 pragma)。键值表:
628
- * `schema_version` 与 `application_id` 两行在首次初始化时写入。
629
- * 仅 PostgreSQL 后端使用。
630
- */
631
- const schemaMeta = {
632
- name: "t_schema_meta",
633
- columns: [{
634
- name: "f_key",
635
- type: "text",
636
- primaryKey: true
637
- }, {
638
- name: "f_value",
639
- type: "text",
640
- notNull: true
641
- }]
642
- };
643
- //#endregion
644
- //#region src/entities/sessions.ts
645
- /**
646
- * `t_sessions` — 会话元数据(`SessionHeader` 列)+ playpen 风格 head 游标
647
- * (`f_head_event_id` / `f_head_sequence`,事务内维护,append 时提供 parent
648
- * 链与下一个 seq)。行的存在即 materialized 信号。
649
- */
650
- const sessions = {
651
- name: "t_sessions",
652
- columns: [
653
- {
654
- name: "f_id",
655
- type: "serial",
656
- primaryKey: true
657
- },
658
- {
659
- name: "f_session_id",
660
- type: "text",
661
- notNull: true,
662
- unique: true
663
- },
664
- {
665
- name: "f_head_event_id",
666
- type: "text",
667
- notNull: true,
668
- default: ""
669
- },
670
- {
671
- name: "f_head_sequence",
672
- type: "integer",
673
- notNull: true,
674
- default: -1
675
- },
676
- {
677
- name: "f_version",
678
- type: "integer",
679
- notNull: true
680
- },
681
- {
682
- name: "f_created_at",
683
- type: "bigint",
684
- notNull: true
685
- },
686
- {
687
- name: "f_cwd",
688
- type: "text"
689
- },
690
- {
691
- name: "f_parent_session",
692
- type: "text"
693
- },
694
- {
695
- name: "f_seed_length",
696
- type: "integer"
697
- },
698
- {
699
- name: "f_origin",
700
- type: "text"
701
- },
702
- {
703
- name: "f_delegation_depth",
704
- type: "integer"
705
- },
706
- {
707
- name: "f_incarnation",
708
- type: "text",
709
- notNull: true
710
- },
711
- {
712
- name: "f_revision",
713
- type: "integer",
714
- notNull: true
715
- }
716
- ]
717
- };
718
- //#endregion
719
- //#region src/entities/events.ts
720
- /**
721
- * `t_events` — 全局可寻址的持久化事件实体:`f_event_id`(UUID 唯一)、
722
- * `f_parent_id`(事件链,空串为 root)、`f_kind`(= 上游 `type`)、
723
- * `f_role` / `f_name` / `f_action_id`(playpen 事件维度)、`f_encoding`
724
- * (`json`)、`f_data`(JSON 文本)、`f_created_at`(= `time`)、
725
- * `f_original_seq`(上游 seq)以及 surface 元数据列(JSON 文本或 NULL)。
726
- */
727
- const events = {
728
- name: "t_events",
729
- columns: [
730
- {
731
- name: "f_id",
732
- type: "serial",
733
- primaryKey: true
734
- },
735
- {
736
- name: "f_event_id",
737
- type: "text",
738
- notNull: true,
739
- unique: true
740
- },
741
- {
742
- name: "f_parent_id",
743
- type: "text",
744
- notNull: true,
745
- default: ""
746
- },
747
- {
748
- name: "f_kind",
749
- type: "text",
750
- notNull: true,
751
- default: ""
752
- },
753
- {
754
- name: "f_role",
755
- type: "text",
756
- notNull: true,
757
- default: ""
758
- },
759
- {
760
- name: "f_name",
761
- type: "text",
762
- notNull: true,
763
- default: ""
764
- },
765
- {
766
- name: "f_action_id",
767
- type: "text",
768
- notNull: true,
769
- default: ""
770
- },
771
- {
772
- name: "f_encoding",
773
- type: "text",
774
- notNull: true,
775
- default: ""
776
- },
777
- {
778
- name: "f_data",
779
- type: "text",
780
- notNull: true
781
- },
782
- {
783
- name: "f_created_at",
784
- type: "bigint",
785
- notNull: true,
786
- default: 0
787
- },
788
- {
789
- name: "f_original_seq",
790
- type: "integer",
791
- notNull: true
792
- },
793
- {
794
- name: "f_source_event_seqs",
795
- type: "text"
796
- },
797
- {
798
- name: "f_surface_op",
799
- type: "text"
800
- }
801
- ]
802
- };
803
- //#endregion
804
- //#region src/entities/session-events.ts
805
- /**
806
- * `t_session_events` — 会话↔事件桥接表。`(f_session_id, f_sequence)` 唯一且
807
- * 有序,会话 log 按稠密 seq 读取;删除 torn tail 只删桥接行(事件实体作为
808
- * 全局行保留)。
809
- */
810
- const sessionEvents = {
811
- name: "t_session_events",
812
- columns: [
813
- {
814
- name: "f_id",
815
- type: "serial",
816
- primaryKey: true
817
- },
818
- {
819
- name: "f_session_id",
820
- type: "text",
821
- notNull: true,
822
- references: {
823
- table: "t_sessions",
824
- column: "f_session_id",
825
- onDelete: "cascade"
826
- }
827
- },
828
- {
829
- name: "f_event_id",
830
- type: "text",
831
- notNull: true,
832
- references: {
833
- table: "t_events",
834
- column: "f_event_id",
835
- onDelete: "cascade"
836
- }
837
- },
838
- {
839
- name: "f_sequence",
840
- type: "integer",
841
- notNull: true
842
- }
843
- ],
844
- uniques: [{
845
- name: "uq_session_events_session_sequence",
846
- columns: ["f_session_id", "f_sequence"]
847
- }]
848
- };
849
- //#endregion
850
- //#region src/entities/index.ts
851
- /**
852
- * 实体纯定义:每张表一个文件,方言无关的表描述(列 / 约束 / 索引 / 外键)。
853
- * 具体后端的 drizzle 表对象与建表 DDL 由 `src/adapters/` 从这里转化生成。
854
- *
855
- * @module @morlay/session-rdb/entities
856
- */
857
- /** SQLite 后端使用的表(不含 pg 专用的 `t_schema_meta`)。 */
858
- const sqliteTableDefs = [
859
- persistenceState,
860
- sessions,
861
- events,
862
- sessionEvents
863
- ];
864
- /** PostgreSQL 后端使用的表。 */
865
- const postgresTableDefs = [
866
- persistenceState,
867
- schemaMeta,
868
- sessions,
869
- events,
870
- sessionEvents
871
- ];
872
- //#endregion
873
- //#region src/schema.ts
874
- /**
875
- * The on-disk schema version. Bumped only on a breaking change to the table
876
- * layout; orthogonal to a session's own `version` (which versions the EVENT
877
- * vocabulary, stored per session in the `t_sessions` row).
878
- */
879
- const SCHEMA_VERSION = 1;
880
- /** SQLite application id protecting unrelated databases from persistence writes. */
881
- const SESSION_PERSISTENCE_SQLITE_APPLICATION_ID = 1146308688;
882
- /**
883
- * Event types whose CONTENT is not persisted: the backend drops these rows
884
- * entirely and re-numbers the surviving events to a dense persisted seq.
885
- * Mirrors the persistence proposal's "ephemeral events never enter the
886
- * canonical log" split.
887
- */
888
- const EPHEMERAL_EVENT_TYPES = ["assistant/chunk"];
889
- /** `t_events.f_encoding` value: JSON text. Future compression would switch this per row. */
890
- const EVENT_ENCODING = "json";
891
- /**
892
- * SQLite drizzle tables derived from the single entity definitions in
893
- * `src/entities/`. The runtime column objects are authoritative; TS type
894
- * safety of queries is carried by the hand-written row interfaces in
895
- * `backend.ts` (drizzle cannot infer precise column types from a runtime-built
896
- * column map, so the table handles are deliberately loose).
897
- */
898
- const sqliteTables = toSqliteSchema(sqliteTableDefs);
899
- /** `t_persistence_state` — the singleton row holding the store identity. */
900
- const tPersistenceState = sqliteTables["t_persistence_state"];
901
- /** `t_sessions` — the out-of-log metadata plus the playpen-style head cursor. */
902
- const tSessions = sqliteTables["t_sessions"];
903
- /** `t_events` — the globally addressable persisted event entity. */
904
- const tEvents = sqliteTables["t_events"];
905
- /** `t_session_events` — the session↔event bridge. */
906
- const tSessionEvents = sqliteTables["t_session_events"];
907
- /**
908
- * How long a connection waits for a contended write lock before failing with
909
- * `SQLITE_BUSY`. SQLite's default is 0 (fail immediately): with two processes
910
- * sharing one database (a second `dsh` instance on the same `sessions.sqlite`),
911
- * every append that meets an in-flight commit would fail and that process's
912
- * session would silently lose its tail. A nonzero wait makes the contention
913
- * window a queue instead of a loss.
914
- */
915
- const DEFAULT_BUSY_TIMEOUT_MS = 5e3;
916
- /**
917
- * Whether an event type is ephemeral (its content must not be persisted).
918
- * @param type - the upstream `SessionEvent.type`.
919
- * @returns true for delta events the backend drops at write time.
920
- */
921
- function isEphemeralType(type) {
922
- return EPHEMERAL_EVENT_TYPES.includes(type);
923
- }
924
- /**
925
- * Whether an event must be persisted. An event is dropped at write time when
926
- * its type is ephemeral (content not persisted) OR the writer marked it
927
- * `ignorable` — the envelope contract promises loss of an ignorable event
928
- * cannot affect reconstruction, so it never enters the canonical log (the
929
- * upstream seq is still recorded for provenance pruning, exactly like a
930
- * dropped delta). `session-branch/version` is a branch-layer lineage fact
931
- * carried as an ignorable seed event: it stays in the LIVE log but is NOT
932
- * persisted here — the branch provider persists it in its own version table
933
- * (`t_branch_versions`), keeping canonical-log semantics intact.
934
- */
935
- function isPersistedEvent(event) {
936
- return !isEphemeralType(event.type) && event.ignorable !== true;
937
- }
938
- /**
939
- * Map a persisted event onto the playpen event dimensions. `f_kind` is the
940
- * upstream type; `f_role`/`f_name`/`f_action_id` are the playpen classification
941
- * columns. Unknown (plugin-merged) event types keep the playpen defaults so a
942
- * future extension can classify them without a schema change.
943
- * @param event - the event to classify (never an ephemeral type at write time).
944
- * @returns the role, name, and action-id column values.
945
- */
946
- function eventDimensions(event) {
947
- switch (event.type) {
948
- case "turn/start":
949
- case "turn/end":
950
- case "step/start":
951
- case "step/end":
952
- case "session/end-seed": return {
953
- role: "turn",
954
- name: "",
955
- actionId: ""
956
- };
957
- case "user/message":
958
- case "request/header":
959
- case "request/context": return {
960
- role: "user",
961
- name: "",
962
- actionId: ""
963
- };
964
- case "assistant/message": return {
965
- role: "model",
966
- name: "",
967
- actionId: ""
968
- };
969
- case "tool/call": return {
970
- role: "function",
971
- name: event.data.name,
972
- actionId: event.data.callId
973
- };
974
- case "tool/result": return {
975
- role: "function",
976
- name: "",
977
- actionId: (event.data.message?.content[0])?.toolCallId ?? ""
978
- };
979
- case "todo/write": return {
980
- role: "state",
981
- name: "todos",
982
- actionId: ""
983
- };
984
- default: return {
985
- role: "",
986
- name: "",
987
- actionId: ""
988
- };
989
- }
990
- }
991
- //#endregion
992
- //#region src/sqlite.ts
993
- /**
994
- * SQLite 存储后端:在 `node:sqlite` `DatabaseSync` 之上实现 {@link Backend}。
995
- * drizzle 经 `drizzle-orm/node-sqlite` 驱动包装,查询语义与 PostgreSQL 后端
996
- * 共用同一套 {@link BackendTx} 原语。数据库打开/建表/schema 版本与身份校验
997
- * ({@link openDatabase})也归本模块所有——`SqliteBackend` 的实现不跨文件。
998
- *
999
- * 事务:SQLite 单连接,`BEGIN IMMEDIATE` 提前获取写锁(受 `busy_timeout`
1000
- * pragma 排队保护);`COMMIT`/`ROLLBACK` 之后同一连接继续服务普通查询。
1001
- * @module @morlay/session-rdb/sqlite
1002
- */
1003
- /**
1004
- * Process-wide SQLite write-transaction queues, keyed by database path.
1005
- * SQLite allows exactly one writer, and the async transaction callback (see
1006
- * {@link SqliteBackend.transaction}) must never overlap another connection's
1007
- * `BEGIN IMMEDIATE` inside this process: the second, synchronous BEGIN would
1008
- * busy-wait on the lock and freeze the event loop, so the lock holder could
1009
- * never commit (deadlock until busy_timeout). Serializing per PATH removes the
1010
- * gap entirely.
1011
- *
1012
- * The queue is per path, not global: two backends on DIFFERENT files have no
1013
- * lock to contend for, so serializing them would be pure waste. Two instances
1014
- * sharing one file (the supported multi-process deployment) still share one
1015
- * queue, preserving the deadlock guarantee. `:memory:` databases are distinct
1016
- * per connection but share the key — serializing them is harmless (tests).
1017
- */
1018
- const sqliteTxQueues = /* @__PURE__ */ new Map();
1019
- /** Run `fn` behind the write-transaction queue for one database path. */
1020
- function enqueueSqliteTx(path, fn) {
1021
- const run = (sqliteTxQueues.get(path) ?? Promise.resolve()).then(fn);
1022
- sqliteTxQueues.set(path, run.then(() => void 0, () => void 0));
1023
- return run;
1024
- }
1025
- /**
1026
- * Exclusively create a missing database file with owner-only permissions.
1027
- * Existing files retain their modes, and errors other than `EEXIST` propagate.
1028
- * `DatabaseSync` reopens by path, so this does not protect confidentiality or
1029
- * integrity when another principal can replace the database entry in its parent
1030
- * directory.
1031
- */
1032
- async function createDatabaseFile(path) {
1033
- try {
1034
- await (await open(path, "wx", 384)).close();
1035
- } catch (error) {
1036
- if (error.code !== "EEXIST") throw error;
1037
- }
1038
- }
1039
- /**
1040
- * Open the database and apply its schema and pragmas. An empty database with a
1041
- * zero `user_version` is initialized at {@link SCHEMA_VERSION}; a nonempty
1042
- * unversioned database and every other non-current version reject rather than
1043
- * being migrated in place.
1044
- * @param path - the SQLite database file to open (created when absent).
1045
- * @param journalMode - validated journal pragma.
1046
- * @param busyTimeout - milliseconds to wait for a contended write lock before
1047
- * failing with `SQLITE_BUSY`; `0` fails immediately (SQLite's default).
1048
- * @returns the open handle with pragmas applied and all tables ensured.
1049
- */
1050
- function openDatabase(path, journalMode, busyTimeout = DEFAULT_BUSY_TIMEOUT_MS) {
1051
- const db = new DatabaseSync(path);
1052
- try {
1053
- configureDatabase(db, path, journalMode, busyTimeout);
1054
- return db;
1055
- } catch (error) {
1056
- db.close();
1057
- throw error;
1058
- }
1059
- }
1060
- function configureDatabase(db, path, journalMode, busyTimeout) {
1061
- db.exec("PRAGMA foreign_keys = ON");
1062
- db.exec(`PRAGMA busy_timeout = ${busyTimeout}`);
1063
- drizzle$1({ client: db }).transaction((tx) => {
1064
- const { user_version: onDisk } = tx.get(sql`PRAGMA user_version`);
1065
- const { application_id: applicationId } = tx.get(sql`PRAGMA application_id`);
1066
- const { count: userObjectCount } = tx.get(sql`SELECT COUNT(*) AS count FROM sqlite_schema WHERE name NOT GLOB 'sqlite_*'`);
1067
- if (onDisk === 0 && (applicationId !== 0 || userObjectCount > 0)) throw new Error(`session database at "${path}" has an unversioned schema or application identity`);
1068
- if (onDisk !== 0 && onDisk !== 1) throw new Error(`session database at "${path}" has schema version ${onDisk}, incompatible with this build (1)`);
1069
- if (onDisk === 1 && applicationId !== 1146308688) throw new Error(`session database at "${path}" has application id ${applicationId}, expected ${SESSION_PERSISTENCE_SQLITE_APPLICATION_ID}`);
1070
- for (const statement of createTablesSql("sqlite", sqliteTableDefs)) tx.run(sql.raw(statement));
1071
- tx.insert(tPersistenceState).values({
1072
- fSingleton: 1,
1073
- fStoreId: randomUUID()
1074
- }).onConflictDoNothing().run();
1075
- if (onDisk === 0) {
1076
- tx.run(sql.raw(`PRAGMA application_id = ${SESSION_PERSISTENCE_SQLITE_APPLICATION_ID}`));
1077
- tx.run(sql.raw(`PRAGMA user_version = 1`));
1078
- }
1079
- }, { behavior: "immediate" });
1080
- db.exec(`PRAGMA journal_mode = ${journalMode.toUpperCase()}`);
1081
- }
1082
- /**
1083
- * SQLite 存储后端。打开时创建目录/文件(owner-only)、应用 pragma 与 DDL、
1084
- * 校验 schema 版本/应用身份并读取 store id。
1085
- */
1086
- var SqliteBackend = class {
1087
- options;
1088
- kind = "sqlite";
1089
- storeIdentity;
1090
- /** The resolved database path (queue key); set by {@link open}. */
1091
- dbPath = "";
1092
- db;
1093
- constructor(options) {
1094
- this.options = options;
1095
- }
1096
- async open() {
1097
- const actual = this.options.path === ":memory:" ? this.options.path : resolve(this.options.path);
1098
- this.dbPath = actual;
1099
- if (actual !== ":memory:") {
1100
- await mkdir(dirname(actual), {
1101
- recursive: true,
1102
- mode: 448
1103
- });
1104
- await createDatabaseFile(actual);
1105
- }
1106
- await enqueueSqliteTx(actual, async () => {
1107
- this.db = drizzle$1({ client: openDatabase(actual, this.options.journalMode, this.options.busyTimeout) });
1108
- });
1109
- try {
1110
- const row = this.db.select({ fStoreId: tPersistenceState.fStoreId }).from(tPersistenceState).where(eq(tPersistenceState.fSingleton, 1)).get();
1111
- /* v8 ignore next -- openDatabase inserts the singleton before returning. */
1112
- if (row === void 0) throw new Error(`session database at "${actual}" has no store identity`);
1113
- if (row.fStoreId.length === 0) throw new Error(`session database at "${actual}" has no valid store identity`);
1114
- if (actual !== ":memory:") {
1115
- const identity = statSync(actual, { bigint: true });
1116
- this.storeIdentity = `file:${identity.dev}:${identity.ino}:${identity.birthtimeNs}:store:${row.fStoreId}`;
1117
- } else this.storeIdentity = `memory:store:${row.fStoreId}`;
1118
- } catch (error) {
1119
- this.db.$client.close();
1120
- throw error;
1121
- }
1122
- }
1123
- async close() {
1124
- if (this.db === void 0) return;
1125
- this.db.$client.close();
1126
- }
1127
- async getSession(id) {
1128
- return this.db.select().from(tSessions).where(eq(tSessions.fSessionId, id)).get();
1129
- }
1130
- async getSeqMapRows(id) {
1131
- return this.eventRows().where(eq(tSessionEvents.fSessionId, id)).all();
1132
- }
1133
- async getEventRows(id, fromSequence) {
1134
- return (fromSequence === void 0 ? this.eventRows().where(eq(tSessionEvents.fSessionId, id)) : this.eventRows().where(and(eq(tSessionEvents.fSessionId, id), gte(tSessionEvents.fSequence, fromSequence)))).orderBy(tSessionEvents.fSequence).all();
1135
- }
1136
- async listSessions() {
1137
- return this.db.select().from(tSessions).all();
1138
- }
1139
- async transaction(fn) {
1140
- return enqueueSqliteTx(this.dbPath, async () => {
1141
- this.db.$client.exec("BEGIN IMMEDIATE");
1142
- try {
1143
- const result = await fn(this.tx);
1144
- this.db.$client.exec("COMMIT");
1145
- return result;
1146
- } catch (error) {
1147
- /* v8 ignore start */
1148
- try {
1149
- this.db.$client.exec("ROLLBACK");
1150
- } catch {}
1151
- throw error;
1152
- }
1153
- });
1154
- }
1155
- /**
1156
- * SQLite is a single connection: after `BEGIN IMMEDIATE` every query on the
1157
- * same handle is inside the transaction, so the tx primitives are the same
1158
- * row primitives used by the non-transactional reads.
1159
- */
1160
- tx = {
1161
- upsertSession: (meta, incarnation) => this.upsertSession(meta, incarnation),
1162
- getHead: (id) => this.getHead(id),
1163
- insertEvents: (events) => this.insertEvents(events),
1164
- insertBridges: (rows) => this.insertBridges(rows),
1165
- updateHead: (id, headEventId, headSequence) => this.updateHead(id, headEventId, headSequence),
1166
- bumpRevision: (id) => this.bumpRevision(id),
1167
- deleteBridgeTail: (id, fromSequence) => this.deleteBridgeTail(id, fromSequence),
1168
- getPrevBridge: (id, sequence) => this.getPrevBridge(id, sequence),
1169
- getLastBridge: (id) => this.getLastBridge(id),
1170
- updateEventFields: (id, sequence, fields) => this.updateEventFields(id, sequence, fields)
1171
- };
1172
- async upsertSession(meta, incarnation) {
1173
- this.db.insert(tSessions).values(sessionInsertRow(meta, incarnation)).onConflictDoUpdate({
1174
- target: tSessions.fSessionId,
1175
- set: sessionConflictRow(meta)
1176
- }).run();
1177
- }
1178
- async getHead(id) {
1179
- const head = this.db.select({
1180
- fHeadEventId: tSessions.fHeadEventId,
1181
- fHeadSequence: tSessions.fHeadSequence
1182
- }).from(tSessions).where(eq(tSessions.fSessionId, id)).get();
1183
- /* v8 ignore next -- appendBatch/commitRepair always materialize the row before reading the head */
1184
- if (head === void 0) throw new Error(`session "${id}" has no materialized row`);
1185
- return head;
1186
- }
1187
- async insertEvents(events) {
1188
- if (events.length === 0) return;
1189
- this.db.insert(tEvents).values(events.map((event) => ({ ...event }))).run();
1190
- }
1191
- async insertBridges(rows) {
1192
- if (rows.length === 0) return;
1193
- this.db.insert(tSessionEvents).values(rows.map((row) => ({ ...row }))).run();
1194
- }
1195
- async updateHead(id, headEventId, headSequence) {
1196
- this.db.update(tSessions).set({
1197
- fHeadEventId: headEventId,
1198
- fHeadSequence: headSequence
1199
- }).where(eq(tSessions.fSessionId, id)).run();
1200
- }
1201
- async bumpRevision(id) {
1202
- this.db.update(tSessions).set({ fRevision: sql`${tSessions.fRevision} + 1` }).where(eq(tSessions.fSessionId, id)).run();
1203
- }
1204
- async deleteBridgeTail(id, fromSequence) {
1205
- this.db.delete(tSessionEvents).where(and(eq(tSessionEvents.fSessionId, id), gte(tSessionEvents.fSequence, fromSequence))).run();
1206
- }
1207
- async getPrevBridge(id, sequence) {
1208
- return this.db.select({
1209
- fEventId: tSessionEvents.fEventId,
1210
- fSequence: tSessionEvents.fSequence
1211
- }).from(tSessionEvents).where(and(eq(tSessionEvents.fSessionId, id), eq(tSessionEvents.fSequence, sequence))).get();
1212
- }
1213
- async getLastBridge(id) {
1214
- return this.db.select({
1215
- fEventId: tSessionEvents.fEventId,
1216
- fSequence: tSessionEvents.fSequence
1217
- }).from(tSessionEvents).where(eq(tSessionEvents.fSessionId, id)).orderBy(desc(tSessionEvents.fSequence)).limit(1).get();
1218
- }
1219
- /** The joined event-row projection shared by whole-log and suffix reads. */
1220
- eventRows() {
1221
- return this.db.select({
1222
- fSequence: tSessionEvents.fSequence,
1223
- fOriginalSeq: tEvents.fOriginalSeq,
1224
- fKind: tEvents.fKind,
1225
- fCreatedAt: tEvents.fCreatedAt,
1226
- fData: tEvents.fData,
1227
- fSourceEventSeqs: tEvents.fSourceEventSeqs,
1228
- fSurfaceOp: tEvents.fSurfaceOp
1229
- }).from(tSessionEvents).innerJoin(tEvents, eq(tSessionEvents.fEventId, tEvents.fEventId));
1230
- }
1231
- async updateEventFields(id, sequence, fields) {
1232
- const eventIds = this.db.select({ fEventId: tSessionEvents.fEventId }).from(tSessionEvents).where(and(eq(tSessionEvents.fSessionId, id), eq(tSessionEvents.fSequence, sequence)));
1233
- this.db.update(tEvents).set({
1234
- ...fields.fSourceEventSeqs === void 0 ? {} : { fSourceEventSeqs: fields.fSourceEventSeqs },
1235
- ...fields.fSurfaceOp === void 0 ? {} : { fSurfaceOp: fields.fSurfaceOp },
1236
- ...fields.fData === void 0 ? {} : { fData: fields.fData }
1237
- }).where(inArray(tEvents.fEventId, eventIds)).run();
1238
- }
1239
- };
1240
- //#endregion
1241
- //#region src/postgres.ts
1242
- /**
1243
- * PostgreSQL 存储后端:在 drizzle 的 PG async 驱动之上实现 {@link Backend}。
1244
- * 驱动(`drizzle-orm/node-postgres` 生产 / `drizzle-orm/pglite` 测试)在构造
1245
- * 时注入,因此本模块不依赖具体 PG 客户端包。
1246
- *
1247
- * 与 SQLite 的差异(方言事实,非行为差异):
1248
- * - `f_created_at` 用 `BIGINT`(毫秒时间戳超出 PG `INTEGER` 的 int32 范围);
1249
- * - schema 版本/应用身份校验用 `t_schema_meta` 键值表代替 SQLite 的
1250
- * `PRAGMA user_version` / `application_id`(PG 无等价 pragma);
1251
- * - 事务用 drizzle 的异步 `db.transaction`(PG 无 `BEGIN IMMEDIATE`,写锁靠
1252
- * `busy_timeout` 之外的数据库行锁/唯一约束兜底)。
1253
- * @module @morlay/session-rdb/postgres
1254
- */
1255
- /**
1256
- * PostgreSQL drizzle tables derived from the single entity definitions in
1257
- * `src/entities/` (type safety carried by the hand-written row interfaces in
1258
- * `backend.ts`, same as the SQLite side).
1259
- */
1260
- const pgTables = toPostgresSchema(postgresTableDefs);
1261
- /** `t_persistence_state` — the singleton row holding the store identity. */
1262
- const pgPersistenceState = pgTables["t_persistence_state"];
1263
- /** `t_schema_meta` — PG's schema-version / application-identity key-value store. */
1264
- const pgSchemaMeta = pgTables["t_schema_meta"];
1265
- /** `t_sessions` — the out-of-log metadata plus the playpen-style head cursor. */
1266
- const pgSessions = pgTables["t_sessions"];
1267
- /** `t_events` — the globally addressable persisted event entity. */
1268
- const pgEvents = pgTables["t_events"];
1269
- /** `t_session_events` — the session↔event bridge. */
1270
- const pgSessionEvents = pgTables["t_session_events"];
1271
- /**
1272
- * PostgreSQL 存储后端。构造时注入 drizzle PG 实例;{@link open} 建表并做
1273
- * schema 版本/应用身份校验(`t_schema_meta`,替代 SQLite 的 PRAGMA)。
1274
- */
1275
- var PostgresBackend = class {
1276
- db;
1277
- options;
1278
- kind = "postgres";
1279
- storeIdentity;
1280
- constructor(db, options) {
1281
- this.db = db;
1282
- this.options = options;
1283
- }
1284
- async open() {
1285
- const storeId = await this.db.transaction(async (tx) => {
1286
- const metaExists = (await tx.execute(sql`SELECT to_regclass('t_schema_meta') IS NOT NULL AS exists`)).rows[0]?.exists === true;
1287
- for (const statement of createTablesSql("postgres", postgresTableDefs)) await tx.execute(sql.raw(statement));
1288
- if (!metaExists) await tx.insert(pgSchemaMeta).values([{
1289
- fKey: "schema_version",
1290
- fValue: String(1)
1291
- }, {
1292
- fKey: "application_id",
1293
- fValue: String(SESSION_PERSISTENCE_SQLITE_APPLICATION_ID)
1294
- }]).execute();
1295
- const version = await this.readMeta(tx, "schema_version");
1296
- const applicationId = await this.readMeta(tx, "application_id");
1297
- if (version === void 0 || applicationId === void 0) throw new Error("session database has an unversioned schema or application identity");
1298
- if (Number(version) !== 1) throw new Error(`session database has schema version ${version}, incompatible with this build (1)`);
1299
- if (Number(applicationId) !== 1146308688) throw new Error(`session database has application id ${applicationId}, expected ${SESSION_PERSISTENCE_SQLITE_APPLICATION_ID}`);
1300
- await tx.insert(pgPersistenceState).values({
1301
- fSingleton: 1,
1302
- fStoreId: randomUUID()
1303
- }).onConflictDoNothing().execute();
1304
- const storeId = (await tx.select({ fStoreId: pgPersistenceState.fStoreId }).from(pgPersistenceState).where(eq(pgPersistenceState.fSingleton, 1)).execute())[0]?.fStoreId;
1305
- if (storeId === void 0 || storeId.length === 0) throw new Error("session database has no valid store identity");
1306
- return storeId;
1307
- });
1308
- this.storeIdentity = `${this.options.identityBase}:store:${storeId}`;
1309
- }
1310
- async close() {
1311
- await this.options.close();
1312
- }
1313
- async getSession(id) {
1314
- return (await this.db.select().from(pgSessions).where(eq(pgSessions.fSessionId, id)).execute())[0];
1315
- }
1316
- async getSeqMapRows(id) {
1317
- return this.eventRows(this.db).where(eq(pgSessionEvents.fSessionId, id)).execute();
1318
- }
1319
- async getEventRows(id, fromSequence) {
1320
- return (fromSequence === void 0 ? this.eventRows(this.db).where(eq(pgSessionEvents.fSessionId, id)) : this.eventRows(this.db).where(and(eq(pgSessionEvents.fSessionId, id), gte(pgSessionEvents.fSequence, fromSequence)))).orderBy(pgSessionEvents.fSequence).execute();
1321
- }
1322
- async listSessions() {
1323
- return this.db.select().from(pgSessions).execute();
1324
- }
1325
- async transaction(fn) {
1326
- return this.db.transaction(async (tx) => fn(this.txFor(tx)));
1327
- }
1328
- /** Bind the {@link BackendTx} primitives to one drizzle PG transaction handle. */
1329
- txFor(tx) {
1330
- return {
1331
- upsertSession: (meta, incarnation) => this.upsertSession(tx, meta, incarnation),
1332
- getHead: (id) => this.getHead(tx, id),
1333
- insertEvents: (events) => this.insertEvents(tx, events),
1334
- insertBridges: (rows) => this.insertBridges(tx, rows),
1335
- updateHead: (id, headEventId, headSequence) => this.updateHead(tx, id, headEventId, headSequence),
1336
- bumpRevision: (id) => this.bumpRevision(tx, id),
1337
- deleteBridgeTail: (id, fromSequence) => this.deleteBridgeTail(tx, id, fromSequence),
1338
- getPrevBridge: (id, sequence) => this.getPrevBridge(tx, id, sequence),
1339
- getLastBridge: (id) => this.getLastBridge(tx, id),
1340
- updateEventFields: (id, sequence, fields) => this.updateEventFields(tx, id, sequence, fields)
1341
- };
1342
- }
1343
- async readMeta(exec, key) {
1344
- return (await exec.select({ fValue: pgSchemaMeta.fValue }).from(pgSchemaMeta).where(eq(pgSchemaMeta.fKey, key)).execute())[0]?.fValue;
1345
- }
1346
- async upsertSession(exec, meta, incarnation) {
1347
- await exec.insert(pgSessions).values(sessionInsertRow(meta, incarnation)).onConflictDoUpdate({
1348
- target: pgSessions.fSessionId,
1349
- set: sessionConflictRow(meta)
1350
- }).execute();
1351
- }
1352
- async getHead(exec, id) {
1353
- const head = (await exec.select({
1354
- fHeadEventId: pgSessions.fHeadEventId,
1355
- fHeadSequence: pgSessions.fHeadSequence
1356
- }).from(pgSessions).where(eq(pgSessions.fSessionId, id)).execute())[0];
1357
- /* v8 ignore next -- appendBatch/commitRepair always materialize the row before reading the head */
1358
- if (head === void 0) throw new Error(`session "${id}" has no materialized row`);
1359
- return head;
1360
- }
1361
- async insertEvents(exec, events) {
1362
- if (events.length === 0) return;
1363
- await exec.insert(pgEvents).values(events.map((event) => ({ ...event }))).execute();
1364
- }
1365
- async insertBridges(exec, rows) {
1366
- if (rows.length === 0) return;
1367
- await exec.insert(pgSessionEvents).values(rows.map((row) => ({ ...row }))).execute();
1368
- }
1369
- async updateHead(exec, id, headEventId, headSequence) {
1370
- await exec.update(pgSessions).set({
1371
- fHeadEventId: headEventId,
1372
- fHeadSequence: headSequence
1373
- }).where(eq(pgSessions.fSessionId, id)).execute();
1374
- }
1375
- async bumpRevision(exec, id) {
1376
- await exec.update(pgSessions).set({ fRevision: sql`${pgSessions.fRevision} + 1` }).where(eq(pgSessions.fSessionId, id)).execute();
1377
- }
1378
- async deleteBridgeTail(exec, id, fromSequence) {
1379
- await exec.delete(pgSessionEvents).where(and(eq(pgSessionEvents.fSessionId, id), gte(pgSessionEvents.fSequence, fromSequence))).execute();
1380
- }
1381
- async getPrevBridge(exec, id, sequence) {
1382
- return (await exec.select({
1383
- fEventId: pgSessionEvents.fEventId,
1384
- fSequence: pgSessionEvents.fSequence
1385
- }).from(pgSessionEvents).where(and(eq(pgSessionEvents.fSessionId, id), eq(pgSessionEvents.fSequence, sequence))).execute())[0];
1386
- }
1387
- async getLastBridge(exec, id) {
1388
- return (await exec.select({
1389
- fEventId: pgSessionEvents.fEventId,
1390
- fSequence: pgSessionEvents.fSequence
1391
- }).from(pgSessionEvents).where(eq(pgSessionEvents.fSessionId, id)).orderBy(desc(pgSessionEvents.fSequence)).limit(1).execute())[0];
1392
- }
1393
- /** The joined event-row projection shared by whole-log and suffix reads. */
1394
- eventRows(exec) {
1395
- return exec.select({
1396
- fSequence: pgSessionEvents.fSequence,
1397
- fOriginalSeq: pgEvents.fOriginalSeq,
1398
- fKind: pgEvents.fKind,
1399
- fCreatedAt: pgEvents.fCreatedAt,
1400
- fData: pgEvents.fData,
1401
- fSourceEventSeqs: pgEvents.fSourceEventSeqs,
1402
- fSurfaceOp: pgEvents.fSurfaceOp
1403
- }).from(pgSessionEvents).innerJoin(pgEvents, eq(pgSessionEvents.fEventId, pgEvents.fEventId));
1404
- }
1405
- async updateEventFields(exec, id, sequence, fields) {
1406
- const eventIds = exec.select({ fEventId: pgSessionEvents.fEventId }).from(pgSessionEvents).where(and(eq(pgSessionEvents.fSessionId, id), eq(pgSessionEvents.fSequence, sequence)));
1407
- await exec.update(pgEvents).set({
1408
- ...fields.fSourceEventSeqs === void 0 ? {} : { fSourceEventSeqs: fields.fSourceEventSeqs },
1409
- ...fields.fSurfaceOp === void 0 ? {} : { fSurfaceOp: fields.fSurfaceOp },
1410
- ...fields.fData === void 0 ? {} : { fData: fields.fData }
1411
- }).where(inArray(pgEvents.fEventId, eventIds)).execute();
1412
- }
1413
- };
1414
- //#endregion
1415
- //#region src/branch.ts
1416
- /**
1417
- * RDB 的「额外 provider 抽象」实现:在 `@morlay/session-rdb`
1418
- * 既有 `PersistenceBackend`(append-only 面)之上,实现
1419
- * `@morlay/session-branch` 的 `SessionBranchProvider`(分支面:rewind /
1420
- * forkFrom / readBranchPrefix),并发布 `ctx.sessionBranch` 服务
1421
- * (`SessionBranchRdb extends SessionBranch`)——使本包成为 rewind /
1422
- * retry / fork 的持久化闭环。
1423
- *
1424
- * 关键设计(不改上游代码):
1425
- *
1426
- * - `forkFrom` 走**标准 coordinator 路径**(`create` + `append`):派生是纯
1427
- * append(新 id / parentSession / seedLength),上游 `PersistenceCoordinator`
1428
- * 天然支持;seed 前缀按新会话 log 重新编号(保序重编号,`sourceEventSeqs`
1429
- * 的相对序不变,`source < seq` 校验成立)。
1430
- * - `rewind` 是上游没有的原语(`PersistenceCoordinator` 只有 append-only +
1431
- * torn-tail 修复),因此直接操作本后端的 `Backend` 事务(DELETE 尾部 +
1432
- * head 游标回退 + revision bump),随后**重新 load 同步 coordinator 状态**
1433
- * (revision 变化使 `isPreparedSourceCurrent` 失效 → 重新 adopt →
1434
- * `state.cursor` 与新尾部一致)并更新 `WriteGuard` 确认 head。
1435
- * - 独占条件:无 live owner(`ctx.sessions.get(id) === undefined`)。prepared
1436
- * reservation 无法从公开 API 查询,rewind 通过 revision 变化使其自然失效。
1437
- *
1438
- * @module @morlay/session-rdb/branch
1439
- */
1440
- /**
1441
- * 定位 `atSeq` 锚定的闭合 `turn/end` 边界 seq。见
1442
- * {@link SessionBranchProvider.readBranchPrefix} 的锚定语义说明。
1443
- * @param events - 完整(或前缀)事件列表,seq 连续。
1444
- * @param atSeq - 锚定 seq(inclusive);省略取最后闭合轮次。
1445
- * @param mode - `"after"`(默认)或 `"before"`。
1446
- * @returns 边界事件 seq;`"before"` 模式下 atSeq 之前无闭合轮次时返回 -1
1447
- * (空前缀)。
1448
- */
1449
- function locateTurnEnd(events, atSeq, mode = "after") {
1450
- const ends = events.filter((event) => event.type === "turn/end").map((event) => event.seq);
1451
- if (atSeq === void 0) {
1452
- const last = ends.at(-1);
1453
- if (last === void 0) throw new SessionBranchError("session has no closed turn", "OPEN_TURN");
1454
- return last;
1455
- }
1456
- if (mode === "before") {
1457
- let boundary = -1;
1458
- for (const seq of ends) if (seq < atSeq) boundary = seq;
1459
- else break;
1460
- return boundary;
1461
- }
1462
- const firstAfter = ends.find((seq) => seq >= atSeq);
1463
- if (firstAfter !== void 0) return firstAfter;
1464
- const lastStart = [...events].reverse().find((event) => event.type === "turn/start");
1465
- if (lastStart !== void 0 && lastStart.seq <= atSeq) throw new SessionBranchError(`anchor ${atSeq} lies inside an open turn`, "OPEN_TURN");
1466
- const last = ends.at(-1);
1467
- if (last === void 0) throw new SessionBranchError("session has no closed turn", "OPEN_TURN");
1468
- return last;
1469
- }
1470
- /** 按新会话 log 重新编号:保序重编号,`sourceEventSeqs` 相对序不变。 */
1471
- function renumber(events, offset) {
1472
- return events.map((event, index) => ({
1473
- ...event,
1474
- seq: offset + index
1475
- }));
1476
- }
1477
- /** 派生会话 id(后端 mint 策略,与 apiproxy `session-<uuid>` 一致)。 */
1478
- function mintSessionId() {
1479
- return `session-${randomUUID()}`;
1480
- }
1481
- /**
1482
- * 截断 live 会话的内存 log 并重置全部派生缓存。上游 `Session` 的
1483
- * `log`/`surfaceManager`/`headerFold`/`contextFold`/`derived` 都是增量缓存:
1484
- * 截断 `log` 后必须同步重置,否则下一次 append 会基于陈旧状态校验/投影。
1485
- * 字段名取编译后产物(`@deepseek-ai/dsh-session` lib),并对
1486
- * `SurfaceManager`(持有同一 `log` 数组引用)做全量重折叠复位——
1487
- * `_lastProcessedSeq = baseSeq - 1` 使下次访问重新 fold 整个截断后 log。
1488
- */
1489
- function truncateLiveSession(session, newLength) {
1490
- const s = session;
1491
- s.log.length = newLength;
1492
- s.eventsSnapshot = void 0;
1493
- s.headerFold = void 0;
1494
- s.headerFoldSeq = 0;
1495
- s.contextFold = void 0;
1496
- s.contextFoldSeq = 0;
1497
- s.derived = [];
1498
- s.derivedNodes = 0;
1499
- s.derivedGeneration = 0;
1500
- s.surfaceManager._state = {
1501
- nodes: [],
1502
- replaceGeneration: 0
1503
- };
1504
- s.surfaceManager._lastProcessedSeq = s.surfaceManager.baseSeq - 1;
1505
- s.surfaceManager._pendingPlan = void 0;
1506
- }
1507
- /**
1508
- * RDB 分支数据层实现(`SessionBranchProvider`)。与 `SessionPersistenceRdb`
1509
- * 共享同一数据库连接(`Backend`)与 coordinator 写路径。
1510
- */
1511
- var SessionBranchRdbProvider = class {
1512
- persistence;
1513
- live;
1514
- name = "session-rdb";
1515
- constructor(persistence, live = {
1516
- getSession: () => void 0,
1517
- getAgent: () => void 0,
1518
- flush: async () => true,
1519
- setCoordinatorCursor: () => {},
1520
- setCoordinatorState: () => {}
1521
- }) {
1522
- this.persistence = persistence;
1523
- this.live = live;
1524
- }
1525
- async readBranchPrefix(id, atSeq, mode = "after", signal) {
1526
- const { events } = await this.readRawEvents(id, signal);
1527
- const boundary = locateTurnEnd(events, atSeq, mode);
1528
- return {
1529
- seq: boundary,
1530
- events: events.slice(0, boundary + 1)
1531
- };
1532
- }
1533
- /**
1534
- * 读取会话的**原始**事件(不含 coordinator 补记的合成 closers)。
1535
- *
1536
- * `persistence.inspect` 走 coordinator 的 `prepareCore`,会给未闭合 log
1537
- * 补 `interruptedTurnClosers`(合成 step/end + turn/end)——对普通读取
1538
- * (客户端历史)这是正确的逻辑视图,但对分支编排是误导:未闭合轮次被
1539
- * closers 掩盖成闭合,编辑未闭合轮次的 user 消息会走错边界。这里用
1540
- * `loadStored`(backend hook,scanRows 只做 torn-tail 切割、不补 closers)
1541
- * 读原始事件,使未闭合状态真实可见。
1542
- */
1543
- async readRawEvents(id, signal) {
1544
- const stored = await this.persistence.loadStored(id, signal);
1545
- if (stored === void 0) throw new SessionBranchError(`session "${id}" not found`, "SESSION_NOT_FOUND");
1546
- return {
1547
- meta: stored.meta,
1548
- events: stored.events
1549
- };
1550
- }
1551
- async forkFrom(sourceId, options = {}, signal) {
1552
- signal?.throwIfAborted();
1553
- const { atSeq, anchorMode = "after", seedSuffix = [], childSessionId, meta = {} } = options;
1554
- const source = await this.persistence.inspect(sourceId, signal);
1555
- const boundary = locateTurnEnd(source.events, atSeq, anchorMode);
1556
- const prefix = source.events.slice(0, boundary + 1);
1557
- const childId = childSessionId ?? mintSessionId();
1558
- const childMeta = {
1559
- version: SESSION_FORMAT_VERSION,
1560
- id: childId,
1561
- createdAt: meta.createdAt ?? Date.now(),
1562
- ...meta.cwd !== void 0 ? { cwd: meta.cwd } : source.meta.cwd !== void 0 ? { cwd: source.meta.cwd } : {},
1563
- parentSession: sourceId,
1564
- seedLength: prefix.length,
1565
- ...meta.agentPreset !== void 0 ? { agentPreset: meta.agentPreset } : source.meta.agentPreset !== void 0 ? { agentPreset: source.meta.agentPreset } : {},
1566
- ...meta.origin !== void 0 ? { origin: meta.origin } : {},
1567
- ...meta.delegationDepth !== void 0 ? { delegationDepth: meta.delegationDepth } : {}
1568
- };
1569
- const seed = [...renumber(prefix, 0), ...renumber(seedSuffix, prefix.length)];
1570
- await this.persistence.create(childMeta);
1571
- if (seed.length > 0) await this.persistence.append(childId, seed);
1572
- return childId;
1573
- }
1574
- async rewind(id, toBoundary, signal) {
1575
- signal?.throwIfAborted();
1576
- if (!Number.isSafeInteger(toBoundary) || toBoundary < -1) throw new SessionBranchError(`rewind boundary must be a non-negative safe integer, got ${toBoundary}`, "INVALID_BOUNDARY");
1577
- const live = this.live.getSession(id);
1578
- if (live !== void 0) await this.live.flush(live);
1579
- const raw = live === void 0 ? await this.readRawEvents(id, signal) : void 0;
1580
- const inspection = live === void 0 ? void 0 : await this.persistence.inspect(id, signal);
1581
- const events = live === void 0 ? raw.events : inspection.events;
1582
- const meta = live === void 0 ? raw.meta : inspection.meta;
1583
- const boundaryEvent = events[toBoundary];
1584
- if (toBoundary === -1) {} else if (boundaryEvent === void 0) throw new SessionBranchError(`rewind boundary ${toBoundary} does not exist in session "${id}"`, "INVALID_BOUNDARY");
1585
- else if (boundaryEvent.type !== "turn/end" && boundaryEvent.type !== "user/message") throw new SessionBranchError(`rewind boundary ${toBoundary} is not a turn/end or user/message (${boundaryEvent.type})`, "INVALID_BOUNDARY");
1586
- const keepLength = toBoundary === -1 ? 0 : boundaryEvent.type === "turn/end" ? toBoundary + 1 : toBoundary;
1587
- const internals = this.persistence.internals();
1588
- const denseBoundary = live === void 0 ? keepLength - 1 : events.slice(0, keepLength).filter(isPersistedEvent).length - 1;
1589
- await internals.backend.transaction(async (tx) => {
1590
- signal?.throwIfAborted();
1591
- const head = await tx.getHead(id);
1592
- if (denseBoundary > head.fHeadSequence) throw new SessionBranchError(`rewind boundary ${toBoundary} is beyond the stored head ${head.fHeadSequence}`, "INVALID_BOUNDARY");
1593
- if (denseBoundary < head.fHeadSequence) {
1594
- await tx.deleteBridgeTail(id, denseBoundary + 1);
1595
- const prev = denseBoundary === -1 ? void 0 : await tx.getPrevBridge(id, denseBoundary);
1596
- if (prev === void 0) await tx.updateHead(id, "", -1);
1597
- else await tx.updateHead(id, prev.fEventId, prev.fSequence);
1598
- }
1599
- await tx.bumpRevision(id);
1600
- });
1601
- internals.writeGuard.confirmHead(id, denseBoundary);
1602
- if (live !== void 0) {
1603
- truncateLiveSession(live, keepLength);
1604
- const agent = this.live.getAgent(id);
1605
- if (agent !== void 0) {
1606
- agent.requestHeaderLogged = false;
1607
- const lastTurn = live.events.findLast((e) => e.type === "turn/start")?.data.turn ?? 0;
1608
- const phase = agent.phase;
1609
- if (phase !== void 0) phase.lastTurn = lastTurn;
1610
- }
1611
- this.live.setCoordinatorCursor(id, keepLength);
1612
- } else if (boundaryEvent?.type === "user/message") this.live.setCoordinatorState(id, keepLength, meta);
1613
- else await this.persistence.load(id);
1614
- const row = await internals.backend.getSession(id);
1615
- if (row === void 0) return {
1616
- header: {
1617
- version: SESSION_FORMAT_VERSION,
1618
- id,
1619
- createdAt: meta.createdAt,
1620
- ...meta.cwd !== void 0 ? { cwd: meta.cwd } : {},
1621
- ...meta.parentSession !== void 0 ? { parentSession: meta.parentSession } : {},
1622
- ...meta.seedLength !== void 0 ? { seedLength: meta.seedLength } : {},
1623
- ...meta.origin !== void 0 ? { origin: meta.origin } : {},
1624
- ...meta.delegationDepth !== void 0 ? { delegationDepth: meta.delegationDepth } : {},
1625
- ...meta.agentPreset !== void 0 ? { agentPreset: meta.agentPreset } : {}
1626
- },
1627
- revision: await internals.readStoredRevision(id) ?? await this.persistence.readStoredRevision(id)
1628
- };
1629
- return {
1630
- header: rowToMeta(row),
1631
- revision: await internals.readStoredRevision(id)
1632
- };
1633
- }
1634
- };
1635
- /**
1636
- * RDB 的 `ctx.sessionBranch` 服务:组合 {@link SessionBranchRdbProvider} 的
1637
- * 数据层原语与共享版本树投影。插件把本类注册为 `sessionBranch` 服务后,
1638
- * 编排层即可通过统一服务面完成 rewind / retry / fork。
1639
- */
1640
- var SessionBranchRdb = class extends SessionBranch {
1641
- static inject = ["sessionPersistence", "sessions"];
1642
- constructor(ctx) {
1643
- super(ctx);
1644
- }
1645
- provider = new SessionBranchRdbProvider(this.ctx.sessionPersistence, {
1646
- getSession: (id) => this.ctx.sessions.get(id),
1647
- getAgent: (id) => {
1648
- return this.ctx.get("agents")?.get(id);
1649
- },
1650
- flush: (session) => this.ctx.sessions.flush(session),
1651
- setCoordinatorCursor: (id, cursor) => {
1652
- const state = this.ctx.sessionPersistence.coordinator?.states?.get(id);
1653
- if (state !== void 0) state.cursor = cursor;
1654
- },
1655
- setCoordinatorState: (id, cursor, meta) => {
1656
- const states = this.ctx.sessionPersistence.coordinator?.states;
1657
- if (states === void 0) return;
1658
- const state = states.get(id);
1659
- if (state !== void 0) state.cursor = cursor;
1660
- else states.set(id, {
1661
- meta,
1662
- cursor,
1663
- materialized: true
1664
- });
1665
- }
1666
- });
1667
- readBranchPrefix(id, atSeq, mode, signal) {
1668
- return this.provider.readBranchPrefix(id, atSeq, mode, signal);
1669
- }
1670
- /**
1671
- * 读取会话的**原始**事件(不含 coordinator 补记的合成 closers)。
1672
- * 编排层用它识别未闭合轮次(inspect 会把未闭合 log 补成闭合)。
1673
- */
1674
- readRawEvents(id, signal) {
1675
- return this.provider.readRawEvents(id, signal);
1676
- }
1677
- forkFrom(sourceId, options, signal) {
1678
- return this.provider.forkFrom(sourceId, options, signal);
1679
- }
1680
- rewind(id, toBoundary, signal) {
1681
- return this.provider.rewind(id, toBoundary, signal);
1682
- }
1683
- /**
1684
- * 清洗一个 session 的 surface/provenance 坐标到稠密空间(持久化层透传)。
1685
- * 旧数据(rewind 前写入)的 sourceEventSeqs / surfaceOp / shadowedRange
1686
- * 是上游坐标,读取时每次都要对齐;清洗一次性写回稠密坐标,之后读取无需
1687
- * 对齐。返回实际变更的事件数。
1688
- */
1689
- cleanseSession(sessionId, signal) {
1690
- return this.ctx.sessionPersistence.cleanseSession(sessionId, signal);
1691
- }
1692
- /**
1693
- * 同步 live 会话的 coordinator 内存 cursor,跳过 ignorable 占位事件。
1694
- * 编排层在 rewind 后把 ignorable 版本效果 push 进 live log(不发布、不
1695
- * 进缓冲)——它占用一个上游 seq,但 coordinator 的 cursor(rewind 设到
1696
- * 截断后长度)看不见它;不跳过的话,flush 的 `appendLiveBatch` 会以
1697
- * `e.seq >= cursor` 过滤掉 cursor 之前的事件(manualTurn 永远不落盘),
1698
- * 或 `appendCore` 的 seq 连续性校验错位。这里把 cursor 从当前值起跳过
1699
- * 连续的 ignorable 事件,对齐到下一个待持久化事件的 seq。
1700
- */
1701
- syncLiveCursor(sessionId) {
1702
- const live = this.ctx.sessions.get(sessionId);
1703
- if (live === void 0) return;
1704
- const state = this.ctx.sessionPersistence.coordinator?.states?.get(sessionId);
1705
- if (state === void 0) return;
1706
- let cursor = state.cursor;
1707
- while (live.events[cursor]?.ignorable === true) cursor += 1;
1708
- state.cursor = cursor;
1709
- }
1710
- async timeline(sessionId, signal) {
1711
- const persistence = this.ctx.sessionPersistence;
1712
- const snapshots = await persistence.listSnapshots(signal);
1713
- const readOwnEvents = async (id, fromSeq, s) => {
1714
- const live = this.ctx.sessions.get(id);
1715
- if (live !== void 0) return live.events.slice(fromSeq);
1716
- return (await persistence.readFrom(id, fromSeq, s)).events;
1717
- };
1718
- return buildTimeline(snapshots, readOwnEvents, sessionId, signal);
1719
- }
1720
- };
1721
- //#endregion
1722
- //#region src/index.ts
1723
- /**
1724
- * The persistence backend. Load as a plugin; it registers as
1725
- * `ctx.sessionPersistence` and (via the coordinator) installs the write-path
1726
- * listeners. Its torn-tail marker is the persisted seq to delete from.
1727
- *
1728
- * Configuration resolution: `$DSH_HOME/settings.yaml` 的
1729
- * `session-rdb` namespace(settings 服务)覆盖 cordis 层 entry
1730
- * config,见 {@link installSettingsSection}。
1731
- */
1732
- var SessionPersistenceRdb = class SessionPersistenceRdb extends SessionPersistence {
1733
- config;
1734
- static inject = ["sessions", "settings"];
1735
- static Config = z.union([z.object({
1736
- type: z.const("sqlite"),
1737
- path: z.string().required(),
1738
- journalMode: z.union([
1739
- "wal",
1740
- "delete",
1741
- "truncate",
1742
- "persist"
1743
- ]).default("wal"),
1744
- busyTimeout: z.number().step(1).min(0).default(DEFAULT_BUSY_TIMEOUT_MS)
1745
- }), z.object({
1746
- type: z.const("postgres"),
1747
- connectionString: z.string().required()
1748
- })]);
1749
- /** settings namespace:`$DSH_HOME/settings.yaml` 的 `session-rdb` section。 */
1750
- static settingsNs = settingsNamespace("session-rdb");
1751
- /**
1752
- * Backend label for the coordinator's dispose diagnostics. Intentionally
1753
- * shadows cordis `Service.name` (set to `'sessionPersistence'` by the base);
1754
- * see the JSONL backend for why this does not affect service resolution.
1755
- */
1756
- name = "session-rdb";
1757
- /** One RDB database holds every session; there is no per-session raw artifact. */
1758
- supportsRawArtifacts = false;
1759
- backend;
1760
- storeIdentity;
1761
- ready;
1762
- coordinator;
1763
- /**
1764
- * Write-authority state: the confirmed dense head per session (concurrent-
1765
- * writer detection) and the dropped delta seqs per session (provenance
1766
- * pruning). See {@link WriteGuard} for the timing contract.
1767
- */
1768
- writeGuard = new WriteGuard();
1769
- constructor(ctx, config, injectedBackend) {
1770
- let resolved = config;
1771
- const settings = ctx.reflect.get("settings");
1772
- if (settings !== void 0) {
1773
- const scope = settings.register(SessionPersistenceRdb.settingsNs, SessionPersistenceRdb.Config, { base: config });
1774
- resolved = scope.get();
1775
- scope.watch(() => {
1776
- ctx.logger.warn("session-rdb: settings changed; restart to apply the new configuration");
1777
- });
1778
- }
1779
- super(ctx);
1780
- this.config = config;
1781
- this.config = resolved;
1782
- this.backend = injectedBackend ?? createBackend(resolved);
1783
- this.ready = this.init();
1784
- this.coordinator = new PersistenceCoordinator(this.ctx, this);
1785
- new SessionBranchRdb(this.ctx);
1786
- }
1787
- async init() {
1788
- await this.backend.open();
1789
- this.storeIdentity = this.backend.storeIdentity;
1790
- }
1791
- /** The backend has one database, not an independent local artifact per session. */
1792
- locate(_meta) {}
1793
- create(meta) {
1794
- return this.coordinator.create(meta);
1795
- }
1796
- append(id, events) {
1797
- return this.coordinator.append(id, events);
1798
- }
1799
- load(id) {
1800
- return this.coordinator.load(id);
1801
- }
1802
- inspect(id, signal) {
1803
- return this.coordinator.inspect(id, signal);
1804
- }
1805
- readFrom(id, fromSeq, signal) {
1806
- return this.coordinator.readFrom(id, fromSeq, signal);
1807
- }
1808
- /** Read a stored prefix by id (ids are globally unique — no scope to scan). */
1809
- loadStored(id, signal) {
1810
- return this.readPrefix(id, signal);
1811
- }
1812
- /**
1813
- * Seek-capable suffix read: the backend selects `f_sequence >= fromSeq`
1814
- * directly, so the read scales with the suffix, not the log. Provenance
1815
- * remapping still needs every row's upstream seq, so a lightweight
1816
- * two-column map is read alongside. Torn rows past the preserved region are
1817
- * dropped, never repaired (non-mutating read).
1818
- */
1819
- async loadStoredFrom(id, fromSeq, signal) {
1820
- const log = await this.readLog(id, { fromSeq }, signal);
1821
- if (log === void 0) return void 0;
1822
- return {
1823
- meta: log.meta,
1824
- events: log.events
1825
- };
1826
- }
1827
- /**
1828
- * Read a session's row + ordered events into a {@link StoredPrefix}. The
1829
- * torn-tail marker is the persisted seq from which a never-committed tail
1830
- * must be deleted (`scanRows` already returns it as `number | undefined`).
1831
- * Records the confirmed dense head (or confirmed absence) so a later
1832
- * `appendBatch` can detect a second writer that advanced the log.
1833
- */
1834
- async readPrefix(id, signal) {
1835
- const log = await this.readLog(id, {}, signal);
1836
- if (log === void 0) {
1837
- this.writeGuard.confirmHead(id, -1);
1838
- return;
1839
- }
1840
- this.writeGuard.confirmHead(id, log.events.at(-1)?.seq ?? -1);
1841
- normalizeSurfaceReplaceProvenance(log.events);
1842
- return {
1843
- meta: log.meta,
1844
- events: log.events,
1845
- revision: SessionPersistenceRevision(`${this.storeIdentity}:incarnation:${log.incarnation}:revision:${log.revision}`),
1846
- ...log.tornFrom !== void 0 ? { tornMarker: log.tornFrom } : {}
1847
- };
1848
- }
1849
- /**
1850
- * Read the current source-qualified revision for one stored session without
1851
- * loading its event log. Returns `undefined` when the identity is absent.
1852
- * The representation matches {@link loadStored}'s `revision` and
1853
- * {@link listSnapshots} — the coordinator compares them with `===`.
1854
- */
1855
- async readStoredRevision(id, signal) {
1856
- signal?.throwIfAborted();
1857
- await this.ready;
1858
- signal?.throwIfAborted();
1859
- const row = await this.backend.getSession(id);
1860
- if (row === void 0) return void 0;
1861
- return SessionPersistenceRevision(`${this.storeIdentity}:incarnation:${row.fIncarnation}:revision:${row.fRevision}`);
1862
- }
1863
- /**
1864
- * Shared read pipeline: session row → meta, event rows → preserved prefix.
1865
- * A whole-log read (`fromSeq` absent) builds the seq map from the same rows;
1866
- * a suffix read keeps the backend's lightweight two-column seq-map source so
1867
- * the query still scales with the suffix, not the log.
1868
- */
1869
- async readLog(id, options = {}, signal) {
1870
- signal?.throwIfAborted();
1871
- await this.ready;
1872
- signal?.throwIfAborted();
1873
- const row = await this.backend.getSession(id);
1874
- if (row === void 0) return void 0;
1875
- const meta = rowToMeta(row);
1876
- let eventRows;
1877
- let seqMap;
1878
- let denseTypeMap;
1879
- if (options.fromSeq === void 0) {
1880
- eventRows = await this.backend.getEventRows(id);
1881
- seqMap = buildSeqMap(eventRows);
1882
- denseTypeMap = new Map(eventRows.map((r) => [r.fSequence, r.fKind]));
1883
- } else {
1884
- eventRows = await this.backend.getEventRows(id, options.fromSeq);
1885
- const seqRows = await this.backend.getSeqMapRows(id);
1886
- seqMap = buildSeqMap(seqRows);
1887
- denseTypeMap = new Map(seqRows.map((r) => [r.fSequence, r.fKind]));
1888
- }
1889
- signal?.throwIfAborted();
1890
- const { preserved, tornFrom } = scanRows(eventRows, options.fromSeq ?? 0, seqMap, denseTypeMap);
1891
- return {
1892
- meta,
1893
- events: preserved,
1894
- incarnation: row.fIncarnation,
1895
- revision: row.fRevision,
1896
- ...tornFrom !== void 0 ? { tornFrom } : {}
1897
- };
1898
- }
1899
- /**
1900
- * Durably append a batch in ONE transaction: materialize the sessions row (if
1901
- * lazy) and INSERT every persisted event (plus its bridge row), or roll back
1902
- * entirely. Delta events and events the writer marked `ignorable` are dropped
1903
- * and the surviving events are re-numbered densely from the session's head
1904
- * cursor; a batch that contains only dropped events is a no-op (no row
1905
- * materialization, no revision bump). Dropped events' upstream seqs are
1906
- * recorded per session so a later batch's surface provenance can prune
1907
- * references to them (see {@link surfaceBindings}).
1908
- * The transaction is the atomicity + durability boundary, so a mid-batch
1909
- * failure (a UNIQUE violation on a duplicated seq) leaves the stored log
1910
- * untouched.
1911
- *
1912
- * SQLite acquires the write lock up front (`BEGIN IMMEDIATE`, queued behind
1913
- * `busy_timeout`); PostgreSQL relies on the transaction's row locks and the
1914
- * `UNIQUE (f_session_id, f_sequence)` constraint to reject a colliding batch.
1915
- * Either way {@link assertNoConcurrentWriter} rejects a second writer before
1916
- * re-numbering — a session has exactly one writer per log, and a second
1917
- * writer fails loud instead of corrupting the log.
1918
- *
1919
- * The row upsert runs UNCONDITIONALLY, not only when `!isMaterialized`: a
1920
- * delta-only batch leaves the coordinator's materialized flag true while no
1921
- * row exists, so the flag cannot be trusted as the row's existence signal.
1922
- * The upsert keeps an existing row's head cursor (only header columns are
1923
- * refreshed on conflict), so a fresh row still starts at the initial head.
1924
- */
1925
- async appendBatch(meta, events, _isMaterialized) {
1926
- await this.ready;
1927
- const droppedSeqs = /* @__PURE__ */ new Set();
1928
- for (const event of events) if (!isPersistedEvent(event)) droppedSeqs.add(event.seq);
1929
- if (droppedSeqs.size > 0) this.writeGuard.noteDropped(meta.id, droppedSeqs);
1930
- const persisted = events.filter(isPersistedEvent);
1931
- if (persisted.length === 0) return;
1932
- let confirmedHead = -1;
1933
- await this.backend.transaction(async (tx) => {
1934
- await tx.upsertSession(meta, randomUUID());
1935
- const head = await tx.getHead(meta.id);
1936
- this.writeGuard.assertNoConcurrentWriter(meta.id, head.fHeadSequence);
1937
- const { headEventId, headSequence } = await appendEventTail(tx, meta, persisted, {
1938
- parentId: head.fHeadEventId,
1939
- nextSeq: head.fHeadSequence + 1
1940
- }, (refs) => this.writeGuard.pruneRefs(meta.id, refs));
1941
- await tx.updateHead(meta.id, headEventId, headSequence);
1942
- await tx.bumpRevision(meta.id);
1943
- confirmedHead = headSequence;
1944
- });
1945
- this.writeGuard.confirmHead(meta.id, confirmedHead);
1946
- }
1947
- /**
1948
- * Make a crash repair durable in ONE transaction: DELETE the torn tail (from
1949
- * `tornMarker`), rewind the head cursor to the last surviving event, INSERT
1950
- * the synthetic `closers`, and bump the revision once. After COMMIT the
1951
- * stored rows == the balanced log.
1952
- */
1953
- async commitRepair(meta, tornMarker, closers) {
1954
- await this.ready;
1955
- const persistedClosers = closers.filter(isPersistedEvent);
1956
- if (tornMarker === void 0 && persistedClosers.length === 0) return;
1957
- await this.backend.transaction(async (tx) => {
1958
- if (tornMarker !== void 0) {
1959
- await tx.deleteBridgeTail(meta.id, tornMarker);
1960
- const prev = await tx.getPrevBridge(meta.id, tornMarker - 1);
1961
- if (prev === void 0) await tx.updateHead(meta.id, "", -1);
1962
- else await tx.updateHead(meta.id, prev.fEventId, prev.fSequence);
1963
- }
1964
- if (persistedClosers.length > 0) {
1965
- const last = await tx.getLastBridge(meta.id);
1966
- const { headEventId, headSequence } = await appendEventTail(tx, meta, persistedClosers, {
1967
- parentId: last?.fEventId ?? "",
1968
- nextSeq: (last?.fSequence ?? -1) + 1
1969
- });
1970
- await tx.updateHead(meta.id, headEventId, headSequence);
1971
- }
1972
- await tx.bumpRevision(meta.id);
1973
- });
1974
- const row = await this.backend.getSession(meta.id);
1975
- this.writeGuard.confirmHead(meta.id, row?.fHeadSequence ?? -1);
1976
- }
1977
- /**
1978
- * 一次性清洗一个 session 的 surface/provenance 坐标到稠密空间并写回。
1979
- *
1980
- * 旧数据(rewind 前的代码写入)的 `sourceEventSeqs`、`surfaceOp` replace
1981
- * range 与 compaction `shadowedRange` 是上游坐标,读取时每次都要做坐标
1982
- * 解析对齐;清洗用与读取完全相同的解析(稠密优先 + 上游映射 + 剪枝 +
1983
- * replace provenance 补全)把它们重写为稠密坐标,之后读取无需再对齐
1984
- * (对已清洗数据解析恒为恒等)。torn tail 片段不参与(读取路径也不会
1985
- * 保留它们)。返回实际变更的事件数。
1986
- */
1987
- async cleanseSession(id, signal) {
1988
- signal?.throwIfAborted();
1989
- await this.ready;
1990
- signal?.throwIfAborted();
1991
- const eventRows = await this.backend.getEventRows(id);
1992
- if (eventRows.length === 0) return { changed: 0 };
1993
- const { preserved } = scanRows(eventRows, 0, buildSeqMap(eventRows), new Map(eventRows.map((r) => [r.fSequence, r.fKind])));
1994
- normalizeSurfaceReplaceProvenance(preserved);
1995
- const bySeq = new Map(preserved.map((event) => [event.seq, event]));
1996
- const updates = [];
1997
- for (const row of eventRows) {
1998
- const event = bySeq.get(row.fSequence);
1999
- if (event === void 0) continue;
2000
- const surface = event;
2001
- const newSeqs = surface.sourceEventSeqs === void 0 ? null : JSON.stringify(surface.sourceEventSeqs);
2002
- const newOp = surface.surfaceOp === void 0 ? null : JSON.stringify(surface.surfaceOp);
2003
- const fields = {};
2004
- if (newSeqs !== row.fSourceEventSeqs) fields.fSourceEventSeqs = newSeqs;
2005
- if (newOp !== row.fSurfaceOp) fields.fSurfaceOp = newOp;
2006
- if (row.fKind === "compaction/summary" || row.fKind === "compaction/prune") {
2007
- const storedData = JSON.parse(row.fData);
2008
- const newData = event.data;
2009
- if (JSON.stringify(storedData.shadowedRange) !== JSON.stringify(newData.shadowedRange)) fields.fData = JSON.stringify(event.data);
2010
- }
2011
- if (Object.keys(fields).length > 0) updates.push({
2012
- fSequence: row.fSequence,
2013
- ...fields
2014
- });
2015
- }
2016
- if (updates.length === 0) return { changed: 0 };
2017
- await this.backend.transaction(async (tx) => {
2018
- for (const update of updates) await tx.updateEventFields(id, update.fSequence, {
2019
- ...update.fSourceEventSeqs === void 0 ? {} : { fSourceEventSeqs: update.fSourceEventSeqs },
2020
- ...update.fSurfaceOp === void 0 ? {} : { fSurfaceOp: update.fSurfaceOp },
2021
- ...update.fData === void 0 ? {} : { fData: update.fData }
2022
- });
2023
- });
2024
- return { changed: updates.length };
2025
- }
2026
- /** List all materialized sessions' metadata (every row is a materialized session). */
2027
- async list(signal) {
2028
- signal?.throwIfAborted();
2029
- await this.ready;
2030
- signal?.throwIfAborted();
2031
- const rows = await this.backend.listSessions();
2032
- signal?.throwIfAborted();
2033
- return rows.map(rowToMeta);
2034
- }
2035
- /** List metadata with a source-qualified monotonic revision per session. */
2036
- async listSnapshots(signal) {
2037
- signal?.throwIfAborted();
2038
- await this.ready;
2039
- signal?.throwIfAborted();
2040
- const rows = await this.backend.listSessions();
2041
- signal?.throwIfAborted();
2042
- return rows.map((row) => ({
2043
- header: rowToMeta(row),
2044
- revision: SessionPersistenceRevision(`${this.storeIdentity}:incarnation:${row.fIncarnation}:revision:${row.fRevision}`)
2045
- }));
2046
- }
2047
- /** Close the database connection (awaited by the coordinator's dispose, post-drain). */
2048
- async close() {
2049
- await this.ready;
2050
- await this.backend.close();
2051
- }
2052
- /**
2053
- * 同包分支 provider 的内部访问面(rewind / forkFrom 共享后端与写路径)。
2054
- * @internal 仅供 `SessionBranchRdbProvider` 使用;不是公开 API。
2055
- */
2056
- internals() {
2057
- return {
2058
- backend: this.backend,
2059
- writeGuard: this.writeGuard,
2060
- create: (meta) => this.create(meta),
2061
- append: (id, events) => this.append(id, events),
2062
- load: (id) => this.load(id),
2063
- inspect: (id, signal) => this.inspect(id, signal),
2064
- readFrom: (id, fromSeq, signal) => this.readFrom(id, fromSeq, signal),
2065
- listSnapshots: (signal) => this.listSnapshots(signal),
2066
- readStoredRevision: (id, signal) => this.readStoredRevision(id, signal)
2067
- };
2068
- }
2069
- };
2070
- /**
2071
- * Build the configured backend. The PostgreSQL arm creates the `node-postgres`
2072
- * pool here (its identity base comes from the parsed pool options); tests
2073
- * inject a drizzle PG instance directly via {@link PostgresBackend}.
2074
- */
2075
- function createBackend(config) {
2076
- if (config.type === "sqlite") return new SqliteBackend({
2077
- path: config.path,
2078
- journalMode: config.journalMode ?? "wal",
2079
- busyTimeout: config.busyTimeout ?? 5e3
2080
- });
2081
- const pool = new Pool({ connectionString: config.connectionString });
2082
- pool.on("error", () => {});
2083
- return new PostgresBackend(drizzle({ client: pool }), {
2084
- identityBase: [
2085
- "postgres",
2086
- pool.options.host ?? "localhost",
2087
- String(pool.options.port ?? 5432),
2088
- pool.options.database ?? ""
2089
- ].join(":"),
2090
- close: () => pool.end()
2091
- });
2092
- }
2093
- /**
2094
- * Serialize an event's surface-metadata fields for SQL binding. Both fields are
2095
- * nullable TEXT columns — null when the event has no surface metadata (non-surface
2096
- * events, events written before surface support).
2097
- *
2098
- * `sourceEventSeqs` references events by UPSTREAM seq. Delta events dropped at
2099
- * write time never get a persisted row, so a reference to one can never be
2100
- * remapped on read — keeping it verbatim produces a `source >= current seq`
2101
- * provenance violation when the log is replayed as a session seed. The write
2102
- * path therefore prunes references through {@link WriteGuard.pruneRefs}; a
2103
- * fully pruned list is stored as null (no provenance).
2104
- * @param event - the event to serialize.
2105
- * @param prune - prunes references to dropped deltas before binding. Defaults
2106
- * to identity (e.g. repair closers, which never carry provenance).
2107
- */
2108
- function surfaceBindings(event, prune = (refs) => refs) {
2109
- const se = event;
2110
- const sourceSeqs = se.sourceEventSeqs === void 0 ? void 0 : prune(se.sourceEventSeqs);
2111
- return [sourceSeqs !== void 0 && sourceSeqs.length > 0 ? JSON.stringify(sourceSeqs) : null, se.surfaceOp !== void 0 ? JSON.stringify(se.surfaceOp) : null];
2112
- }
2113
- /**
2114
- * Durably append one batch of persisted events to a session's tail inside the
2115
- * enclosing transaction: mint each event's row (parent chain + playpen
2116
- * dimensions + surface-metadata columns) and its bridge row, land both as ONE
2117
- * multi-row INSERT each (N events are 2 statements instead of 2N), and return
2118
- * the resulting head cursor.
2119
- *
2120
- * The anchor is the caller's responsibility: a normal append starts from the
2121
- * head cursor (`head.fHeadEventId` / `head.fHeadSequence + 1`), while
2122
- * crash-repair closers start from the ACTUAL tail row (the head cursor can lag
2123
- * a hand-written torn tail). Both callers then persist the returned cursor via
2124
- * {@link BackendTx.updateHead}.
2125
- * @param tx - the enclosing transaction.
2126
- * @param meta - the session being written (`meta.id` drives the bridge rows).
2127
- * @param events - persisted events to append (callers already filtered out
2128
- * ephemeral/ignorable events; non-empty).
2129
- * @param anchor - the parent event id to chain from and the next dense seq.
2130
- * @param prune - forwarded to {@link surfaceBindings}; defaults to identity
2131
- * (repair closers never carry provenance).
2132
- * @returns the new head cursor (last event id + its dense seq).
2133
- */
2134
- async function appendEventTail(tx, meta, events, anchor, prune = (refs) => refs) {
2135
- let parentId = anchor.parentId;
2136
- let nextSeq = anchor.nextSeq;
2137
- const eventRows = [];
2138
- const bridgeRows = [];
2139
- for (const event of events) {
2140
- const eventId = randomUUID();
2141
- const { role, name, actionId } = eventDimensions(event);
2142
- const [surfaceSeqs, surfaceOp] = surfaceBindings(event, prune);
2143
- eventRows.push({
2144
- fEventId: eventId,
2145
- fParentId: parentId,
2146
- fKind: event.type,
2147
- fRole: role,
2148
- fName: name,
2149
- fActionId: actionId,
2150
- fEncoding: EVENT_ENCODING,
2151
- fData: JSON.stringify(event.data),
2152
- fCreatedAt: event.time,
2153
- fOriginalSeq: event.seq,
2154
- fSourceEventSeqs: surfaceSeqs,
2155
- fSurfaceOp: surfaceOp
2156
- });
2157
- bridgeRows.push({
2158
- fSessionId: meta.id,
2159
- fEventId: eventId,
2160
- fSequence: nextSeq
2161
- });
2162
- parentId = eventId;
2163
- nextSeq++;
2164
- }
2165
- await tx.insertEvents(eventRows);
2166
- await tx.insertBridges(bridgeRows);
2167
- return {
2168
- headEventId: parentId,
2169
- headSequence: nextSeq - 1
2170
- };
2171
- }
2172
- //#endregion
2173
- export { EPHEMERAL_EVENT_TYPES, SCHEMA_VERSION, SessionBranchRdb, SessionBranchRdbProvider, SessionPersistenceRdb, SessionPersistenceRdb as default, locateTurnEnd };
2174
-
2175
- //# sourceMappingURL=index.mjs.map