@morlay/session-rdb 0.0.15 → 0.0.16-alpha.1

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.
Files changed (65) hide show
  1. package/README.md +11 -13
  2. package/dist/artifact.d.mts +30 -16
  3. package/dist/artifact.mjs +3 -3
  4. package/dist/{import-DFed8DBt.mjs → import-mZZgDBLd.mjs} +73 -83
  5. package/dist/import.d.mts +2 -3
  6. package/dist/import.mjs +2 -2
  7. package/dist/index-GdKkSSb-.d.mts +239 -0
  8. package/dist/index.d.mts +3 -3
  9. package/dist/index.mjs +763 -168
  10. package/dist/log-DBFUBPhv.mjs +394 -0
  11. package/dist/{schema-CFXZURX7.d.mts → schema-COK7-wRV.d.mts} +3 -15
  12. package/dist/sqlite-CnWwGToZ.mjs +698 -0
  13. package/dist/storage.d.mts +2 -7
  14. package/dist/storage.mjs +2 -3
  15. package/dist/testing.d.mts +30 -1
  16. package/dist/testing.mjs +676 -1991
  17. package/drizzle/postgres/20260908072805_v2_initial/migration.sql +58 -0
  18. package/drizzle/postgres/20260908072805_v2_initial/snapshot.json +686 -0
  19. package/drizzle/postgres/20260908072806_v3_drop_original_seq/migration.sql +1 -0
  20. package/drizzle/postgres/20260908072806_v3_drop_original_seq/snapshot.json +673 -0
  21. package/drizzle/sqlite/20260908072751_v2_initial/migration.sql +53 -0
  22. package/drizzle/sqlite/20260908072751_v2_initial/snapshot.json +492 -0
  23. package/drizzle/sqlite/20260908072752_v3_drop_original_seq/migration.sql +6 -0
  24. package/drizzle/sqlite/20260908072752_v3_drop_original_seq/snapshot.json +513 -0
  25. package/package.json +17 -12
  26. package/src/adapters/index.ts +10 -2
  27. package/src/adapters/to-postgres.ts +19 -15
  28. package/src/adapters/to-sqlite.ts +20 -14
  29. package/src/{entities → adapters}/types.ts +7 -8
  30. package/src/backend.ts +0 -7
  31. package/src/branch.ts +29 -110
  32. package/src/drizzle/postgres-v2.ts +11 -0
  33. package/src/drizzle/postgres-v3.ts +11 -0
  34. package/src/drizzle/sqlite-v2.ts +10 -0
  35. package/src/drizzle/sqlite-v3.ts +11 -0
  36. package/src/entities/index.ts +2 -30
  37. package/src/entities/v2/index.ts +20 -0
  38. package/src/entities/v2/session-events.ts +11 -0
  39. package/src/entities/v3/events.ts +27 -0
  40. package/src/entities/v3/index.ts +27 -0
  41. package/src/entities/v3/persistence-state.ts +10 -0
  42. package/src/entities/v3/schema-meta.ts +9 -0
  43. package/src/entities/v3/session-events.ts +26 -0
  44. package/src/entities/v3/sessions.ts +20 -0
  45. package/src/import.ts +65 -57
  46. package/src/index.ts +929 -227
  47. package/src/invariant.ts +0 -2
  48. package/src/legacy.ts +110 -0
  49. package/src/log.ts +143 -91
  50. package/src/postgres.ts +75 -93
  51. package/src/schema.ts +3 -14
  52. package/src/sqlite.ts +59 -46
  53. package/src/testing/contract.ts +460 -375
  54. package/src/testing/coordinator-contract.ts +108 -1374
  55. package/src/testing.ts +1 -6
  56. package/dist/index-uUvn6gYp.d.mts +0 -111
  57. package/dist/schema-vwzwEXNE.mjs +0 -878
  58. package/dist/sqlite-CG_Qcx5C.mjs +0 -356
  59. package/src/adapters/ddl.ts +0 -77
  60. package/src/entities/events.ts +0 -27
  61. package/src/entities/persistence-state.ts +0 -10
  62. package/src/entities/schema-meta.ts +0 -9
  63. package/src/entities/session-events.ts +0 -29
  64. package/src/entities/sessions.ts +0 -20
  65. package/src/migrate.ts +0 -137
package/dist/index.mjs CHANGED
@@ -1,14 +1,19 @@
1
- import { i as WriteGuard, r as createTablesSql, t as SqliteBackend } from "./sqlite-CG_Qcx5C.mjs";
2
- import { A as sessionInsertRow, D as rowToMeta, O as scanRows, T as repairSurfaceOps, _ as buildSeqMap, a as SESSION_PERSISTENCE_SQLITE_APPLICATION_ID, b as recomputeReplaceProvenance, g as toPostgresSchema, i as SCHEMA_VERSION, j as toJsonlArtifact, k as sessionConflictRow, l as isPersistedEvent, m as postgresTableDefs, n as EPHEMERAL_EVENT_TYPES, o as eventDimensions, r as EVENT_ENCODING, t as DEFAULT_BUSY_TIMEOUT_MS, w as repairOrphanInboxSplices } from "./schema-vwzwEXNE.mjs";
3
- import { c as SessionBranchRdb, l as SessionBranchRdbProvider, s as registerSessionImport, u as locateTurnEnd } from "./import-DFed8DBt.mjs";
1
+ import { a as SCHEMA_VERSION, g as WriteGuard, h as toPostgresSchema, i as EVENT_ENCODING, m as postgresTableDefs, r as DEFAULT_BUSY_TIMEOUT_MS, s as eventDimensions, t as SqliteBackend } from "./sqlite-CnWwGToZ.mjs";
2
+ import { d as sessionConflictRow, f as sessionInsertRow, l as rowToMeta, m as toJsonlArtifact, o as repairReadView, u as scanRows } from "./log-DBFUBPhv.mjs";
3
+ import { c as SessionBranchRdbProvider, l as locateTurnEnd, o as registerSessionImport, s as SessionBranchRdb } from "./import-mZZgDBLd.mjs";
4
4
  import z from "@deepseek-ai/schemastery";
5
5
  import { randomUUID } from "node:crypto";
6
6
  import { Pool } from "pg";
7
7
  import { drizzle } from "drizzle-orm/node-postgres";
8
- import { PersistenceCoordinator, SessionPersistence, SessionPersistenceRevision } from "@deepseek-ai/dsh-session-persistence";
9
- import { SessionLogOffset } from "@deepseek-ai/dsh-session";
10
- import { and, desc, eq, gte, sql } from "drizzle-orm";
8
+ import { SessionAlreadyExistsError, SessionAlreadyOwnedError, SessionHandleClosedError, SessionPersistence, SessionPersistenceNotFoundError, SessionPersistenceRevision, SessionReadOnlyError, assertContiguous, assertVersion, materializeAppendBatch, materializeCreateHeader, validateStoredEvents } from "@deepseek-ai/dsh-session-persistence";
9
+ import { SESSION_FORMAT_VERSION, SessionLogOffset } from "@deepseek-ai/dsh-session";
10
+ import { sessionFormatCatalog } from "@deepseek-ai/dsh-session-format-catalog";
11
+ import { and, eq, gte, sql } from "drizzle-orm";
12
+ import { readdirSync } from "node:fs";
13
+ import { migrate } from "drizzle-orm/node-postgres/migrator";
11
14
  //#region src/postgres.ts
