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

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 +6 -16
  3. package/dist/artifact.mjs +3 -3
  4. package/dist/{import-DFed8DBt.mjs → import-CKrzjXVB.mjs} +73 -83
  5. package/dist/import.d.mts +2 -3
  6. package/dist/import.mjs +2 -2
  7. package/dist/index-CgGDHQSK.d.mts +225 -0
  8. package/dist/index.d.mts +3 -3
  9. package/dist/index.mjs +694 -169
  10. package/dist/log-DTJsxMud.mjs +314 -0
  11. package/dist/{schema-CFXZURX7.d.mts → schema-COK7-wRV.d.mts} +3 -15
  12. package/dist/sqlite-DCy4VTD8.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 +868 -223
  47. package/src/invariant.ts +0 -2
  48. package/src/legacy.ts +67 -0
  49. package/src/log.ts +41 -87
  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-DCy4VTD8.mjs";
2
+ import { a as repairSurfaceOps, c as scanRows, d as toJsonlArtifact, i as repairOrphanInboxSplices, l as sessionConflictRow, r as recomputeReplaceProvenance, s as rowToMeta, u as sessionInsertRow } from "./log-DTJsxMud.mjs";
3
+ import { c as SessionBranchRdbProvider, l as locateTurnEnd, o as registerSessionImport, s as SessionBranchRdb } from "./import-CKrzjXVB.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";
8
+ import { SessionAlreadyExistsError, SessionAlreadyOwnedError, SessionHandleClosedError, SessionPersistence, SessionPersistenceNotFoundError, SessionPersistenceRevision, SessionReadOnlyError, assertContiguous, assertVersion, materializeAppendBatch, materializeCreateHeader, validateStoredEvents } from "@deepseek-ai/dsh-session-persistence";
9
9
  import { SessionLogOffset } from "@deepseek-ai/dsh-session";
10
- import { and, desc, eq, gte, sql } from "drizzle-orm";
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,315 @@ 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
+ //#endregion
164
213
  //#region src/index.ts