15
+ /** drizzle-kit 生成的迁移目录(随包根 drizzle/ 发布;src/dist 形态经相对 URL 统一解析)。 */
16
+ const postgresMigrationsDir = new URL("../drizzle/postgres/", import.meta.url).pathname;
12
17
  var PostgresBackend = class PostgresBackend {
13
18
  db;
14
19
  options;
@@ -21,45 +26,47 @@ var PostgresBackend = class PostgresBackend {
21
26
  this.tables = toPostgresSchema(postgresTableDefs, this.options.schema ?? "public");
22
27
  }
23
28
  async open() {
29
+ const schema = this.options.schema ?? "public";
30
+ const qualifiedMeta = schema === "public" ? "t_schema_meta" : `"${schema}".t_schema_meta`;
31
+ if ((await this.db.execute(sql`SELECT to_regclass(${qualifiedMeta}) IS NOT NULL AS exists`)).rows[0]?.exists === true) {
32
+ if (await this.readMeta(this.db, "schema_version") === "2") await this.baselineV2();
33
+ }
34
+ await migrate(this.db, { migrationsFolder: postgresMigrationsDir });
24
35
  const storeId = await this.db.transaction(async (tx) => {
25
- const schema = this.options.schema ?? "public";
26
- const qualifiedMeta = schema === "public" ? "t_schema_meta" : `"${schema}".t_schema_meta`;
27
- const metaExists = (await tx.execute(sql`SELECT to_regclass(${qualifiedMeta}) IS NOT NULL AS exists`)).rows[0]?.exists === true;
28
- for (const statement of createTablesSql("postgres", postgresTableDefs, schema)) await tx.execute(sql.raw(statement));
29
- if (!metaExists) await tx.insert(this.tables["t_schema_meta"]).values([{
30
- fKey: "schema_version",
31
- fValue: String(2)
32
- }, {
33
- fKey: "application_id",
34
- fValue: String(SESSION_PERSISTENCE_SQLITE_APPLICATION_ID)
35
- }]).execute();
36
- const version = await this.readMeta(tx, "schema_version");
37
- const applicationId = await this.readMeta(tx, "application_id");
38
- if (version === void 0 || applicationId === void 0) throw new Error("session database has an unversioned schema or application identity");
39
- if (Number(version) !== 2) throw new Error(`session database has schema version ${version}, incompatible with this build (2)`);
40
- if (Number(applicationId) !== 1146308688) throw new Error(`session database has application id ${applicationId}, expected ${SESSION_PERSISTENCE_SQLITE_APPLICATION_ID}`);
41
36
  await tx.insert(this.tables["t_persistence_state"]).values({
42
37
  fSingleton: 1,
43
38
  fStoreId: randomUUID()
44
39
  }).onConflictDoNothing().execute();
45
- const storeId = (await tx.select({ fStoreId: this.tables["t_persistence_state"].fStoreId }).from(this.tables["t_persistence_state"]).where(eq(this.tables["t_persistence_state"].fSingleton, 1)).execute())[0]?.fStoreId;
46
- if (storeId === void 0 || storeId.length === 0) throw new Error("session database has no valid store identity");
47
- return storeId;
40
+ const id = (await tx.select({ fStoreId: this.tables["t_persistence_state"].fStoreId }).from(this.tables["t_persistence_state"]).where(eq(this.tables["t_persistence_state"].fSingleton, 1)).execute())[0]?.fStoreId;
41
+ if (id === void 0 || id.length === 0) throw new Error("session database has no valid store identity");
42
+ return id;
48
43
  });
49
44
  this.storeIdentity = `${this.options.identityBase}:store:${storeId}`;
50
45
  }
46
+ /** 标记 v2 baseline 已应用(迁移表 v1 结构:id/hash/created_at/name/applied_at)。 */
47
+ async baselineV2() {
48
+ const dir = readdirSync(postgresMigrationsDir).find((name) => name.endsWith("_v2_initial"));
49
+ if (dir === void 0) throw new Error("missing v2 baseline migration in drizzle/postgres");
50
+ await this.db.execute(sql`
51
+ CREATE SCHEMA IF NOT EXISTS drizzle
52
+ `);
53
+ await this.db.execute(sql`
54
+ CREATE TABLE IF NOT EXISTS drizzle.__drizzle_migrations (
55
+ id SERIAL PRIMARY KEY,
56
+ hash text NOT NULL,
57
+ created_at bigint,
58
+ name text,
59
+ applied_at timestamp with time zone DEFAULT now()
60
+ )
61
+ `);
62
+ await this.db.execute(sql`INSERT INTO drizzle.__drizzle_migrations (hash, created_at, name) VALUES ('baseline', 0, ${dir})`);
63
+ }
51
64
  async close() {
52
65
  await this.options.close();
53
66
  }
54
67
  async getSession(id) {
55
68
  return (await this.db.select().from(this.tables["t_sessions"]).where(eq(this.tables["t_sessions"].fSessionId, id)).execute())[0];
56
69
  }
57
- async getSeqMapRows(id) {
58
- return this.db.select({
59
- fSequence: this.tables["t_session_events"].fSequence,
60
- fOriginalSeq: this.tables["t_session_events"].fOriginalSeq
61
- }).from(this.tables["t_session_events"]).where(eq(this.tables["t_session_events"].fSessionId, id)).execute();
62
- }
63
70
  async getEventRows(id, fromSequence) {
64
71
  return (fromSequence === void 0 ? this.eventRows(this.db).where(eq(this.tables["t_session_events"].fSessionId, id)) : this.eventRows(this.db).where(and(eq(this.tables["t_session_events"].fSessionId, id), gte(this.tables["t_session_events"].fSequence, fromSequence)))).orderBy(this.tables["t_session_events"].fSequence).execute();
65
72
  }
@@ -80,8 +87,7 @@ var PostgresBackend = class PostgresBackend {
80
87
  updateHead: (id, headEventId, headSequence) => this.updateHead(tx, id, headEventId, headSequence),
81
88
  bumpRevision: (id) => this.bumpRevision(tx, id),
82
89
  deleteBridgeTail: (id, fromSequence) => this.deleteBridgeTail(tx, id, fromSequence),
83
- getPrevBridge: (id, sequence) => this.getPrevBridge(tx, id, sequence),
84
- getLastBridge: (id) => this.getLastBridge(tx, id)
90
+ getPrevBridge: (id, sequence) => this.getPrevBridge(tx, id, sequence)
85
91
  };
86
92
  }
87
93
  async readMeta(exec, key) {
@@ -138,17 +144,10 @@ var PostgresBackend = class PostgresBackend {
138
144
  fSequence: this.tables["t_session_events"].fSequence
139
145
  }).from(this.tables["t_session_events"]).where(and(eq(this.tables["t_session_events"].fSessionId, id), eq(this.tables["t_session_events"].fSequence, sequence))).execute())[0];
140
146
  }