214
+ /** 会话级写所有权与 live 路由簿记。 */
215
+ var RdbBackendTracker = class {
216
+ name;
217
+ /** 每个 id 的活跃 write handle;`null` 表示 claim 构造中。 */
218
+ writers = /* @__PURE__ */ new Map();
219
+ pending = /* @__PURE__ */ new Map();
220
+ openHandles = /* @__PURE__ */ new Set();
221
+ counter = 0;
222
+ constructor(name) {
223
+ this.name = name;
224
+ }
225
+ registerCreated(header, inheritedEventCount) {
226
+ if (this.writers.has(header.id)) throw new SessionAlreadyExistsError(header.id);
227
+ this.writers.set(header.id, null);
228
+ this.pending.set(header.id, {
229
+ header,
230
+ revision: SessionPersistenceRevision(`memory:${this.name}:${++this.counter}`),
231
+ inheritedEventCount,
232
+ cursor: 0,
233
+ everAppended: false
234
+ });
235
+ }
236
+ /** 更新 pending 的 cursor / everAppended(handle append 后同步)。 */
237
+ updatePending(id, cursor, everAppended) {
238
+ const entry = this.pending.get(id);
239
+ if (entry === void 0) return;
240
+ this.pending.set(id, {
241
+ ...entry,
242
+ cursor,
243
+ everAppended
244
+ });
245
+ }
246
+ claimWrite(id) {
247
+ if (this.writers.has(id)) throw new SessionAlreadyOwnedError(id);
248
+ this.writers.set(id, null);
249
+ }
250
+ releaseClaim(id) {
251
+ this.writers.delete(id);
252
+ }
253
+ pendingOf(id) {
254
+ return this.pending.get(id);
255
+ }
256
+ hasPending(id) {
257
+ return this.pending.has(id);
258
+ }
259
+ pendingEntries() {
260
+ return this.pending.entries();
261
+ }
262
+ materialized(id) {
263
+ this.pending.delete(id);
264
+ }
265
+ adopt(handle) {
266
+ this.openHandles.add(handle);
267
+ if (handle.access === "write") this.writers.set(handle.id, handle);
268
+ return handle;
269
+ }
270
+ release(handle, materialized) {
271
+ this.openHandles.delete(handle);
272
+ if (handle.access !== "write") return;
273
+ this.writers.delete(handle.id);
274
+ if (!materialized) this.pending.delete(handle.id);
275
+ }
276
+ writerOf(id) {
277
+ const writer = this.writers.get(id);
278
+ return writer === null ? void 0 : writer;
279
+ }
280
+ async flushAll() {
281
+ const errors = [];
282
+ for (const writer of this.writers.values()) {
283
+ if (writer === null) continue;
284
+ try {
285
+ await writer.drainLive();
286
+ await writer.flush();
287
+ } catch (error) {
288
+ if (error instanceof SessionHandleClosedError) continue;
289
+ errors.push(error);
290
+ }
291
+ }
292
+ if (errors.length > 0) throw new AggregateError(errors, `${this.name} flush failed`);
293
+ }
294
+ async closeAll() {
295
+ const errors = [];
296
+ for (const handle of this.openHandles) try {
297
+ await handle.close();
298
+ } catch (error) {
299
+ errors.push(error);
300
+ }
301
+ if (errors.length > 0) throw new AggregateError(errors, `${this.name} dispose failed`);
302
+ }
303
+ };
304
+ /** 一个打开会话的存储句柄:read / append / flush / close。 */
305
+ var RdbSessionHandle = class RdbSessionHandle {
306
+ persistence;
307
+ id;
308
+ header;
309
+ access;
310
+ state;
311
+ chain = Promise.resolve();
312
+ closing;
313
+ /** 稠密 next-seq(输入空间计数,与旧版 coordinator cursor 同语义)。 */
314
+ cursor;
315
+ materialized;
316
+ /** live 路由缓冲(上游 seq 事件,drain 时过滤 delta 后重编号稠密)。 */
317
+ buffered = [];
318
+ batchTimer;
319
+ drainPaused = false;
320
+ draining;
321
+ /** write open 时发现的 torn tail 起点(append 前先截断)。 */
322
+ tornTruncateTo;
323
+ /** 是否调用过 append(即使全 delta 未落库)——close 时保留 pending。 */
324
+ everAppended = false;
325
+ constructor(persistence, id, header, access, state) {
326
+ this.persistence = persistence;
327
+ this.id = id;
328
+ this.header = header;
329
+ this.access = access;
330
+ this.state = state;
331
+ this.cursor = state.cursor;
332
+ this.materialized = state.materialized;
333
+ this.tornTruncateTo = state.tornTruncateTo;
334
+ }
335
+ get inheritedEventCount() {
336
+ return this.state.inheritedEventCount;
337
+ }
338
+ /** 输入空间 cursor(下一个待落库的上游 seq)。 */
339
+ get cursorValue() {
340
+ return this.cursor;
341
+ }
342
+ async read(offset = 0, length = Number.MAX_SAFE_INTEGER, options) {
343
+ this.assertOpen("read");
344
+ if (!Number.isSafeInteger(offset) || offset < 0) throw new TypeError(`read offset must be a non-negative safe integer, got ${String(offset)}`);
345
+ if (!Number.isSafeInteger(length) || length < 0) throw new TypeError(`read length must be a non-negative safe integer, got ${String(length)}`);
346
+ options?.signal?.throwIfAborted();
347
+ const log = await this.persistence.readLog(this.id, {}, options?.signal);
348
+ if (log === void 0) {
349
+ if (this.persistence.tracker.hasPending(this.id)) return {
350
+ eventState: "detached",
351
+ events: []
352
+ };
353
+ throw new SessionPersistenceNotFoundError(this.id);
354
+ }
355
+ repairSurfaceOps(log.events);
356
+ recomputeReplaceProvenance(log.events);
357
+ repairOrphanInboxSplices(log.events);
358
+ return {
359
+ eventState: "detached",
360
+ events: log.events.slice(offset, offset + length)
361
+ };
362
+ }
363
+ async append(events, options) {
364
+ this.assertOpen("append");
365
+ const batch = materializeAppendBatch(events);
366
+ return this.run("append", async () => {
367
+ options?.signal?.throwIfAborted();
368
+ if (this.access !== "write") throw new SessionReadOnlyError(this.id, "append");
369
+ if (batch.length === 0) return;
370
+ this.everAppended = true;
371
+ assertContiguous(this.id, batch, this.cursor);
372
+ await this.persistence.appendBatch(this.header, this.state.inheritedEventCount, batch, this.tornTruncateTo);
373
+ this.tornTruncateTo = void 0;
374
+ this.cursor += batch.length;
375
+ this.materialized = true;
376
+ this.persistence.tracker.updatePending(this.id, this.cursor, true);
377
+ });
378
+ }
379
+ async flush(options) {
380
+ return this.run("flush", async () => {
381
+ options?.signal?.throwIfAborted();
382
+ if (this.access !== "write") throw new SessionReadOnlyError(this.id, "flush");
383
+ if (this.materialized) return;
384
+ await this.persistence.materializeEmpty(this.header, this.state.inheritedEventCount);
385
+ this.materialized = true;
386
+ });
387
+ }
388
+ close() {
389
+ return this.closing ??= (async () => {
390
+ let drainFailure;
391
+ for (;;) {
392
+ try {
393
+ await this.drainLive();
394
+ } catch (error) {
395
+ drainFailure = error;
396
+ break;
397
+ }
398
+ await this.chain;
399
+ if (this.buffered.length === 0) break;
400
+ }
401
+ await this.chain;
402
+ const failures = [];
403
+ if (drainFailure !== void 0) failures.push(drainFailure instanceof Error ? drainFailure : new Error(JSON.stringify(drainFailure)));
404
+ this.persistence.tracker.release(this, this.materialized || this.everAppended);
405
+ if (failures.length > 1) throw new AggregateError(failures, `session "${this.id}": close failed to drain`);
406
+ if (failures[0] !== void 0) throw failures[0];
407
+ })();
408
+ }
409
+ [Symbol.asyncDispose]() {
410
+ return this.close();
411
+ }
412
+ /** live 路由:缓冲一个已发布事件(持久化自有副本),并启动批量窗口。 */
413
+ enqueueLive(event, reportBackgroundFailure) {
414
+ this.buffered.push(structuredClone(event));
415
+ if (this.batchTimer !== void 0 || this.drainPaused) return;
416
+ this.batchTimer = setTimeout(() => {
417
+ this.batchTimer = void 0;
418
+ this.drainLive().catch(reportBackgroundFailure);
419
+ }, RdbSessionHandle.LIVE_WRITE_BATCH_MAX_DELAY_MS);
420
+ }
421
+ /** rewind 截断后对齐 handle 的稠密 cursor 与继承前缀(DB 已截断)。 */
422
+ resetAfterRewind(cursor, inheritedEventCount) {
423
+ this.cursor = cursor;
424
+ if (inheritedEventCount !== void 0) this.state.inheritedEventCount = SessionLogOffset(inheritedEventCount);
425
+ }
426
+ /** 排空 live 缓冲(过滤 delta + 重编号稠密 + append)。 */
427
+ drainLive() {
428
+ return this.draining ??= this.drainBuffered().finally(() => {
429
+ this.draining = void 0;
430
+ });
431
+ }
432
+ async drainBuffered() {
433
+ if (this.batchTimer !== void 0) {
434
+ clearTimeout(this.batchTimer);
435
+ this.batchTimer = void 0;
436
+ }
437
+ this.drainPaused = false;
438
+ while (this.buffered.length > 0) await this.enqueueChain(async () => {
439
+ const batch = this.buffered.splice(0);
440
+ try {
441
+ const fresh = batch.filter((event) => event.seq >= this.cursor);
442
+ if (fresh.length === 0) return;
443
+ 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}`);
444
+ await this.persistence.appendBatch(this.header, this.state.inheritedEventCount, fresh, this.tornTruncateTo);
445
+ this.tornTruncateTo = void 0;
446
+ this.cursor += fresh.length;
447
+ this.materialized = true;
448
+ } catch (error) {
449
+ this.buffered = batch.concat(this.buffered);
450
+ this.drainPaused = true;
451
+ throw error;
452
+ }
453
+ });
454
+ }
455
+ enqueueChain(op) {
456
+ const next = this.chain.then(op);
457
+ this.chain = next.catch(() => {});
458
+ return next;
459
+ }
460
+ async run(operation, op) {
461
+ this.assertOpen(operation);
462
+ return this.enqueueChain(async () => {
463
+ this.assertOpen(operation);
464
+ return op();
465
+ });
466
+ }
467
+ assertOpen(operation) {
468
+ if (this.closing !== void 0) throw new SessionHandleClosedError(this.id, operation);
469
+ }
470
+ static LIVE_WRITE_BATCH_MAX_DELAY_MS = 200;
471
+ };
165
472
  var SessionPersistenceRdb = class SessionPersistenceRdb extends SessionPersistence {
166
473
  config;
167
474
  static inject = ["sessions", "settings"];
@@ -182,29 +489,15 @@ var SessionPersistenceRdb = class SessionPersistenceRdb extends SessionPersisten
182
489
  })]);
183
490
  static settingsNs = "session-rdb";
184
491
  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
- }
492
+ tracker = new RdbBackendTracker(this.name);
202
493
  backend;
203
494
  storeIdentity;
204
495
  ready;
205
- coordinator;
206
496
  writeGuard = new WriteGuard();
207
497
  reuseEventIds = /* @__PURE__ */ new Map();
498
+ /** live 路由:session/created 后 handle 就绪前的缓冲。 */
499
+ liveBuffers = /* @__PURE__ */ new Map();
500
+ liveReady = /* @__PURE__ */ new Map();
208
501
  constructor(ctx, config, injectedBackend) {
209
502
  let resolved = config;
210
503
  const settings = ctx.reflect.get("settings");
@@ -220,7 +513,7 @@ var SessionPersistenceRdb = class SessionPersistenceRdb extends SessionPersisten
220
513
  this.config = resolved;
221
514
  this.backend = injectedBackend ?? createBackend(resolved);
222
515
  this.ready = this.init();
223
- this.coordinator = new PersistenceCoordinator(this.ctx, this);
516
+ this.installLiveRouting(ctx);
224
517
  new SessionBranchRdb(this.ctx);
225
518
  registerSessionImport(this.ctx, this);
226
519
  }
@@ -228,103 +521,163 @@ var SessionPersistenceRdb = class SessionPersistenceRdb extends SessionPersisten
228
521
  await this.backend.open();
229
522
  this.storeIdentity = this.backend.storeIdentity;
230
523
  }
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);
524
+ async create(header, options) {
525
+ options?.signal?.throwIfAborted();
526
+ const snapshot = materializeCreateHeader(header);
527
+ if (snapshot.isSeeded && options?.inheritedEventCount === void 0) throw new TypeError("seeded session metadata requires an inherited event count");
528
+ const inheritedEventCount = SessionLogOffset(options?.inheritedEventCount ?? 0);
529
+ if (!snapshot.isSeeded && inheritedEventCount !== 0) throw new TypeError("unseeded session metadata inherited event count must be 0");
530
+ await this.ready;
531
+ options?.signal?.throwIfAborted();
532
+ if (this.tracker.hasPending(snapshot.id) || await this.backend.getSession(snapshot.id) !== void 0) throw new SessionAlreadyExistsError(snapshot.id);
533
+ this.tracker.registerCreated(snapshot, inheritedEventCount);
534
+ return this.tracker.adopt(new RdbSessionHandle(this, snapshot.id, snapshot, "write", {
535
+ cursor: 0,
536
+ materialized: false,
537
+ inheritedEventCount
538
+ }));
246
539
  }
247
- borrowSession(id, signal) {
248
- return this.coordinator.borrowSession(id, signal);
540
+ async open(id, access, options) {
541
+ options?.signal?.throwIfAborted();
542
+ await this.ready;
543
+ options?.signal?.throwIfAborted();
544
+ const pending = this.tracker.pendingOf(id);
545
+ if (access === "read") {
546
+ if (pending !== void 0) return this.tracker.adopt(new RdbSessionHandle(this, id, pending.header, "read", {
547
+ cursor: 0,
548
+ materialized: false,
549
+ inheritedEventCount: pending.inheritedEventCount
550
+ }));
551
+ const log = await this.readLog(id, {}, options?.signal);
552
+ if (log === void 0) throw new SessionPersistenceNotFoundError(id);
553
+ validateStoredEvents(log.meta, log.events);
554
+ return this.tracker.adopt(new RdbSessionHandle(this, id, log.meta, "read", {
555
+ cursor: log.events.length,
556
+ materialized: true,
557
+ inheritedEventCount: SessionLogOffset(log.inheritedEventCount)
558
+ }));
559
+ }
560
+ this.tracker.claimWrite(id);
561
+ try {
562
+ if (pending !== void 0) return this.tracker.adopt(new RdbSessionHandle(this, id, pending.header, "write", {
563
+ cursor: pending.cursor,
564
+ materialized: false,
565
+ inheritedEventCount: pending.inheritedEventCount
566
+ }));
567
+ const log = await this.readLog(id, {}, options?.signal);
568
+ if (log === void 0) throw new SessionPersistenceNotFoundError(id);
569
+ validateStoredEvents(log.meta, log.events);
570
+ this.writeGuard.confirmHead(id, log.events.at(-1)?.seq ?? -1);
571
+ return this.tracker.adopt(new RdbSessionHandle(this, id, log.meta, "write", {
572
+ cursor: log.events.length,
573
+ materialized: true,
574
+ inheritedEventCount: SessionLogOffset(log.inheritedEventCount),
575
+ ...log.tornFrom !== void 0 ? { tornTruncateTo: log.tornFrom } : {}
576
+ }));
577
+ } catch (error) {
578
+ this.tracker.releaseClaim(id);
579
+ throw error;
580
+ }
249
581
  }
250
- loadStored(id, signal) {
251
- return this.readPrefix(id, signal);
582
+ flush() {
583
+ return this.tracker.flushAll();
252
584
  }
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
585
+ async stat(id, options) {
586
+ options?.signal?.throwIfAborted();
587
+ await this.ready;
588
+ options?.signal?.throwIfAborted();
589
+ const pending = this.tracker.pendingOf(id);
590
+ if (pending !== void 0) return {
591
+ header: pending.header,
592
+ revision: pending.revision
260
593
  };
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);
594
+ const row = await this.backend.getSession(id);
595
+ if (row === void 0) return void 0;
271
596
  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 } : {}
597
+ header: rowToMeta(row),
598
+ revision: this.rowRevision(row)
277
599
  };
278
600
  }
279
- async readStoredRevision(id, signal) {
601
+ async list(options) {
602
+ const signal = options?.signal;
603
+ const snapshots = [];
604
+ const listed = /* @__PURE__ */ new Set();
605
+ for (const [id, pending] of this.tracker.pendingEntries()) {
606
+ snapshots.push({
607
+ header: pending.header,
608
+ revision: pending.revision
609
+ });
610
+ listed.add(id);
611
+ }
280
612
  signal?.throwIfAborted();
281
613
  await this.ready;
282
614
  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}`);
615
+ const rows = await this.backend.listSessions();
616
+ signal?.throwIfAborted();
617
+ for (const row of rows) {
618
+ if (listed.has(row.fSessionId)) continue;
619
+ snapshots.push({
620
+ header: rowToMeta(row),
621
+ revision: this.rowRevision(row)
622
+ });
623
+ }
624
+ return snapshots;
286
625
  }
287
- async readLog(id, options = {}, signal) {
626
+ /** 导出 artifact(jsonl v2 文本),视图只读不落库。 */
627
+ async readRaw(id, signal) {
288
628
  signal?.throwIfAborted();
289
629
  await this.ready;
290
630
  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);
631
+ const log = await this.readLog(id, {}, signal);
632
+ if (log === void 0) return void 0;
633
+ repairSurfaceOps(log.events);
634
+ recomputeReplaceProvenance(log.events);
635
+ const inheritedEventCount = Math.min(log.inheritedEventCount, log.events.length);
306
636
  return {
307
- meta,
308
- inheritedEventCount: row.fSeedLength ?? 0,
309
- events: preserved,
310
- incarnation: row.fIncarnation,
311
- revision: row.fRevision,
312
- ...tornFrom !== void 0 ? { tornFrom } : {}
637
+ meta: log.meta,
638
+ inheritedEventCount,
639
+ filename: "session.jsonl",
640
+ content: toJsonlArtifact(log.meta, inheritedEventCount, log.events)
313
641
  };
314
642
  }
315
- async appendBatch(storage, events, _isMaterialized) {
643
+ /** 空会话 materialize(flush 的持久化屏障)。 */
644
+ async materializeEmpty(meta, inheritedEventCount) {
316
645
  await this.ready;
317
- const persisted = events.filter(isPersistedEvent);
318
- if (persisted.length === 0) return;
319
- const meta = storage.meta;
646
+ await this.backend.transaction(async (tx) => {
647
+ await tx.upsertSession({
648
+ meta,
649
+ inheritedEventCount
650
+ }, randomUUID());
651
+ await tx.bumpRevision(meta.id);
652
+ });
653
+ this.tracker.materialized(meta.id);
654
+ this.writeGuard.confirmHead(meta.id, -1);
655
+ }
656
+ /** 原样 append 落库(handle 已校验 contiguity;torn tail 先截断)。
657
+ * 与上游 JSONL 一致:ignorable 事件原样存储,不做过滤。写路径校验 v2
658
+ * 形状(fail-closed):未知类型(非 ignorable)与非法消息形状拒绝入库;
659
+ * 旧格式(v0/v1)数据只在读取时经 legacy 转换链动态转换,不落新库。 */
660
+ async appendBatch(meta, inheritedEventCount, events, tornTruncateTo) {
661
+ await this.ready;
662
+ if (events.length === 0) return false;
663
+ validateStoredEvents(meta, [...events]);
320
664
  const reuse = this.reuseEventIds.get(meta.id);
321
665
  if (reuse !== void 0) this.reuseEventIds.delete(meta.id);
322
666
  let confirmedHead = -1;
323
667
  await this.backend.transaction(async (tx) => {
324
- await tx.upsertSession(storage, randomUUID());
668
+ if (tornTruncateTo !== void 0) {
669
+ await tx.deleteBridgeTail(meta.id, tornTruncateTo);
670
+ const prev = await tx.getPrevBridge(meta.id, tornTruncateTo - 1);
671
+ if (prev === void 0) await tx.updateHead(meta.id, "", -1);
672
+ else await tx.updateHead(meta.id, prev.fEventId, prev.fSequence);
673
+ }
674
+ await tx.upsertSession({
675
+ meta,
676
+ inheritedEventCount
677
+ }, randomUUID());
325
678
  const head = await tx.getHead(meta.id);
326
679
  this.writeGuard.assertNoConcurrentWriter(meta.id, head.fHeadSequence);
327
- const { headEventId, headSequence } = await appendEventTail(tx, meta, persisted, {
680
+ const { headEventId, headSequence } = await appendEventTail(tx, meta, events, {
328
681
  parentId: head.fHeadEventId,
329
682
  nextSeq: head.fHeadSequence + 1
330
683
  }, reuse);
@@ -333,51 +686,121 @@ var SessionPersistenceRdb = class SessionPersistenceRdb extends SessionPersisten
333
686
  confirmedHead = headSequence;
334
687
  });
335
688
  this.writeGuard.confirmHead(meta.id, confirmedHead);
689
+ this.tracker.materialized(meta.id);
690
+ return true;
336
691
  }
337
- async commitRepair(storage, tornMarker, closers) {
338
- await this.ready;
339
- const meta = storage.meta;
340
- const persistedClosers = closers.filter(isPersistedEvent);
341
- if (tornMarker === void 0 && persistedClosers.length === 0) return;
342
- 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);
358
- });
359
- const row = await this.backend.getSession(meta.id);
360
- this.writeGuard.confirmHead(meta.id, row?.fHeadSequence ?? -1);
361
- }
362
- async list(signal) {
692
+ /** 读取一个会话的稠密 log(含 torn tail 检测,不含修复改写)。 */
693
+ async readLog(id, options = {}, signal) {
363
694
  signal?.throwIfAborted();
364
695
  await this.ready;
365
696
  signal?.throwIfAborted();
366
- const rows = await this.backend.listSessions();
697
+ const row = await this.backend.getSession(id);
698
+ if (row === void 0) return void 0;
699
+ const meta = rowToMeta(row);
700
+ const eventRows = options.fromSeq === void 0 ? await this.backend.getEventRows(id) : await this.backend.getEventRows(id, options.fromSeq);
367
701
  signal?.throwIfAborted();
368
- return rows.map(rowToMeta);
702
+ if (isLegacyVersion(row.fVersion)) {
703
+ const converted = convertLegacyRows(row, eventRows);
704
+ return {
705
+ meta: converted.meta,
706
+ inheritedEventCount: converted.inheritedEventCount,
707
+ events: converted.events,
708
+ incarnation: row.fIncarnation,
709
+ revision: row.fRevision
710
+ };
711
+ }
712
+ const { preserved, tornFrom } = scanRows(eventRows, options.fromSeq ?? 0);
713
+ return {
714
+ meta,
715
+ inheritedEventCount: row.fSeedLength ?? 0,
716
+ events: preserved,
717
+ incarnation: row.fIncarnation,
718
+ revision: row.fRevision,
719
+ ...tornFrom !== void 0 ? { tornFrom } : {}
720
+ };
369
721
  }
370
722
  async listSnapshots(signal) {
371
723
  signal?.throwIfAborted();
372
724
  await this.ready;
373
725
  signal?.throwIfAborted();
726
+ const snapshots = [];
727
+ const listed = /* @__PURE__ */ new Set();
728
+ for (const [id, pending] of this.tracker.pendingEntries()) {
729
+ snapshots.push({
730
+ header: pending.header,
731
+ revision: pending.revision,
732
+ inheritedEventCount: pending.inheritedEventCount
733
+ });
734
+ listed.add(id);
735
+ }
374
736
  const rows = await this.backend.listSessions();
375
737
  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
- }));
738
+ for (const row of rows) {
739
+ if (listed.has(row.fSessionId)) continue;
740
+ snapshots.push({
741
+ header: rowToMeta(row),
742
+ revision: this.rowRevision(row),
743
+ inheritedEventCount: row.fSeedLength ?? 0
744
+ });
745
+ }
746
+ return snapshots;
747
+ }
748
+ async readStoredRevision(id, signal) {
749
+ signal?.throwIfAborted();
750
+ await this.ready;
751
+ signal?.throwIfAborted();
752
+ const row = await this.backend.getSession(id);
753
+ if (row === void 0) return void 0;
754
+ return this.rowRevision(row);
755
+ }
756
+ /** 便捷:create + append + close(测试与导入路径共用)。 */
757
+ async createAndAppend(header, events, inheritedEventCount) {
758
+ const handle = await this.create(header, inheritedEventCount === void 0 ? void 0 : { inheritedEventCount: SessionLogOffset(inheritedEventCount) });
759
+ try {
760
+ if (events.length > 0) await handle.append(events);
761
+ } finally {
762
+ await handle.close();
763
+ }
764
+ }
765
+ /** 便捷:open(read) + read 全量 + close。 */
766
+ async load(id, signal) {
767
+ const handle = await this.open(id, "read", signal === void 0 ? void 0 : { signal });
768
+ try {
769
+ const { events } = await handle.read(0, void 0, signal === void 0 ? void 0 : { signal });
770
+ const row = await this.backend.getSession(id);
771
+ if (row === void 0) throw new SessionPersistenceNotFoundError(id);
772
+ if (isLegacyVersion(row.fVersion)) return {
773
+ meta: handle.header,
774
+ inheritedEventCount: handle.inheritedEventCount,
775
+ events
776
+ };
777
+ return {
778
+ meta: rowToMeta(row),
779
+ inheritedEventCount: SessionLogOffset(row.fSeedLength ?? 0),
780
+ events
781
+ };
782
+ } finally {
783
+ await handle.close();
784
+ }
785
+ }
786
+ /** 便捷:open(write) + append + close。 */
787
+ async append(id, events) {
788
+ const handle = await this.open(id, "write");
789
+ try {
790
+ await handle.append(events);
791
+ } finally {
792
+ await handle.close();
793
+ }
794
+ }
795
+ /** 便捷:readFrom(稠密后缀)。 */
796
+ async readFrom(id, fromSeq, signal) {
797
+ const log = await this.readLog(id, { fromSeq }, signal);
798
+ if (log === void 0) throw new SessionPersistenceNotFoundError(id);
799
+ return {
800
+ meta: log.meta,
801
+ inheritedEventCount: log.inheritedEventCount,
802
+ events: log.events
803
+ };
381
804
  }
382
805
  async close() {
383
806
  await this.ready;
@@ -390,17 +813,116 @@ var SessionPersistenceRdb = class SessionPersistenceRdb extends SessionPersisten
390
813
  return {
391
814
  backend: this.backend,
392
815
  writeGuard: this.writeGuard,
393
- create: (meta, inheritedEventCount) => this.create(meta, inheritedEventCount),
816
+ create: (meta, inheritedEventCount) => this.create(meta, inheritedEventCount === void 0 ? void 0 : { inheritedEventCount: SessionLogOffset(inheritedEventCount) }),
394
817
  append: (id, events) => this.append(id, events),
395
818
  load: (id) => this.load(id),
396
- inspect: (id, signal) => this.inspect(id, signal),
819
+ inspect: (id, signal) => this.load(id, signal),
397
820
  readFrom: (id, fromSeq, signal) => this.readFrom(id, fromSeq, signal),
398
821
  listSnapshots: (signal) => this.listSnapshots(signal),
399
822
  readStoredRevision: (id, signal) => this.readStoredRevision(id, signal),
400
823
  registerReuseEventIds: (childId, map) => this.registerReuseEventIds(childId, map)
401
824
  };
402
825
  }
826
+ rowRevision(row) {
827
+ return SessionPersistenceRevision(`${this.storeIdentity}:incarnation:${row.fIncarnation}:revision:${row.fRevision}`);
828
+ }
829
+ installLiveRouting(ctx) {
830
+ ctx.on("session/created", (session) => {
831
+ this.liveBuffers.set(session.id, []);
832
+ const ready = this.ensureLiveHandle(session);
833
+ this.liveReady.set(session.id, ready);
834
+ ready.catch((error) => {
835
+ ctx.logger.warn(`session-rdb: live session "${session.id}" persistence init failed: ${String(error)}`);
836
+ });
837
+ });
838
+ for (const session of ctx.sessions.list()) {
839
+ this.liveBuffers.set(session.id, []);
840
+ const ready = this.ensureLiveHandle(session);
841
+ this.liveReady.set(session.id, ready);
842
+ ready.catch((error) => {
843
+ ctx.logger.warn(`session-rdb: live session "${session.id}" persistence init failed: ${String(error)}`);
844
+ });
845
+ }
846
+ ctx.on("session/event", (session, event) => {
847
+ const handle = this.tracker.writerOf(session.id);
848
+ if (handle !== void 0) {
849
+ handle.enqueueLive(event, (error) => {
850
+ ctx.logger.warn(`session-rdb: background write for session "${session.id}" failed (buffered events retained): ${String(error)}`);
851
+ });
852
+ return;
853
+ }
854
+ this.liveBuffers.get(session.id)?.push(structuredClone(event));
855
+ });
856
+ ctx.on("session/flush", (session) => {
857
+ const handle = this.tracker.writerOf(session.id);
858
+ if (handle === void 0) {
859
+ const ready = this.liveReady.get(session.id);
860
+ if (ready === void 0) return void 0;
861
+ return ready.then(() => {
862
+ const settled = this.tracker.writerOf(session.id);
863
+ if (settled === void 0) return void 0;
864
+ return settled.drainLive().then(() => settled.flush());
865
+ });
866
+ }
867
+ return handle.drainLive().then(() => handle.flush());
868
+ });
869
+ ctx.on("session/disposed", (session) => {
870
+ const ready = this.liveReady.get(session.id);
871
+ this.liveBuffers.delete(session.id);
872
+ this.liveReady.delete(session.id);
873
+ const closeHandle = () => {
874
+ const handle = this.tracker.writerOf(session.id);
875
+ if (handle === void 0) return;
876
+ handle.close().catch((error) => {
877
+ ctx.logger.warn(`session-rdb: final drain for session "${session.id}" failed: ${String(error)}`);
878
+ });
879
+ };
880
+ if (ready === void 0) {
881
+ closeHandle();
882
+ return;
883
+ }
884
+ ready.then(closeHandle, closeHandle);
885
+ });
886
+ ctx.effect(() => async () => {
887
+ await Promise.allSettled(this.liveReady.values());
888
+ await this.tracker.closeAll();
889
+ await this.close();
890
+ }, `${this.name} open handles`);
891
+ }
892
+ /** session/created 后为 live 会话建立 write handle(create 或 adopt)。 */
893
+ async ensureLiveHandle(session) {
894
+ const id = session.header.id;
895
+ if (this.tracker.writerOf(id) !== void 0) return;
896
+ await this.ready;
897
+ const stored = await this.readLog(id, {});
898
+ let handle;
899
+ if (stored === void 0) {
900
+ handle = await this.create(session.header, { inheritedEventCount: session.inheritedEventCount });
901
+ const seed = session.snapshotEvents();
902
+ if (seed.length > 0) await handle.append(seed);
903
+ } else {
904
+ 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)`);
905
+ if (stored.inheritedEventCount !== session.inheritedEventCount) throw new Error(`session "${id}" is already persisted with a different inherited event count (id collision)`);
906
+ assertVersion(stored.meta);
907
+ const seed = session.snapshotEvents();
908
+ 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)`);
909
+ handle = await this.open(id, "write");
910
+ const suffix = seed.slice(stored.events.length);
911
+ if (suffix.length > 0) await handle.append(suffix);
912
+ }
913
+ const buffered = this.liveBuffers.get(id);
914
+ if (buffered !== void 0 && buffered.length > 0) {
915
+ this.liveBuffers.set(id, []);
916
+ for (const event of buffered) handle.enqueueLive(event, () => {});
917
+ }
918
+ }
403
919
  };