141
- async getLastBridge(exec, id) {
142
- return (await exec.select({
143
- fEventId: this.tables["t_session_events"].fEventId,
144
- fSequence: this.tables["t_session_events"].fSequence
145
- }).from(this.tables["t_session_events"]).where(eq(this.tables["t_session_events"].fSessionId, id)).orderBy(desc(this.tables["t_session_events"].fSequence)).limit(1).execute())[0];
146
- }
147
147
  eventRows(exec) {
148
148
  return exec.select({
149
149
  fEventId: this.tables["t_session_events"].fEventId,
150
150
  fSequence: this.tables["t_session_events"].fSequence,
151
- fOriginalSeq: this.tables["t_session_events"].fOriginalSeq,
152
151
  fType: this.tables["t_events"].fType,
153
152
  fKind: this.tables["t_events"].fKind,
154
153
  fRole: this.tables["t_events"].fRole,
@@ -161,7 +160,343 @@ var PostgresBackend = class PostgresBackend {
161
160
  }
162
161
  };
163
162
  //#endregion
163
+ //#region src/legacy.ts
164
+ /** 重建 v0/v1 物理 header 记录(RDB 行 → 物理 JSON 对象)。 */
165
+ function physicalHeader(row) {
166
+ return {
167
+ type: "session",
168
+ version: row.fVersion,
169
+ id: row.fSessionId,
170
+ createdAt: row.fCreatedAt,
171
+ ...row.fCwd !== null ? { cwd: row.fCwd } : {},
172
+ ...row.fParentSession !== null ? { parentSession: row.fParentSession } : {},
173
+ ...row.fSeedLength !== null ? { seedLength: row.fSeedLength } : {},
174
+ ...row.fOrigin !== null ? { origin: row.fOrigin } : {},
175
+ delegationDepth: row.fDelegationDepth ?? 0
176
+ };
177
+ }
178
+ /** 重建一条 v0/v1 物理事件记录(桥接行 + 事件行 → 物理 JSON 对象)。 */
179
+ function physicalEvent(row) {
180
+ return {
181
+ type: row.fType,
182
+ seq: row.fSequence,
183
+ time: row.fCreatedAt,
184
+ data: JSON.parse(row.fData),
185
+ ...row.fSurfaceOp !== null ? { surfaceOp: JSON.parse(row.fSurfaceOp) } : {}
186
+ };
187
+ }
188
+ /**
189
+ * 把旧格式(v0/v1)行转换为 v2 逻辑事件与 header。
190
+ * @param row - 会话行(f_version < 2)。
191
+ * @param eventRows - 已扫描保留的桥接行(稠密 seq,torn tail 已剔除)。
192
+ * @returns v2 逻辑 header、继承前缀长度与事件。
193
+ * @throws 迁移链拒绝(格式损坏 / 无法迁移)时原样抛出。
194
+ */
195
+ function convertLegacyRows(row, eventRows) {
196
+ const restore = sessionFormatCatalog.createRestore(physicalHeader(row), {
197
+ recovery: "strict",
198
+ validation: "transformed"
199
+ });
200
+ for (const eventRow of eventRows) restore.decodeRow(physicalEvent(eventRow));
201
+ const artifact = restore.finish();
202
+ return {
203
+ meta: artifact.header,
204
+ inheritedEventCount: artifact.inheritedEventCount,
205
+ events: artifact.events
206
+ };
207
+ }
208
+ /** 会话行是否携带旧格式(v0/v1)数据。 */
209
+ function isLegacyVersion(version) {
210
+ return version < 2;
211
+ }
212
+ /**
213
+ * 迁移链拒绝后,把旧格式行直接当**当前格式**事件视图采用。
214
+ *
215
+ * 旧写入器只在建会话时落一次 header version,之后跟随上游演进继续追加
216
+ * 事件——同一个 log 因此可能是混合世代(v0 时代的行 + 新版本字段),不是
217
+ * 任何单一已发布格式,严格迁移链必然拒绝。这些行的数据形状(消息包装、
218
+ * 类型名)已经是当前格式,只需 header 版本归一到当前、按稠密 seq 读取;
219
+ * 事件数据的补全与 surface 修复由读取视图修复(repairReadView)负责。
220
+ *
221
+ * 调用方须在此之后执行 `validateStoredEvents`:真正属于旧格式的类型
222
+ * (`assistant/chunk` / `compact/*` 等)仍会被 fail-loud 拒绝。
223
+ *
224
+ * @param row - 会话行(f_version < 2)。
225
+ * @param eventRows - 该会话全部桥接行(按 f_sequence 升序)。
226
+ * @returns 当前格式 header(version 归一到 SESSION_FORMAT_VERSION)、收缩后的
227
+ * 继承前缀长度、稠密事件与可选 torn tail 起点。
228
+ * @throws scanRows 的已提交区损坏错误(seq gap / 不可解析行)。
229
+ */
230
+ function adoptLegacyRows(row, eventRows) {
231
+ const { preserved, tornFrom } = scanRows(eventRows, 0);
232
+ return {
233
+ meta: {
234
+ ...rowToMeta(row),
235
+ version: SESSION_FORMAT_VERSION
236
+ },
237
+ inheritedEventCount: Math.min(row.fSeedLength ?? 0, preserved.length),
238
+ events: preserved,
239
+ ...tornFrom !== void 0 ? { tornFrom } : {}
240
+ };
241
+ }
242
+ //#endregion
164
243
  //#region src/index.ts
244
+ /** 会话级写所有权与 live 路由簿记。 */
245
+ var RdbBackendTracker = class {
246
+ name;
247
+ /** 每个 id 的活跃 write handle;`null` 表示 claim 构造中。 */
248
+ writers = /* @__PURE__ */ new Map();
249
+ pending = /* @__PURE__ */ new Map();
250
+ openHandles = /* @__PURE__ */ new Set();
251
+ counter = 0;
252
+ constructor(name) {
253
+ this.name = name;
254
+ }
255
+ registerCreated(header, inheritedEventCount) {
256
+ if (this.writers.has(header.id)) throw new SessionAlreadyExistsError(header.id);
257
+ this.writers.set(header.id, null);
258
+ this.pending.set(header.id, {
259
+ header,
260
+ revision: SessionPersistenceRevision(`memory:${this.name}:${++this.counter}`),
261
+ inheritedEventCount,
262
+ cursor: 0,
263
+ everAppended: false
264
+ });
265
+ }
266
+ /** 更新 pending 的 cursor / everAppended(handle append 后同步)。 */
267
+ updatePending(id, cursor, everAppended) {
268
+ const entry = this.pending.get(id);
269
+ if (entry === void 0) return;
270
+ this.pending.set(id, {
271
+ ...entry,
272
+ cursor,
273
+ everAppended
274
+ });
275
+ }
276
+ claimWrite(id) {
277
+ if (this.writers.has(id)) throw new SessionAlreadyOwnedError(id);
278
+ this.writers.set(id, null);
279
+ }
280
+ releaseClaim(id) {
281
+ this.writers.delete(id);
282
+ }
283
+ pendingOf(id) {
284
+ return this.pending.get(id);
285
+ }
286
+ hasPending(id) {
287
+ return this.pending.has(id);
288
+ }
289
+ pendingEntries() {
290
+ return this.pending.entries();
291
+ }
292
+ materialized(id) {
293
+ this.pending.delete(id);
294
+ }
295
+ adopt(handle) {
296
+ this.openHandles.add(handle);
297
+ if (handle.access === "write") this.writers.set(handle.id, handle);
298
+ return handle;
299
+ }
300
+ release(handle, materialized) {
301
+ this.openHandles.delete(handle);
302
+ if (handle.access !== "write") return;
303
+ this.writers.delete(handle.id);
304
+ if (!materialized) this.pending.delete(handle.id);
305
+ }
306
+ writerOf(id) {
307
+ const writer = this.writers.get(id);
308
+ return writer === null ? void 0 : writer;
309
+ }
310
+ async flushAll() {
311
+ const errors = [];
312
+ for (const writer of this.writers.values()) {
313
+ if (writer === null) continue;
314
+ try {
315
+ await writer.drainLive();
316
+ await writer.flush();
317
+ } catch (error) {
318
+ if (error instanceof SessionHandleClosedError) continue;
319
+ errors.push(error);
320
+ }
321
+ }
322
+ if (errors.length > 0) throw new AggregateError(errors, `${this.name} flush failed`);
323
+ }
324
+ async closeAll() {
325
+ const errors = [];
326
+ for (const handle of this.openHandles) try {
327
+ await handle.close();
328
+ } catch (error) {
329
+ errors.push(error);
330
+ }
331
+ if (errors.length > 0) throw new AggregateError(errors, `${this.name} dispose failed`);
332
+ }
333
+ };
334
+ /** 一个打开会话的存储句柄:read / append / flush / close。 */
335
+ var RdbSessionHandle = class RdbSessionHandle {
336
+ persistence;
337
+ id;
338
+ header;
339
+ access;
340
+ state;
341
+ chain = Promise.resolve();
342
+ closing;
343
+ /** 稠密 next-seq(输入空间计数,与旧版 coordinator cursor 同语义)。 */
344
+ cursor;
345
+ materialized;
346
+ /** live 路由缓冲(上游 seq 事件,drain 时过滤 delta 后重编号稠密)。 */
347
+ buffered = [];
348
+ batchTimer;
349
+ drainPaused = false;
350
+ draining;
351
+ /** write open 时发现的 torn tail 起点(append 前先截断)。 */
352
+ tornTruncateTo;
353
+ /** 是否调用过 append(即使全 delta 未落库)——close 时保留 pending。 */
354
+ everAppended = false;
355
+ constructor(persistence, id, header, access, state) {
356
+ this.persistence = persistence;
357
+ this.id = id;
358
+ this.header = header;
359
+ this.access = access;
360
+ this.state = state;
361
+ this.cursor = state.cursor;
362
+ this.materialized = state.materialized;
363
+ this.tornTruncateTo = state.tornTruncateTo;
364
+ }
365
+ get inheritedEventCount() {
366
+ return this.state.inheritedEventCount;
367
+ }
368
+ /** 输入空间 cursor(下一个待落库的上游 seq)。 */
369
+ get cursorValue() {
370
+ return this.cursor;
371
+ }
372
+ async read(offset = 0, length = Number.MAX_SAFE_INTEGER, options) {
373
+ this.assertOpen("read");
374
+ if (!Number.isSafeInteger(offset) || offset < 0) throw new TypeError(`read offset must be a non-negative safe integer, got ${String(offset)}`);
375
+ if (!Number.isSafeInteger(length) || length < 0) throw new TypeError(`read length must be a non-negative safe integer, got ${String(length)}`);
376
+ options?.signal?.throwIfAborted();
377
+ const log = await this.persistence.readLog(this.id, {}, options?.signal);
378
+ if (log === void 0) {
379
+ if (this.persistence.tracker.hasPending(this.id)) return {
380
+ eventState: "detached",
381
+ events: []
382
+ };
383
+ throw new SessionPersistenceNotFoundError(this.id);
384
+ }
385
+ repairReadView(log.events);
386
+ return {
387
+ eventState: "detached",
388
+ events: log.events.slice(offset, offset + length)
389
+ };
390
+ }
391
+ async append(events, options) {
392
+ this.assertOpen("append");
393
+ const batch = materializeAppendBatch(events);
394
+ return this.run("append", async () => {
395
+ options?.signal?.throwIfAborted();
396
+ if (this.access !== "write") throw new SessionReadOnlyError(this.id, "append");
397
+ if (batch.length === 0) return;
398
+ this.everAppended = true;
399
+ assertContiguous(this.id, batch, this.cursor);
400
+ await this.persistence.appendBatch(this.header, this.state.inheritedEventCount, batch, this.tornTruncateTo);
401
+ this.tornTruncateTo = void 0;
402
+ this.cursor += batch.length;
403
+ this.materialized = true;
404
+ this.persistence.tracker.updatePending(this.id, this.cursor, true);
405
+ });
406
+ }
407
+ async flush(options) {
408
+ return this.run("flush", async () => {
409
+ options?.signal?.throwIfAborted();
410
+ if (this.access !== "write") throw new SessionReadOnlyError(this.id, "flush");
411
+ if (this.materialized) return;
412
+ await this.persistence.materializeEmpty(this.header, this.state.inheritedEventCount);
413
+ this.materialized = true;
414
+ });
415
+ }
416
+ close() {
417
+ return this.closing ??= (async () => {
418
+ let drainFailure;
419
+ for (;;) {
420
+ try {
421
+ await this.drainLive();
422
+ } catch (error) {
423
+ drainFailure = error;
424
+ break;
425
+ }
426
+ await this.chain;
427
+ if (this.buffered.length === 0) break;
428
+ }
429
+ await this.chain;
430
+ const failures = [];
431
+ if (drainFailure !== void 0) failures.push(drainFailure instanceof Error ? drainFailure : new Error(JSON.stringify(drainFailure)));
432
+ this.persistence.tracker.release(this, this.materialized || this.everAppended);
433
+ if (failures.length > 1) throw new AggregateError(failures, `session "${this.id}": close failed to drain`);
434
+ if (failures[0] !== void 0) throw failures[0];
435
+ })();
436
+ }
437
+ [Symbol.asyncDispose]() {
438
+ return this.close();
439
+ }
440
+ /** live 路由:缓冲一个已发布事件(持久化自有副本),并启动批量窗口。 */
441
+ enqueueLive(event, reportBackgroundFailure) {
442
+ this.buffered.push(structuredClone(event));
443
+ if (this.batchTimer !== void 0 || this.drainPaused) return;
444
+ this.batchTimer = setTimeout(() => {
445
+ this.batchTimer = void 0;
446
+ this.drainLive().catch(reportBackgroundFailure);
447
+ }, RdbSessionHandle.LIVE_WRITE_BATCH_MAX_DELAY_MS);
448
+ }
449
+ /** rewind 截断后对齐 handle 的稠密 cursor 与继承前缀(DB 已截断)。 */
450
+ resetAfterRewind(cursor, inheritedEventCount) {
451
+ this.cursor = cursor;
452
+ if (inheritedEventCount !== void 0) this.state.inheritedEventCount = SessionLogOffset(inheritedEventCount);
453
+ }
454
+ /** 排空 live 缓冲(过滤 delta + 重编号稠密 + append)。 */
455
+ drainLive() {
456
+ return this.draining ??= this.drainBuffered().finally(() => {
457
+ this.draining = void 0;
458
+ });
459
+ }
460
+ async drainBuffered() {
461
+ if (this.batchTimer !== void 0) {
462
+ clearTimeout(this.batchTimer);
463
+ this.batchTimer = void 0;
464
+ }
465
+ this.drainPaused = false;
466
+ while (this.buffered.length > 0) await this.enqueueChain(async () => {
467
+ const batch = this.buffered.splice(0);
468
+ try {
469
+ const fresh = batch.filter((event) => event.seq >= this.cursor);
470
+ if (fresh.length === 0) return;
471
+ for (const [index, event] of fresh.entries()) if (event.seq !== this.cursor + index) throw new Error(`append seq mismatch for "${this.id}": expected ${this.cursor + index} at index ${index}, got ${event.seq}`);
472
+ await this.persistence.appendBatch(this.header, this.state.inheritedEventCount, fresh, this.tornTruncateTo);
473
+ this.tornTruncateTo = void 0;
474
+ this.cursor += fresh.length;
475
+ this.materialized = true;
476
+ } catch (error) {
477
+ this.buffered = batch.concat(this.buffered);
478
+ this.drainPaused = true;
479
+ throw error;
480
+ }
481
+ });
482
+ }
483
+ enqueueChain(op) {
484
+ const next = this.chain.then(op);
485
+ this.chain = next.catch(() => {});
486
+ return next;
487
+ }
488
+ async run(operation, op) {
489
+ this.assertOpen(operation);
490
+ return this.enqueueChain(async () => {
491
+ this.assertOpen(operation);
492
+ return op();
493
+ });
494
+ }
495
+ assertOpen(operation) {
496
+ if (this.closing !== void 0) throw new SessionHandleClosedError(this.id, operation);
497
+ }
498
+ static LIVE_WRITE_BATCH_MAX_DELAY_MS = 200;
499
+ };
165
500
  var SessionPersistenceRdb = class SessionPersistenceRdb extends SessionPersistence {
166
501
  config;
167
502
  static inject = ["sessions", "settings"];
@@ -182,29 +517,15 @@ var SessionPersistenceRdb = class SessionPersistenceRdb extends SessionPersisten
182
517
  })]);