920
+ function seedCoversPrefix(seed, prefix) {
921
+ return prefix.length <= seed.length && prefix.every((event, index) => {
922
+ const seedEvent = seed[index];
923
+ return seedEvent !== void 0 && JSON.stringify(seedEvent) === JSON.stringify(event);
924
+ });
925
+ }
404
926
  function createBackend(config) {
405
927
  if (config.type === "sqlite") return new SqliteBackend({
406
928
  path: config.path,
@@ -431,6 +953,7 @@ async function appendEventTail(tx, meta, events, anchor, reuse) {
431
953
  const eventId = reusedId ?? randomUUID();
432
954
  if (reusedId === void 0) {
433
955
  const { kind, role, name, actionId } = eventDimensions(event);
956
+ const { data, surfaceOp: _surfaceOp, sourceEventSeqs: _sourceEventSeqs, ...envelope } = event;
434
957
  eventRows.push({
435
958
  fEventId: eventId,
436
959
  fParentId: parentId,
@@ -440,7 +963,10 @@ async function appendEventTail(tx, meta, events, anchor, reuse) {
440
963
  fName: name,
441
964
  fActionId: actionId,
442
965
  fEncoding: EVENT_ENCODING,
443
- fData: JSON.stringify(event.data),
966
+ fData: JSON.stringify({
967
+ ...envelope,
968
+ data
969
+ }),
444
970
  fCreatedAt: event.time
445
971
  });
446
972
  }
@@ -449,7 +975,6 @@ async function appendEventTail(tx, meta, events, anchor, reuse) {
449
975
  fSessionId: meta.id,
450
976
  fEventId: eventId,
451
977
  fSequence: nextSeq,
452
- fOriginalSeq: event.seq,
453
978
  fSurfaceOp: surfaceOp
454
979
  });
455
980
  parentId = eventId;
@@ -463,4 +988,4 @@ async function appendEventTail(tx, meta, events, anchor, reuse) {
463
988
  };
464
989
  }
465
990
  //#endregion
466
- export { EPHEMERAL_EVENT_TYPES, SCHEMA_VERSION, SessionBranchRdb, SessionBranchRdbProvider, SessionPersistenceRdb, SessionPersistenceRdb as default, locateTurnEnd };
991
+ export { SCHEMA_VERSION, SessionBranchRdb, SessionBranchRdbProvider, SessionPersistenceRdb, SessionPersistenceRdb as default, locateTurnEnd };