183
518
  static settingsNs = "session-rdb";
184
519
  name = "session-rdb";
185
- supportsRawArtifacts = true;
186
- async readRaw(id, signal) {
187
- signal?.throwIfAborted();
188
- await this.ready;
189
- signal?.throwIfAborted();
190
- const log = await this.readLog(id, {}, signal);
191
- if (log === void 0) return void 0;
192
- repairSurfaceOps(log.events);
193
- recomputeReplaceProvenance(log.events);
194
- const inheritedEventCount = Math.min(log.inheritedEventCount, log.events.length);
195
- return {
196
- meta: log.meta,
197
- inheritedEventCount: SessionLogOffset(inheritedEventCount),
198
- filename: "session.jsonl",
199
- content: toJsonlArtifact(log.meta, inheritedEventCount, log.events)
200
- };
201
- }
520
+ tracker = new RdbBackendTracker(this.name);
202
521
  backend;
203
522
  storeIdentity;
204
523
  ready;
205
- coordinator;
206
524
  writeGuard = new WriteGuard();
207
525
  reuseEventIds = /* @__PURE__ */ new Map();
526
+ /** live 路由:session/created 后 handle 就绪前的缓冲。 */
527
+ liveBuffers = /* @__PURE__ */ new Map();
528
+ liveReady = /* @__PURE__ */ new Map();
208
529
  constructor(ctx, config, injectedBackend) {
209
530
  let resolved = config;
210
531
  const settings = ctx.reflect.get("settings");
@@ -220,7 +541,7 @@ var SessionPersistenceRdb = class SessionPersistenceRdb extends SessionPersisten
220
541
  this.config = resolved;
221
542
  this.backend = injectedBackend ?? createBackend(resolved);
222
543
  this.ready = this.init();
223
- this.coordinator = new PersistenceCoordinator(this.ctx, this);
544
+ this.installLiveRouting(ctx);
224
545
  new SessionBranchRdb(this.ctx);
225
546
  registerSessionImport(this.ctx, this);
226
547
  }
@@ -228,103 +549,163 @@ var SessionPersistenceRdb = class SessionPersistenceRdb extends SessionPersisten
228
549
  await this.backend.open();
229
550
  this.storeIdentity = this.backend.storeIdentity;
230
551
  }
231
- locate(_meta) {}
232
- create(meta, inheritedEventCount) {
233
- return this.coordinator.create(meta, inheritedEventCount === void 0 ? void 0 : SessionLogOffset(inheritedEventCount));
234
- }
235
- append(id, events) {
236
- return this.coordinator.append(id, events);
237
- }
238
- load(id) {
239
- return this.coordinator.load(id);
240
- }
241
- inspect(id, signal) {
242
- return this.coordinator.inspect(id, signal);
243
- }
244
- readFrom(id, fromSeq, signal) {
245
- return this.coordinator.readFrom(id, fromSeq, signal);
552
+ async create(header, options) {
553
+ options?.signal?.throwIfAborted();
554
+ const snapshot = materializeCreateHeader(header);
555
+ if (snapshot.isSeeded && options?.inheritedEventCount === void 0) throw new TypeError("seeded session metadata requires an inherited event count");
556
+ const inheritedEventCount = SessionLogOffset(options?.inheritedEventCount ?? 0);
557
+ if (!snapshot.isSeeded && inheritedEventCount !== 0) throw new TypeError("unseeded session metadata inherited event count must be 0");
558
+ await this.ready;
559
+ options?.signal?.throwIfAborted();
560
+ if (this.tracker.hasPending(snapshot.id) || await this.backend.getSession(snapshot.id) !== void 0) throw new SessionAlreadyExistsError(snapshot.id);
561
+ this.tracker.registerCreated(snapshot, inheritedEventCount);
562
+ return this.tracker.adopt(new RdbSessionHandle(this, snapshot.id, snapshot, "write", {
563
+ cursor: 0,
564
+ materialized: false,
565
+ inheritedEventCount
566
+ }));
246
567
  }
247
- borrowSession(id, signal) {
248
- return this.coordinator.borrowSession(id, signal);
568
+ async open(id, access, options) {
569
+ options?.signal?.throwIfAborted();
570
+ await this.ready;
571
+ options?.signal?.throwIfAborted();
572
+ const pending = this.tracker.pendingOf(id);
573
+ if (access === "read") {
574
+ if (pending !== void 0) return this.tracker.adopt(new RdbSessionHandle(this, id, pending.header, "read", {
575
+ cursor: 0,
576
+ materialized: false,
577
+ inheritedEventCount: pending.inheritedEventCount
578
+ }));
579
+ const log = await this.readLog(id, {}, options?.signal);
580
+ if (log === void 0) throw new SessionPersistenceNotFoundError(id);
581
+ validateStoredEvents(log.meta, log.events);
582
+ return this.tracker.adopt(new RdbSessionHandle(this, id, log.meta, "read", {
583
+ cursor: log.events.length,
584
+ materialized: true,
585
+ inheritedEventCount: SessionLogOffset(log.inheritedEventCount)
586
+ }));
587
+ }
588
+ this.tracker.claimWrite(id);
589
+ try {
590
+ if (pending !== void 0) return this.tracker.adopt(new RdbSessionHandle(this, id, pending.header, "write", {
591
+ cursor: pending.cursor,
592
+ materialized: false,
593
+ inheritedEventCount: pending.inheritedEventCount
594
+ }));
595
+ const log = await this.readLog(id, {}, options?.signal);
596
+ if (log === void 0) throw new SessionPersistenceNotFoundError(id);
597
+ validateStoredEvents(log.meta, log.events);
598
+ if (log.migrated && log.events.length !== log.storedCount) await this.rewriteMigratedLog(id, log);
599
+ this.writeGuard.confirmHead(id, log.events.at(-1)?.seq ?? -1);
600
+ return this.tracker.adopt(new RdbSessionHandle(this, id, log.meta, "write", {
601
+ cursor: log.events.length,
602
+ materialized: true,
603
+ inheritedEventCount: SessionLogOffset(log.inheritedEventCount),
604
+ ...log.tornFrom !== void 0 ? { tornTruncateTo: log.tornFrom } : {}
605
+ }));
606
+ } catch (error) {
607
+ this.tracker.releaseClaim(id);
608
+ throw error;
609
+ }
249
610
  }
250
- loadStored(id, signal) {
251
- return this.readPrefix(id, signal);
611
+ flush() {
612
+ return this.tracker.flushAll();
252
613
  }
253
- async loadStoredFrom(id, fromSeq, signal) {
254
- const log = await this.readLog(id, { fromSeq }, signal);
255
- if (log === void 0) return void 0;
256
- return {
257
- meta: log.meta,
258
- inheritedEventCount: SessionLogOffset(log.inheritedEventCount),
259
- events: log.events
614
+ async stat(id, options) {
615
+ options?.signal?.throwIfAborted();
616
+ await this.ready;
617
+ options?.signal?.throwIfAborted();
618
+ const pending = this.tracker.pendingOf(id);
619
+ if (pending !== void 0) return {
620
+ header: pending.header,
621
+ revision: pending.revision
260
622
  };
261
- }
262
- async readPrefix(id, signal) {
263
- const log = await this.readLog(id, {}, signal);
264
- if (log === void 0) {
265
- this.writeGuard.confirmHead(id, -1);
266
- return;
267
- }
268
- this.writeGuard.confirmHead(id, log.events.at(-1)?.seq ?? -1);
269
- recomputeReplaceProvenance(log.events);
270
- repairOrphanInboxSplices(log.events);
623
+ const row = await this.backend.getSession(id);
624
+ if (row === void 0) return void 0;
271
625
  return {
272
- meta: log.meta,
273
- inheritedEventCount: SessionLogOffset(log.inheritedEventCount),
274
- events: log.events,
275
- revision: SessionPersistenceRevision(`${this.storeIdentity}:incarnation:${log.incarnation}:revision:${log.revision}`),
276
- ...log.tornFrom !== void 0 ? { tornMarker: log.tornFrom } : {}
626
+ header: rowToMeta(row),
627
+ revision: this.rowRevision(row)
277
628
  };
278
629
  }
279
- async readStoredRevision(id, signal) {
630
+ async list(options) {
631
+ const signal = options?.signal;
632
+ const snapshots = [];
633
+ const listed = /* @__PURE__ */ new Set();
634
+ for (const [id, pending] of this.tracker.pendingEntries()) {
635
+ snapshots.push({
636
+ header: pending.header,
637
+ revision: pending.revision
638
+ });
639
+ listed.add(id);
640
+ }
280
641
  signal?.throwIfAborted();
281
642
  await this.ready;
282
643
  signal?.throwIfAborted();
283
- const row = await this.backend.getSession(id);
284
- if (row === void 0) return void 0;
285
- return SessionPersistenceRevision(`${this.storeIdentity}:incarnation:${row.fIncarnation}:revision:${row.fRevision}`);
644
+ const rows = await this.backend.listSessions();
645
+ signal?.throwIfAborted();
646
+ for (const row of rows) {
647
+ if (listed.has(row.fSessionId)) continue;
648
+ snapshots.push({
649
+ header: rowToMeta(row),
650
+ revision: this.rowRevision(row)
651
+ });
652
+ }
653
+ return snapshots;
286
654
  }
287
- async readLog(id, options = {}, signal) {
655
+ /** 导出 artifact(jsonl v2 文本),视图只读不落库。 */
656
+ async readRaw(id, signal) {
288
657
  signal?.throwIfAborted();
289
658
  await this.ready;
290
659
  signal?.throwIfAborted();
291
- const row = await this.backend.getSession(id);
292
- if (row === void 0) return void 0;
293
- const meta = rowToMeta(row);
294
- let eventRows;
295
- let seqMap;
296
- if (options.fromSeq === void 0) {
297
- eventRows = await this.backend.getEventRows(id);
298
- seqMap = buildSeqMap(eventRows);
299
- } else {
300
- eventRows = await this.backend.getEventRows(id, options.fromSeq);
301
- const seqRows = await this.backend.getSeqMapRows(id);
302
- seqMap = buildSeqMap(seqRows);
303
- }
304
- signal?.throwIfAborted();
305
- const { preserved, tornFrom } = scanRows(eventRows, options.fromSeq ?? 0, seqMap);
660
+ const log = await this.readLog(id, {}, signal);
661
+ if (log === void 0) return void 0;
662
+ repairReadView(log.events);
663
+ const inheritedEventCount = Math.min(log.inheritedEventCount, log.events.length);
306
664
  return {
307
- meta,
308
- inheritedEventCount: row.fSeedLength ?? 0,
309
- events: preserved,
310
- incarnation: row.fIncarnation,
311
- revision: row.fRevision,
312
- ...tornFrom !== void 0 ? { tornFrom } : {}
665
+ meta: log.meta,
666
+ inheritedEventCount,
667
+ filename: "session.jsonl",
668
+ content: toJsonlArtifact(log.meta, inheritedEventCount, log.events)
313
669
  };
314
670
  }
315
- async appendBatch(storage, events, _isMaterialized) {
671
+ /** 空会话 materialize(flush 的持久化屏障)。 */
672
+ async materializeEmpty(meta, inheritedEventCount) {
316
673
  await this.ready;
317
- const persisted = events.filter(isPersistedEvent);
318
- if (persisted.length === 0) return;
319
- const meta = storage.meta;
674
+ await this.backend.transaction(async (tx) => {
675
+ await tx.upsertSession({
676
+ meta,
677
+ inheritedEventCount
678
+ }, randomUUID());
679
+ await tx.bumpRevision(meta.id);
680
+ });
681
+ this.tracker.materialized(meta.id);
682
+ this.writeGuard.confirmHead(meta.id, -1);
683
+ }
684
+ /** 原样 append 落库(handle 已校验 contiguity;torn tail 先截断)。
685
+ * 与上游 JSONL 一致:ignorable 事件原样存储,不做过滤。写路径校验 v2
686
+ * 形状(fail-closed):未知类型(非 ignorable)与非法消息形状拒绝入库;
687
+ * 旧格式(v0/v1)数据只在读取时经 legacy 转换链动态转换,不落新库。 */
688
+ async appendBatch(meta, inheritedEventCount, events, tornTruncateTo) {
689
+ await this.ready;
690
+ if (events.length === 0) return false;
691
+ validateStoredEvents(meta, [...events]);
320
692
  const reuse = this.reuseEventIds.get(meta.id);
321
693
  if (reuse !== void 0) this.reuseEventIds.delete(meta.id);
322
694
  let confirmedHead = -1;
323
695
  await this.backend.transaction(async (tx) => {
324
- await tx.upsertSession(storage, randomUUID());
696
+ if (tornTruncateTo !== void 0) {
697
+ await tx.deleteBridgeTail(meta.id, tornTruncateTo);
698
+ const prev = await tx.getPrevBridge(meta.id, tornTruncateTo - 1);
699
+ if (prev === void 0) await tx.updateHead(meta.id, "", -1);
700
+ else await tx.updateHead(meta.id, prev.fEventId, prev.fSequence);
701
+ }
702
+ await tx.upsertSession({
703
+ meta,
704
+ inheritedEventCount
705
+ }, randomUUID());
325
706
  const head = await tx.getHead(meta.id);
326
707
  this.writeGuard.assertNoConcurrentWriter(meta.id, head.fHeadSequence);
327
- const { headEventId, headSequence } = await appendEventTail(tx, meta, persisted, {
708
+ const { headEventId, headSequence } = await appendEventTail(tx, meta, events, {
328
709
  parentId: head.fHeadEventId,
329
710
  nextSeq: head.fHeadSequence + 1
330
711
  }, reuse);
@@ -333,51 +714,162 @@ var SessionPersistenceRdb = class SessionPersistenceRdb extends SessionPersisten
333
714
  confirmedHead = headSequence;
334
715
  });
335
716
  this.writeGuard.confirmHead(meta.id, confirmedHead);
717
+ this.tracker.materialized(meta.id);
718
+ return true;
336
719
  }
337
- async commitRepair(storage, tornMarker, closers) {
720
+ /** 读取一个会话的稠密 log(含 torn tail 检测,不含修复改写)。 */
721
+ async readLog(id, options = {}, signal) {
722
+ signal?.throwIfAborted();
338
723
  await this.ready;
339
- const meta = storage.meta;
340
- const persistedClosers = closers.filter(isPersistedEvent);
341
- if (tornMarker === void 0 && persistedClosers.length === 0) return;
724
+ signal?.throwIfAborted();
725
+ const row = await this.backend.getSession(id);
726
+ if (row === void 0) return void 0;
727
+ const meta = rowToMeta(row);
728
+ const eventRows = options.fromSeq === void 0 ? await this.backend.getEventRows(id) : await this.backend.getEventRows(id, options.fromSeq);
729
+ signal?.throwIfAborted();
730
+ if (isLegacyVersion(row.fVersion)) try {
731
+ const converted = convertLegacyRows(row, eventRows);
732
+ return {
733
+ meta: converted.meta,
734
+ inheritedEventCount: converted.inheritedEventCount,
735
+ events: converted.events,
736
+ incarnation: row.fIncarnation,
737
+ revision: row.fRevision,
738
+ storedCount: eventRows.length,
739
+ migrated: true
740
+ };
741
+ } catch (error) {
742
+ this.ctx.logger.warn(`session-rdb: session "${id}" is not a single released format; adopting its stored rows as current-format data (${error instanceof Error ? error.message : String(error)})`);
743
+ const adopted = adoptLegacyRows(row, eventRows);
744
+ return {
745
+ meta: adopted.meta,
746
+ inheritedEventCount: adopted.inheritedEventCount,
747
+ events: adopted.events,
748
+ incarnation: row.fIncarnation,
749
+ revision: row.fRevision,
750
+ storedCount: eventRows.length,
751
+ migrated: false,
752
+ ...adopted.tornFrom !== void 0 ? { tornFrom: adopted.tornFrom } : {}
753
+ };
754
+ }
755
+ const { preserved, tornFrom } = scanRows(eventRows, options.fromSeq ?? 0);
756
+ return {
757
+ meta,
758
+ inheritedEventCount: row.fSeedLength ?? 0,
759
+ events: preserved,
760
+ incarnation: row.fIncarnation,
761
+ revision: row.fRevision,
762
+ storedCount: eventRows.length,
763
+ migrated: false,
764
+ ...tornFrom !== void 0 ? { tornFrom } : {}
765
+ };
766
+ }
767
+ /**
768
+ * 把迁移链读出的 v2 视图整体落库(旧格式会话写打开时的一次性迁移)。
769
+ *
770
+ * 迁移链会生成/合并事件(end-seed / attempt / chunk 合并),事件 seq 空间
771
+ * 与存储桥接行数不再相等;写路径以存储 head 为锚点重编号,二者不一致会让
772
+ * append 撞上已有行。这里在同一事务内删光本会话桥接行、按迁移视图重建
773
+ * (新事件行,完整信封),并更新 head 与 revision;旧事件行保留(可能被
774
+ * fork 子会话引用,孤儿由惰性 GC 处理)。
775
+ */
776
+ async rewriteMigratedLog(id, log) {
342
777
  await this.backend.transaction(async (tx) => {
343
- if (tornMarker !== void 0) {
344
- await tx.deleteBridgeTail(meta.id, tornMarker);
345
- const prev = await tx.getPrevBridge(meta.id, tornMarker - 1);
346
- if (prev === void 0) await tx.updateHead(meta.id, "", -1);
347
- else await tx.updateHead(meta.id, prev.fEventId, prev.fSequence);
348
- }
349
- if (persistedClosers.length > 0) {
350
- const last = await tx.getLastBridge(meta.id);
351
- const { headEventId, headSequence } = await appendEventTail(tx, meta, persistedClosers, {
352
- parentId: last?.fEventId ?? "",
353
- nextSeq: (last?.fSequence ?? -1) + 1
354
- });
355
- await tx.updateHead(meta.id, headEventId, headSequence);
356
- }
357
- await tx.bumpRevision(meta.id);
778
+ await tx.deleteBridgeTail(id, 0);
779
+ await tx.upsertSession({
780
+ meta: log.meta,
781
+ inheritedEventCount: SessionLogOffset(log.inheritedEventCount)
782
+ }, randomUUID());
783
+ const { headEventId, headSequence } = await appendEventTail(tx, log.meta, log.events, {
784
+ parentId: "",
785
+ nextSeq: 0
786
+ });
787
+ await tx.updateHead(id, headEventId, headSequence);
788
+ await tx.bumpRevision(id);
358
789
  });
359
- const row = await this.backend.getSession(meta.id);
360
- this.writeGuard.confirmHead(meta.id, row?.fHeadSequence ?? -1);
361
790
  }
362
- async list(signal) {
791
+ async listSnapshots(signal) {
363
792
  signal?.throwIfAborted();
364
793
  await this.ready;
365
794
  signal?.throwIfAborted();
795
+ const snapshots = [];
796
+ const listed = /* @__PURE__ */ new Set();
797
+ for (const [id, pending] of this.tracker.pendingEntries()) {
798
+ snapshots.push({
799
+ header: pending.header,
800
+ revision: pending.revision,
801
+ inheritedEventCount: pending.inheritedEventCount
802
+ });
803
+ listed.add(id);
804
+ }
366
805
  const rows = await this.backend.listSessions();
367
806
  signal?.throwIfAborted();
368
- return rows.map(rowToMeta);
807
+ for (const row of rows) {
808
+ if (listed.has(row.fSessionId)) continue;
809
+ snapshots.push({
810
+ header: rowToMeta(row),
811
+ revision: this.rowRevision(row),
812
+ inheritedEventCount: row.fSeedLength ?? 0
813
+ });
814
+ }
815
+ return snapshots;
369
816
  }
370
- async listSnapshots(signal) {
817
+ async readStoredRevision(id, signal) {
371
818
  signal?.throwIfAborted();
372
819
  await this.ready;
373
820
  signal?.throwIfAborted();
374
- const rows = await this.backend.listSessions();
375
- signal?.throwIfAborted();
376
- return rows.map((row) => ({
377
- header: rowToMeta(row),
378
- revision: SessionPersistenceRevision(`${this.storeIdentity}:incarnation:${row.fIncarnation}:revision:${row.fRevision}`),
379
- inheritedEventCount: row.fSeedLength ?? 0
380
- }));
821
+ const row = await this.backend.getSession(id);
822
+ if (row === void 0) return void 0;
823
+ return this.rowRevision(row);
824
+ }
825
+ /** 便捷:create + append + close(测试与导入路径共用)。 */
826
+ async createAndAppend(header, events, inheritedEventCount) {
827
+ const handle = await this.create(header, inheritedEventCount === void 0 ? void 0 : { inheritedEventCount: SessionLogOffset(inheritedEventCount) });
828
+ try {
829
+ if (events.length > 0) await handle.append(events);
830
+ } finally {
831
+ await handle.close();
832
+ }
833
+ }
834
+ /** 便捷:open(read) + read 全量 + close。 */
835
+ async load(id, signal) {
836
+ const handle = await this.open(id, "read", signal === void 0 ? void 0 : { signal });
837
+ try {
838
+ const { events } = await handle.read(0, void 0, signal === void 0 ? void 0 : { signal });
839
+ const row = await this.backend.getSession(id);
840
+ if (row === void 0) throw new SessionPersistenceNotFoundError(id);
841
+ if (isLegacyVersion(row.fVersion)) return {
842
+ meta: handle.header,
843
+ inheritedEventCount: handle.inheritedEventCount,
844
+ events
845
+ };
846
+ return {
847
+ meta: rowToMeta(row),
848
+ inheritedEventCount: SessionLogOffset(row.fSeedLength ?? 0),
849
+ events
850
+ };
851
+ } finally {
852
+ await handle.close();
853
+ }
854
+ }
855
+ /** 便捷:open(write) + append + close。 */
856
+ async append(id, events) {
857
+ const handle = await this.open(id, "write");
858
+ try {
859
+ await handle.append(events);
860
+ } finally {
861
+ await handle.close();
862
+ }
863
+ }
864
+ /** 便捷:readFrom(稠密后缀)。 */
865
+ async readFrom(id, fromSeq, signal) {
866
+ const log = await this.readLog(id, { fromSeq }, signal);
867
+ if (log === void 0) throw new SessionPersistenceNotFoundError(id);
868
+ return {
869
+ meta: log.meta,
870
+ inheritedEventCount: log.inheritedEventCount,
871
+ events: log.events
872
+ };
381
873
  }
382
874
  async close() {
383
875
  await this.ready;
@@ -390,17 +882,117 @@ var SessionPersistenceRdb = class SessionPersistenceRdb extends SessionPersisten
390
882
  return {
391
883
  backend: this.backend,
392
884
  writeGuard: this.writeGuard,
393
- create: (meta, inheritedEventCount) => this.create(meta, inheritedEventCount),
885
+ create: (meta, inheritedEventCount) => this.create(meta, inheritedEventCount === void 0 ? void 0 : { inheritedEventCount: SessionLogOffset(inheritedEventCount) }),
394
886
  append: (id, events) => this.append(id, events),
395
887
  load: (id) => this.load(id),
396
- inspect: (id, signal) => this.inspect(id, signal),
888
+ inspect: (id, signal) => this.load(id, signal),
397
889
  readFrom: (id, fromSeq, signal) => this.readFrom(id, fromSeq, signal),
398
890
  listSnapshots: (signal) => this.listSnapshots(signal),
399
891
  readStoredRevision: (id, signal) => this.readStoredRevision(id, signal),
400
892
  registerReuseEventIds: (childId, map) => this.registerReuseEventIds(childId, map)
401
893
  };
402
894
  }
895
+ rowRevision(row) {
896
+ return SessionPersistenceRevision(`${this.storeIdentity}:incarnation:${row.fIncarnation}:revision:${row.fRevision}`);
897
+ }
898
+ installLiveRouting(ctx) {
899
+ ctx.on("session/created", (session) => {
900
+ this.liveBuffers.set(session.id, []);
901
+ const ready = this.ensureLiveHandle(session);
902
+ this.liveReady.set(session.id, ready);
903
+ ready.catch((error) => {
904
+ ctx.logger.warn(`session-rdb: live session "${session.id}" persistence init failed: ${String(error)}`);
905
+ });
906
+ });
907
+ for (const session of ctx.sessions.list()) {
908
+ this.liveBuffers.set(session.id, []);
909
+ const ready = this.ensureLiveHandle(session);
910
+ this.liveReady.set(session.id, ready);
911
+ ready.catch((error) => {
912
+ ctx.logger.warn(`session-rdb: live session "${session.id}" persistence init failed: ${String(error)}`);
913
+ });
914
+ }
915
+ ctx.on("session/event", (session, event) => {
916
+ const handle = this.tracker.writerOf(session.id);
917
+ if (handle !== void 0) {
918
+ handle.enqueueLive(event, (error) => {
919
+ ctx.logger.warn(`session-rdb: background write for session "${session.id}" failed (buffered events retained): ${String(error)}`);
920
+ });
921
+ return;
922
+ }
923
+ this.liveBuffers.get(session.id)?.push(structuredClone(event));
924
+ });
925
+ ctx.on("session/flush", (session) => {
926
+ const handle = this.tracker.writerOf(session.id);
927
+ if (handle === void 0) {
928
+ const ready = this.liveReady.get(session.id);
929
+ if (ready === void 0) return void 0;
930
+ return ready.then(() => {
931
+ const settled = this.tracker.writerOf(session.id);
932
+ if (settled === void 0) return void 0;
933
+ return settled.drainLive().then(() => settled.flush());
934
+ });
935
+ }
936
+ return handle.drainLive().then(() => handle.flush());
937
+ });
938
+ ctx.on("session/disposed", (session) => {
939
+ const ready = this.liveReady.get(session.id);
940
+ this.liveBuffers.delete(session.id);
941
+ this.liveReady.delete(session.id);
942
+ const closeHandle = () => {
943
+ const handle = this.tracker.writerOf(session.id);
944
+ if (handle === void 0) return;
945
+ handle.close().catch((error) => {
946
+ ctx.logger.warn(`session-rdb: final drain for session "${session.id}" failed: ${String(error)}`);
947
+ });
948
+ };
949
+ if (ready === void 0) {
950
+ closeHandle();
951
+ return;
952
+ }
953
+ ready.then(closeHandle, closeHandle);
954
+ });
955
+ ctx.effect(() => async () => {
956
+ await Promise.allSettled(this.liveReady.values());
957
+ await this.tracker.closeAll();
958
+ await this.close();
959
+ }, `${this.name} open handles`);
960
+ }
961
+ /** session/created 后为 live 会话建立 write handle(create 或 adopt)。 */
962
+ async ensureLiveHandle(session) {
963
+ const id = session.header.id;
964
+ if (this.tracker.writerOf(id) !== void 0) return;
965
+ await this.ready;
966
+ const stored = await this.readLog(id, {});
967
+ let handle;
968
+ if (stored === void 0) {
969
+ handle = await this.create(session.header, { inheritedEventCount: session.inheritedEventCount });
970
+ const seed = session.snapshotEvents();
971
+ if (seed.length > 0) await handle.append(seed);
972
+ } else {
973
+ if (stored.meta.cwd !== session.header.cwd) throw new Error(`session "${id}" is already persisted at a different cwd (persisted: ${String(stored.meta.cwd)}, live: ${String(session.header.cwd)}) (id collision)`);
974
+ if (stored.inheritedEventCount !== session.inheritedEventCount) throw new Error(`session "${id}" is already persisted with a different inherited event count (id collision)`);
975
+ assertVersion(stored.meta);
976
+ repairReadView(stored.events);
977
+ const seed = session.snapshotEvents();
978
+ if (!seedCoversPrefix(seed, stored.events)) throw new Error(`session "${id}" already has a persisted log on disk that does not match this live session (id collision)`);
979
+ handle = await this.open(id, "write");
980
+ const suffix = seed.slice(stored.events.length);
981
+ if (suffix.length > 0) await handle.append(suffix);
982
+ }
983
+ const buffered = this.liveBuffers.get(id);
984
+ if (buffered !== void 0 && buffered.length > 0) {
985
+ this.liveBuffers.set(id, []);
986
+ for (const event of buffered) handle.enqueueLive(event, () => {});
987
+ }
988
+ }
403
989
  };
990
+ function seedCoversPrefix(seed, prefix) {
991
+ return prefix.length <= seed.length && prefix.every((event, index) => {
992
+ const seedEvent = seed[index];
993
+ return seedEvent !== void 0 && JSON.stringify(seedEvent) === JSON.stringify(event);
994
+ });
995
+ }
404
996
  function createBackend(config) {
405
997
  if (config.type === "sqlite") return new SqliteBackend({
406
998
  path: config.path,
@@ -431,6 +1023,7 @@ async function appendEventTail(tx, meta, events, anchor, reuse) {
431
1023
  const eventId = reusedId ?? randomUUID();
432
1024
  if (reusedId === void 0) {
433
1025
  const { kind, role, name, actionId } = eventDimensions(event);
1026
+ const { data, surfaceOp: _surfaceOp, sourceEventSeqs: _sourceEventSeqs, ...envelope } = event;
434
1027
  eventRows.push({
435
1028
  fEventId: eventId,
436
1029
  fParentId: parentId,
@@ -440,7 +1033,10 @@ async function appendEventTail(tx, meta, events, anchor, reuse) {
440
1033
  fName: name,
441
1034
  fActionId: actionId,
442
1035
  fEncoding: EVENT_ENCODING,
443
- fData: JSON.stringify(event.data),
1036
+ fData: JSON.stringify({
1037
+ ...envelope,
1038
+ data
1039
+ }),
444
1040
  fCreatedAt: event.time
445
1041
  });
446
1042
  }
@@ -449,7 +1045,6 @@ async function appendEventTail(tx, meta, events, anchor, reuse) {
449
1045
  fSessionId: meta.id,
450
1046
  fEventId: eventId,
451
1047
  fSequence: nextSeq,
452
- fOriginalSeq: event.seq,
453
1048
  fSurfaceOp: surfaceOp
454
1049
  });
455
1050
  parentId = eventId;
@@ -463,4 +1058,4 @@ async function appendEventTail(tx, meta, events, anchor, reuse) {
463
1058
  };
464
1059
  }
465
1060
  //#endregion
466
- export { EPHEMERAL_EVENT_TYPES, SCHEMA_VERSION, SessionBranchRdb, SessionBranchRdbProvider, SessionPersistenceRdb, SessionPersistenceRdb as default, locateTurnEnd };
1061
+ export { SCHEMA_VERSION, SessionBranchRdb, SessionBranchRdbProvider, SessionPersistenceRdb, SessionPersistenceRdb as default, locateTurnEnd };