@morlay/session-rdb 0.0.14 → 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-B-tf5vQn.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 -81
  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-BfPVmr1X.mjs +0 -875
  58. package/dist/sqlite-BUWnS0so.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/README.md CHANGED
@@ -1,9 +1,9 @@
1
1
  # @morlay/session-rdb
2
2
 
3
- RDB(SQLite / PostgreSQL)持久会话后端(`ctx.sessionPersistence`):通过 drizzle
4
- 实现 `PersistenceBackend<number>`,复用上游 `PersistenceCoordinator` 与契约测试套件,
5
- 支持配置选择 SQLite 或 PostgreSQL 后端。设计细节(表结构、delta 过滤、并发写、
6
- 方言差异、仓库结构)见 [docs/design.md](docs/design.md)。
3
+ RDB(SQLite / PostgreSQL)持久会话后端(`ctx.sessionPersistence`):实现上游
4
+ `SessionHandle` 模型(`create`/`open`/`flush`/`stat`/`list`),支持配置选择
5
+ SQLite 或 PostgreSQL 后端。设计细节(表结构、原样存储、并发写、方言差异、
6
+ 仓库结构)见 [docs/design.md](docs/design.md)。
7
7
 
8
8
  ## 配置
9
9
 
@@ -59,18 +59,16 @@ type Config =
59
59
  provider 抽象并**随插件自动注册 `ctx.sessionBranch`**(`SessionBranchRdb`),
60
60
  在不修改上游代码的前提下提供 `rewind / retry / fork` 的持久化闭环:
61
61
 
62
- - **`forkFrom`**:走标准 coordinator 路径(`create` + `append`)从闭合边界
62
+ - **`forkFrom`**:走标准 handle 路径(`create` + `append`)从闭合边界
63
63
  派生新会话(纯 append,`parentSession` / `seedLength` lineage);
64
64
  - **`rewind`**:直接操作后端事务截断到闭合边界(DELETE 尾部 + head 回退 +
65
- revision bump),随后重新 `load` 同步 coordinator 状态,并更新并发写
66
- 检测 head;**支持 live 会话**(`ctx.sessions` owner 时同样就地工作:
67
- flush write-behind 缓冲 截断 RDB 截断 live 内存 log 并复位
68
- surface/header/context/derived 派生缓存 重置 agent 请求头标记
69
- 同步 coordinator cursor;不调用 `load`——live 时 load 会先 flush 撤销
70
- 截断);
65
+ revision bump),并更新并发写检测 head;**支持 live 会话**(`ctx.sessions`
66
+ owner 时同样就地工作:先 flush write-behind 缓冲 → 截断 RDB → 截断
67
+ live 内存 log 并复位 surface/header/context/derived 派生缓存重置 agent
68
+ 请求头标记对齐 handle cursor;不调用 `load`——live 时 load 会先 flush
69
+ 撤销截断);
71
70
  - **`timeline`**:`parentSession` + `seedLength` 版本树投影(live 会话含
72
- 版本效果;cold 会话因版本事件携带 `ignorable` 不进 canonical log 而只有
73
- lineage 骨架)。
71
+ 版本效果;cold 会话版本事件原样落库,效果详情可恢复)。
74
72
 
75
73
  上层编排(edit / reroll / retry / rewind / fork 完整功能)由
76
74
  `@morlay/ui-conversation-message-actions` 提供,或直接在 `ctx.sessionBranch` /
@@ -1,7 +1,7 @@
1
- import { b as EventRow, x as SessionRow } from "./schema-CFXZURX7.mjs";
2
- import { SESSION_IMPORT_PATH, SESSION_LOG_ARTIFACT_FILENAME, expandProvenanceFromStorage, parseImportZip, parseJsonlArtifact, persistImport, registerSessionImport } from "./import.mjs";
1
+ import { v as EventRow, y as SessionRow } from "./schema-COK7-wRV.mjs";
2
+ import { SESSION_IMPORT_PATH, SESSION_LOG_ARTIFACT_FILENAME, parseImportZip, parseJsonlArtifact, persistImport, registerSessionImport } from "./import.mjs";
3
3
  import { SessionStorageMetadata } from "@deepseek-ai/dsh-session-persistence";
4
- import { SessionEvent, SessionHeader, SurfaceOp } from "@deepseek-ai/dsh-session";
4
+ import { SessionEvent, SessionHeader } from "@deepseek-ai/dsh-session";
5
5
  //#region src/log.d.ts
6
6
  declare function rowToMeta(row: SessionRow): SessionHeader;
7
7
  declare function sessionInsertRow(storage: SessionStorageMetadata, incarnation: string): {
@@ -27,17 +27,7 @@ declare function sessionConflictRow(storage: SessionStorageMetadata): {
27
27
  fOrigin: string | null;
28
28
  fDelegationDepth: number | null;
29
29
  };
30
- declare function remapSurfaceOp(op: SurfaceOp, remap: (seq: number) => number): SurfaceOp;
31
- declare function remapShadowedRange(range: {
32
- start: number;
33
- end: number;
34
- }, remap: (seq: number) => number): {
35
- start: number;
36
- end: number;
37
- };
38
- declare function remapShadowedSeqs(seqs: readonly number[], remap: (seq: number) => number): number[];
39
- declare function rowToEvent(row: EventRow, seqMap: ReadonlyMap<number, number>): SessionEvent;
40
- declare function buildSeqMap(rows: readonly Pick<EventRow, "fSequence" | "fOriginalSeq">[]): Map<number, number>;
30
+ declare function rowToEvent(row: EventRow): SessionEvent;
41
31
  /**
42
32
  * 读取时重计算 replace 的 sourceEventSeqs(sourceEventSeqs 不落库)。
43
33
  *
@@ -68,10 +58,10 @@ declare function repairSurfaceOps(events: SessionEvent[]): void;
68
58
  declare function orphanInboxSpliceSeqs(events: readonly SessionEvent[]): Set<number>;
69
59
  /** 内存修复:把孤儿 inbox splice 改写为 no-op(调用方负责持久化)。 */
70
60
  declare function repairOrphanInboxSplices(events: SessionEvent[]): void;
71
- declare function scanRows(rows: readonly EventRow[], base?: number, seqMap?: ReadonlyMap<number, number>): {
61
+ declare function scanRows(rows: readonly EventRow[], base?: number): {
72
62
  preserved: SessionEvent[];
73
63
  tornFrom?: number;
74
64
  };
75
65
  declare function toJsonlArtifact(meta: SessionHeader, inheritedEventCount: number, events: readonly SessionEvent[]): string;
76
66
  //#endregion
77
- export { SESSION_IMPORT_PATH, SESSION_LOG_ARTIFACT_FILENAME, buildSeqMap, expandProvenanceFromStorage, findSurfaceRepairs, orphanInboxSpliceSeqs, parseImportZip, parseJsonlArtifact, persistImport, recomputeReplaceProvenance, registerSessionImport, remapShadowedRange, remapShadowedSeqs, remapSurfaceOp, repairOrphanInboxSplices, repairSurfaceOps, rowToEvent, rowToMeta, scanRows, sessionConflictRow, sessionInsertRow, toJsonlArtifact };
67
+ export { SESSION_IMPORT_PATH, SESSION_LOG_ARTIFACT_FILENAME, findSurfaceRepairs, orphanInboxSpliceSeqs, parseImportZip, parseJsonlArtifact, persistImport, recomputeReplaceProvenance, registerSessionImport, repairOrphanInboxSplices, repairSurfaceOps, rowToEvent, rowToMeta, scanRows, sessionConflictRow, sessionInsertRow, toJsonlArtifact };
package/dist/artifact.mjs CHANGED
@@ -1,3 +1,3 @@
1
- import { A as sessionInsertRow, C as remapSurfaceOp, D as rowToMeta, E as rowToEvent, O as scanRows, S as remapShadowedSeqs, T as repairSurfaceOps, _ as buildSeqMap, b as recomputeReplaceProvenance, j as toJsonlArtifact, k as sessionConflictRow, v as findSurfaceRepairs, w as repairOrphanInboxSplices, x as remapShadowedRange, y as orphanInboxSpliceSeqs } from "./schema-BfPVmr1X.mjs";
2
- import { a as parseJsonlArtifact, i as parseImportZip, n as SESSION_LOG_ARTIFACT_FILENAME, o as persistImport, r as expandProvenanceFromStorage, s as registerSessionImport, t as SESSION_IMPORT_PATH } from "./import-B-tf5vQn.mjs";
3
- export { SESSION_IMPORT_PATH, SESSION_LOG_ARTIFACT_FILENAME, buildSeqMap, expandProvenanceFromStorage, findSurfaceRepairs, orphanInboxSpliceSeqs, parseImportZip, parseJsonlArtifact, persistImport, recomputeReplaceProvenance, registerSessionImport, remapShadowedRange, remapShadowedSeqs, remapSurfaceOp, repairOrphanInboxSplices, repairSurfaceOps, rowToEvent, rowToMeta, scanRows, sessionConflictRow, sessionInsertRow, toJsonlArtifact };
1
+ import { a as repairSurfaceOps, c as scanRows, d as toJsonlArtifact, i as repairOrphanInboxSplices, l as sessionConflictRow, n as orphanInboxSpliceSeqs, o as rowToEvent, r as recomputeReplaceProvenance, s as rowToMeta, t as findSurfaceRepairs, u as sessionInsertRow } from "./log-DTJsxMud.mjs";
2
+ import { a as persistImport, i as parseJsonlArtifact, n as SESSION_LOG_ARTIFACT_FILENAME, o as registerSessionImport, r as parseImportZip, t as SESSION_IMPORT_PATH } from "./import-CKrzjXVB.mjs";
3
+ export { SESSION_IMPORT_PATH, SESSION_LOG_ARTIFACT_FILENAME, findSurfaceRepairs, orphanInboxSpliceSeqs, parseImportZip, parseJsonlArtifact, persistImport, recomputeReplaceProvenance, registerSessionImport, repairOrphanInboxSplices, repairSurfaceOps, rowToEvent, rowToMeta, scanRows, sessionConflictRow, sessionInsertRow, toJsonlArtifact };
@@ -1,6 +1,7 @@
1
- import { D as rowToMeta, l as isPersistedEvent } from "./schema-BfPVmr1X.mjs";
1
+ import { s as rowToMeta } from "./log-DTJsxMud.mjs";
2
2
  import { randomUUID } from "node:crypto";
3
- import { SESSION_FORMAT_VERSION, SessionLogOffset, decodeSeqRanges, decodeStorageRecord } from "@deepseek-ai/dsh-session";
3
+ import { SESSION_FORMAT_VERSION, SessionLogOffset } from "@deepseek-ai/dsh-session";
4
+ import { sessionFormatCatalog } from "@deepseek-ai/dsh-session-format-catalog";
4
5
  import { SessionBranch, SessionBranchError, balanceRewindPrefix, buildTimeline } from "@morlay/session-branch";
5
6
  import { unzipSync } from "fflate";
6
7
  //#region src/branch.ts
@@ -78,10 +79,7 @@ var SessionBranchRdbProvider = class {
78
79
  constructor(persistence, live = {
79
80
  getSession: () => void 0,
80
81
  getAgent: () => void 0,
81
- flush: async () => true,
82
- setCoordinatorCursor: () => {},
83
- setCoordinatorState: () => {},
84
- setCoordinatorSeedLength: () => {}
82
+ flush: async () => true
85
83
  }) {
86
84
  this.persistence = persistence;
87
85
  this.live = live;
@@ -95,7 +93,7 @@ var SessionBranchRdbProvider = class {
95
93
  };
96
94
  }
97
95
  async readRawEvents(id, signal) {
98
- const stored = await this.persistence.loadStored(id, signal);
96
+ const stored = await this.persistence.readLog(id, {}, signal);
99
97
  if (stored === void 0) throw new SessionBranchError(`session "${id}" not found`, "SESSION_NOT_FOUND");
100
98
  return {
101
99
  meta: stored.meta,
@@ -105,7 +103,8 @@ var SessionBranchRdbProvider = class {
105
103
  async forkFrom(sourceId, options = {}, signal) {
106
104
  signal?.throwIfAborted();
107
105
  const { atSeq, anchorMode = "after", seedSuffix = [], childSessionId, meta = {} } = options;
108
- const source = await this.persistence.inspect(sourceId, signal);
106
+ const source = await this.persistence.readLog(sourceId, {}, signal);
107
+ if (source === void 0) throw new SessionBranchError(`session "${sourceId}" not found`, "SESSION_NOT_FOUND");
109
108
  const boundary = locateTurnEnd(source.events, atSeq, anchorMode);
110
109
  const prefix = source.events.slice(0, boundary + 1);
111
110
  const childId = childSessionId ?? mintSessionId();
@@ -123,15 +122,19 @@ var SessionBranchRdbProvider = class {
123
122
  const seed = [...renumber(prefix, 0), ...renumber(seedSuffix, prefix.length)];
124
123
  const internals = this.persistence.internals();
125
124
  const sourceRows = await internals.backend.getEventRows(sourceId);
126
- const sourceEventIds = new Map(sourceRows.map((row) => [row.fOriginalSeq, row.fEventId]));
125
+ const sourceEventIds = new Map(sourceRows.map((row) => [row.fSequence, row.fEventId]));
127
126
  const reuse = /* @__PURE__ */ new Map();
128
127
  for (const event of prefix) {
129
128
  const eventId = sourceEventIds.get(event.seq);
130
129
  if (eventId !== void 0) reuse.set(event.seq, eventId);
131
130
  }
132
131
  internals.registerReuseEventIds(childId, reuse);
133
- await this.persistence.create(childMeta, prefix.length);
134
- if (seed.length > 0) await this.persistence.append(childId, seed);
132
+ const handle = await this.persistence.create(childMeta, { inheritedEventCount: SessionLogOffset(prefix.length) });
133
+ try {
134
+ if (seed.length > 0) await handle.append(seed);
135
+ } finally {
136
+ await handle.close();
137
+ }
135
138
  return childId;
136
139
  }
137
140
  async rewind(id, toBoundary, signal) {
@@ -140,7 +143,7 @@ var SessionBranchRdbProvider = class {
140
143
  const live = this.live.getSession(id);
141
144
  if (live !== void 0) await this.live.flush(live);
142
145
  const raw = live === void 0 ? await this.readRawEvents(id, signal) : void 0;
143
- const inspection = live === void 0 ? void 0 : await this.persistence.inspect(id, signal);
146
+ const inspection = live === void 0 ? void 0 : await this.persistence.internals().inspect(id, signal);
144
147
  const events = live === void 0 ? raw.events : inspection.events;
145
148
  const meta = live === void 0 ? raw.meta : inspection.meta;
146
149
  const boundaryEvent = events[toBoundary];
@@ -149,7 +152,7 @@ var SessionBranchRdbProvider = class {
149
152
  const rawKeepLength = toBoundary === -1 ? 0 : boundaryEvent.type === "turn/end" ? toBoundary + 1 : toBoundary;
150
153
  const keepLength = balanceRewindPrefix(events.slice(0, rawKeepLength)).length;
151
154
  const internals = this.persistence.internals();
152
- const denseBoundary = live === void 0 ? keepLength - 1 : events.slice(0, keepLength).filter(isPersistedEvent).length - 1;
155
+ const denseBoundary = keepLength - 1;
153
156
  const newSeedLength = await internals.backend.transaction(async (tx) => {
154
157
  signal?.throwIfAborted();
155
158
  const head = await tx.getHead(id);
@@ -169,7 +172,6 @@ var SessionBranchRdbProvider = class {
169
172
  await tx.bumpRevision(id);
170
173
  return shrunk;
171
174
  });
172
- if (newSeedLength !== null) this.live.setCoordinatorSeedLength(id, newSeedLength);
173
175
  internals.writeGuard.confirmHead(id, denseBoundary);
174
176
  if (live !== void 0) {
175
177
  truncateLiveSession(live, keepLength);
@@ -180,9 +182,9 @@ var SessionBranchRdbProvider = class {
180
182
  const phase = agent.phase;
181
183
  if (phase !== void 0) phase.lastTurn = lastTurn;
182
184
  }
183
- this.live.setCoordinatorCursor(id, keepLength);
184
- } else if (boundaryEvent?.type === "user/message") this.live.setCoordinatorState(id, keepLength, meta);
185
- else await this.persistence.load(id);
185
+ const handle = this.persistence.tracker.writerOf(id);
186
+ if (handle !== void 0) handle.resetAfterRewind(keepLength, newSeedLength === null ? void 0 : newSeedLength);
187
+ }
186
188
  const row = await internals.backend.getSession(id);
187
189
  if (row === void 0) return {
188
190
  header: {
@@ -214,29 +216,7 @@ var SessionBranchRdb = class extends SessionBranch {
214
216
  getAgent: (id) => {
215
217
  return this.ctx.get("agents")?.get(id);
216
218
  },
217
- flush: (session) => this.ctx.sessions.flush(session),
218
- setCoordinatorCursor: (id, cursor) => {
219
- const state = this.ctx.sessionPersistence.coordinator?.states?.get(id);
220
- if (state !== void 0) state.cursor = cursor;
221
- },
222
- setCoordinatorState: (id, cursor, meta) => {
223
- const states = this.ctx.sessionPersistence.coordinator?.states;
224
- if (states === void 0) return;
225
- const state = states.get(id);
226
- if (state !== void 0) state.cursor = cursor;
227
- else states.set(id, {
228
- meta,
229
- cursor,
230
- materialized: true
231
- });
232
- },
233
- setCoordinatorSeedLength: (id, seedLength) => {
234
- const state = this.ctx.sessionPersistence.coordinator?.states?.get(id);
235
- if (state?.storage !== void 0 && state.storage.inheritedEventCount > seedLength) state.storage = {
236
- ...state.storage,
237
- inheritedEventCount: seedLength
238
- };
239
- }
219
+ flush: (session) => this.ctx.sessions.flush(session)
240
220
  });
241
221
  readBranchPrefix(id, atSeq, mode, signal) {
242
222
  return this.provider.readBranchPrefix(id, atSeq, mode, signal);
@@ -250,22 +230,13 @@ var SessionBranchRdb = class extends SessionBranch {
250
230
  rewind(id, toBoundary, signal) {
251
231
  return this.provider.rewind(id, toBoundary, signal);
252
232
  }
253
- syncLiveCursor(sessionId) {
254
- const live = this.ctx.sessions.get(sessionId);
255
- if (live === void 0) return;
256
- const state = this.ctx.sessionPersistence.coordinator?.states?.get(sessionId);
257
- if (state === void 0) return;
258
- let cursor = state.cursor;
259
- while (live.snapshotEvents()[cursor]?.ignorable === true) cursor += 1;
260
- state.cursor = cursor;
261
- }
262
233
  async timeline(sessionId, signal) {
263
234
  const persistence = this.ctx.sessionPersistence;
264
235
  const snapshots = await persistence.listSnapshots(signal);
265
236
  const readOwnEvents = async (id, fromSeq, s) => {
266
237
  const live = this.ctx.sessions.get(id);
267
238
  if (live !== void 0) return live.snapshotEvents().slice(fromSeq);
268
- return (await persistence.readFrom(id, fromSeq, s)).events;
239
+ return (await persistence.internals().readFrom(id, fromSeq, s)).events;
269
240
  };
270
241
  return buildTimeline(snapshots, readOwnEvents, sessionId, signal);
271
242
  }
@@ -275,16 +246,6 @@ var SessionBranchRdb = class extends SessionBranch {
275
246
  const SESSION_LOG_ARTIFACT_FILENAME = "session.jsonl";
276
247
  const SESSION_IMPORT_PATH = "/api/session.import";
277
248
  const MAX_IMPORT_ZIP_BYTES = 67108864;
278
- function expandProvenanceFromStorage(parsed) {
279
- if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) throw new TypeError("imported session records must be objects");
280
- const record = parsed;
281
- if (record.sourceEventSeqs === void 0) return parsed;
282
- if (!Number.isSafeInteger(record.seq) || record.seq < 0) throw new TypeError("imported session event seq must be a non-negative safe integer");
283
- return {
284
- ...record,
285
- sourceEventSeqs: decodeSeqRanges(record.sourceEventSeqs, record.seq + 1)
286
- };
287
- }
288
249
  function parseJsonlArtifact(content) {
289
250
  const lines = content.split("\n");
290
251
  if (lines.length === 0 || lines[0] === "") throw new Error("imported session log is empty");
@@ -294,10 +255,15 @@ function parseJsonlArtifact(content) {
294
255
  } catch {
295
256
  throw new Error("imported session log has an unparsable header line");
296
257
  }
297
- if (typeof header !== "object" || header === null || header["type"] !== "session" || typeof header["id"] !== "string" || typeof header["version"] !== "number" || !Number.isSafeInteger(header["createdAt"]) || header["createdAt"] < 0) throw new Error("imported session log has an invalid header line");
298
- const seedLength = header["seedLength"];
299
- if (seedLength !== void 0 && (!Number.isSafeInteger(seedLength) || seedLength < 0)) throw new Error("imported session log has an invalid seedLength");
300
- const events = [];
258
+ let restore;
259
+ try {
260
+ restore = sessionFormatCatalog.createRestore(header, {
261
+ recovery: "strict",
262
+ validation: "transformed"
263
+ });
264
+ } catch (error) {
265
+ throw new Error(`imported session log has an invalid header line: ${error instanceof Error ? error.message : String(error)}`);
266
+ }
301
267
  for (let i = 1; i < lines.length; i++) {
302
268
  const line = lines[i];
303
269
  if (line === void 0 || line === "") continue;
@@ -307,23 +273,33 @@ function parseJsonlArtifact(content) {
307
273
  } catch {
308
274
  throw new Error(`imported session log has an unparsable event line at ${i}`);
309
275
  }
310
- for (const event of decodeStorageRecord(expandProvenanceFromStorage(parsed))) events.push(event);
276
+ try {
277
+ restore.decodeRow(parsed);
278
+ } catch (error) {
279
+ throw new Error(`imported session log has an invalid event line at ${i}: ${error instanceof Error ? error.message : String(error)}`);
280
+ }
281
+ }
282
+ let artifact;
283
+ try {
284
+ artifact = restore.finish();
285
+ } catch (error) {
286
+ throw new Error(`imported session log is incomplete: ${error instanceof Error ? error.message : String(error)}`);
311
287
  }
288
+ const meta = artifact.header;
289
+ const events = artifact.events;
312
290
  for (let i = 0; i < events.length; i++) if (events[i].seq !== i) throw new Error(`imported session log seq gap at ${i} (got ${events[i].seq}); import requires a dense log`);
313
- const origin = header["origin"];
314
- const delegationDepth = header["delegationDepth"];
315
- const inheritedEventCount = Math.min(seedLength ?? 0, events.length);
291
+ const inheritedEventCount = Math.min(artifact.inheritedEventCount, events.length);
316
292
  return {
317
293
  meta: {
318
- version: header["version"],
319
- id: header["id"],
320
- createdAt: header["createdAt"],
321
- ...typeof header["cwd"] === "string" ? { cwd: header["cwd"] } : {},
322
- ...typeof header["parentSession"] === "string" ? { parentSession: header["parentSession"] } : {},
323
- isSeeded: seedLength !== void 0,
324
- ...origin === "subagent" ? { origin } : {},
325
- ...Number.isSafeInteger(delegationDepth) && delegationDepth > 0 ? { delegationDepth } : {},
326
- ...typeof header["agentPreset"] === "string" ? { agentPreset: header["agentPreset"] } : {}
294
+ version: SESSION_FORMAT_VERSION,
295
+ id: meta.id,
296
+ createdAt: meta.createdAt,
297
+ ...meta.cwd === void 0 ? {} : { cwd: meta.cwd },
298
+ ...meta.parentSession === void 0 ? {} : { parentSession: meta.parentSession },
299
+ isSeeded: meta.isSeeded,
300
+ ...meta.origin === void 0 ? {} : { origin: meta.origin },
301
+ ...meta.delegationDepth === void 0 ? {} : { delegationDepth: meta.delegationDepth },
302
+ ...meta.agentPreset === void 0 ? {} : { agentPreset: meta.agentPreset }
327
303
  },
328
304
  inheritedEventCount: SessionLogOffset(inheritedEventCount),
329
305
  events
@@ -345,11 +321,25 @@ async function persistImport(persistence, branch, imported, targetId, sessions)
345
321
  if (targetId !== void 0) {
346
322
  if (branch === void 0) throw new Error("sessionBranch service is unavailable");
347
323
  await branch.rewind(targetId, -1);
348
- } else await persistence.create({
349
- ...imported.meta,
350
- id
351
- }, imported.inheritedEventCount);
352
- if (imported.events.length > 0) await persistence.append(id, imported.events);
324
+ const liveHandle = persistence.tracker.writerOf(targetId);
325
+ if (liveHandle !== void 0) {
326
+ if (imported.events.length > 0) await liveHandle.append(imported.events);
327
+ } else {
328
+ const handle = await persistence.open(targetId, "write");
329
+ try {
330
+ if (imported.events.length > 0) await handle.append(imported.events);
331
+ } finally {
332
+ await handle.close();
333
+ }
334
+ }
335
+ } else {
336
+ const handle = await persistence.create({
337
+ ...imported.meta,
338
+ id
339
+ }, { inheritedEventCount: imported.inheritedEventCount });
340
+ if (imported.events.length > 0) await handle.append(imported.events);
341
+ await handle.close();
342
+ }
353
343
  if (targetId !== void 0) {
354
344
  const live = sessions?.get(targetId);
355
345
  if (live !== void 0) replaceLiveSessionLog(live, imported.events);
@@ -435,4 +425,4 @@ function registerSessionImport(ctx, persistence) {
435
425
  });
436
426
  }
437
427
  //#endregion
438
- export { parseJsonlArtifact as a, SessionBranchRdb as c, parseImportZip as i, SessionBranchRdbProvider as l, SESSION_LOG_ARTIFACT_FILENAME as n, persistImport as o, expandProvenanceFromStorage as r, registerSessionImport as s, SESSION_IMPORT_PATH as t, locateTurnEnd as u };
428
+ export { persistImport as a, SessionBranchRdbProvider as c, parseJsonlArtifact as i, locateTurnEnd as l, SESSION_LOG_ARTIFACT_FILENAME as n, registerSessionImport as o, parseImportZip as r, SessionBranchRdb as s, SESSION_IMPORT_PATH as t };
package/dist/import.d.mts CHANGED
@@ -1,11 +1,10 @@
1
- import { n as SessionPersistenceRdb } from "./index-uUvn6gYp.mjs";
1
+ import { n as SessionPersistenceRdb } from "./index-CgGDHQSK.mjs";
2
2
  import { SessionStorageMetadata } from "@deepseek-ai/dsh-session-persistence";
3
3
  import { Session, SessionEvent, SessionId } from "@deepseek-ai/dsh-session";
4
4
  import { Context } from "@deepseek-ai/cordis";
5
5
  //#region src/import.d.ts
6
6
  declare const SESSION_LOG_ARTIFACT_FILENAME = "session.jsonl";
7
7
  declare const SESSION_IMPORT_PATH = "/api/session.import";
8
- declare function expandProvenanceFromStorage(parsed: unknown): unknown;
9
8
  declare function parseJsonlArtifact(content: string): SessionStorageMetadata & {
10
9
  events: SessionEvent[];
11
10
  };
@@ -21,4 +20,4 @@ declare function persistImport(persistence: SessionPersistenceRdb, branch: {
21
20
  }): Promise<SessionId>;
22
21
  declare function registerSessionImport(ctx: Context, persistence: SessionPersistenceRdb): void;
23
22
  //#endregion
24
- export { SESSION_IMPORT_PATH, SESSION_LOG_ARTIFACT_FILENAME, expandProvenanceFromStorage, parseImportZip, parseJsonlArtifact, persistImport, registerSessionImport };
23
+ export { SESSION_IMPORT_PATH, SESSION_LOG_ARTIFACT_FILENAME, parseImportZip, parseJsonlArtifact, persistImport, registerSessionImport };
package/dist/import.mjs CHANGED
@@ -1,2 +1,2 @@
1
- import { a as parseJsonlArtifact, i as parseImportZip, n as SESSION_LOG_ARTIFACT_FILENAME, o as persistImport, r as expandProvenanceFromStorage, s as registerSessionImport, t as SESSION_IMPORT_PATH } from "./import-B-tf5vQn.mjs";
2
- export { SESSION_IMPORT_PATH, SESSION_LOG_ARTIFACT_FILENAME, expandProvenanceFromStorage, parseImportZip, parseJsonlArtifact, persistImport, registerSessionImport };
1
+ import { a as persistImport, i as parseJsonlArtifact, n as SESSION_LOG_ARTIFACT_FILENAME, o as registerSessionImport, r as parseImportZip, t as SESSION_IMPORT_PATH } from "./import-CKrzjXVB.mjs";
2
+ export { SESSION_IMPORT_PATH, SESSION_LOG_ARTIFACT_FILENAME, parseImportZip, parseJsonlArtifact, persistImport, registerSessionImport };
@@ -0,0 +1,225 @@
1
+ import { a as JournalMode, g as Backend, h as WriteGuard } from "./schema-COK7-wRV.mjs";
2
+ import z from "@deepseek-ai/schemastery";
3
+ import { SessionAccess, SessionHandle, SessionHandleAppendOptions, SessionHandleFlushOptions, SessionHandleReadOptions, SessionHandleReadResult, SessionPersistence, SessionPersistenceCreateOptions, SessionPersistenceListOptions, SessionPersistenceOpenOptions, SessionPersistenceRevision, SessionPersistenceSnapshot, SessionPersistenceStatOptions } from "@deepseek-ai/dsh-session-persistence";
4
+ import { Session, SessionEvent, SessionHeader, SessionId, SessionLogOffset } from "@deepseek-ai/dsh-session";
5
+ import { BranchAnchorMode, BranchBoundary, ForkFromOptions, SessionBranch, SessionBranchProvider } from "@morlay/session-branch";
6
+ import { Context } from "@deepseek-ai/cordis";
7
+ //#region src/branch.d.ts
8
+ declare function locateTurnEnd(events: readonly SessionEvent[], atSeq?: number, mode?: BranchAnchorMode): number;
9
+ interface LiveSessionHooks {
10
+ getSession(id: SessionId): Session | undefined;
11
+ getAgent(id: SessionId): LiveAgentLike | undefined;
12
+ flush(session: Session): Promise<boolean>;
13
+ }
14
+ interface LiveAgentLike {
15
+ session: Session;
16
+ requestHeaderLogged?: boolean;
17
+ }
18
+ declare class SessionBranchRdbProvider implements SessionBranchProvider {
19
+ private readonly persistence;
20
+ private readonly live;
21
+ readonly name = "session-rdb";
22
+ constructor(persistence: SessionPersistenceRdb, live?: LiveSessionHooks);
23
+ readBranchPrefix(id: SessionId, atSeq?: number, mode?: BranchAnchorMode, signal?: AbortSignal): Promise<BranchBoundary>;
24
+ readRawEvents(id: SessionId, signal?: AbortSignal): Promise<{
25
+ meta: SessionHeader;
26
+ events: readonly SessionEvent[];
27
+ }>;
28
+ forkFrom(sourceId: SessionId, options?: ForkFromOptions, signal?: AbortSignal): Promise<SessionId>;
29
+ rewind(id: SessionId, toBoundary: number, signal?: AbortSignal): Promise<SessionPersistenceSnapshot>;
30
+ }
31
+ declare class SessionBranchRdb extends SessionBranch {
32
+ static inject: string[];
33
+ constructor(ctx: import("@deepseek-ai/cordis").Context);
34
+ private readonly provider;
35
+ readBranchPrefix(id: SessionId, atSeq?: number, mode?: BranchAnchorMode, signal?: AbortSignal): Promise<BranchBoundary>;
36
+ readRawEvents(id: SessionId, signal?: AbortSignal): Promise<{
37
+ meta: SessionHeader;
38
+ events: readonly SessionEvent[];
39
+ }>;
40
+ forkFrom(sourceId: SessionId, options?: ForkFromOptions, signal?: AbortSignal): Promise<SessionId>;
41
+ rewind(id: SessionId, toBoundary: number, signal?: AbortSignal): Promise<SessionPersistenceSnapshot>;
42
+ timeline(sessionId: SessionId, signal?: AbortSignal): Promise<import("@morlay/session-branch").BranchTimeline>;
43
+ }
44
+ //#endregion
45
+ //#region src/index.d.ts
46
+ interface SessionPersistenceRdbInternals {
47
+ readonly backend: Backend;
48
+ readonly writeGuard: WriteGuard;
49
+ create(meta: SessionHeader, inheritedEventCount?: number): Promise<SessionHandle>;
50
+ append(id: SessionId, events: readonly SessionEvent[]): Promise<void>;
51
+ load(id: SessionId): Promise<import("@deepseek-ai/dsh-session-persistence").SessionInspection>;
52
+ inspect(id: SessionId, signal?: AbortSignal): Promise<import("@deepseek-ai/dsh-session-persistence").SessionInspection>;
53
+ readFrom(id: SessionId, fromSeq: number, signal?: AbortSignal): Promise<{
54
+ meta: SessionHeader;
55
+ inheritedEventCount: number;
56
+ events: readonly SessionEvent[];
57
+ }>;
58
+ listSnapshots(signal?: AbortSignal): Promise<SessionPersistenceSnapshot[]>;
59
+ readStoredRevision(id: SessionId, signal?: AbortSignal): Promise<import("@deepseek-ai/dsh-session-persistence").SessionPersistenceRevision | undefined>;
60
+ registerReuseEventIds(childId: SessionId, map: ReadonlyMap<number, string>): void;
61
+ }
62
+ type Config = {
63
+ type: "sqlite";
64
+ path: string;
65
+ journalMode?: JournalMode;
66
+ busyTimeout?: number;
67
+ } | {
68
+ type: "postgres";
69
+ connectionString: string;
70
+ schema?: string;
71
+ };
72
+ /** 一个已创建但未 materialize 的会话(本进程可见,其他进程不可见)。 */
73
+ interface PendingSession {
74
+ readonly header: SessionHeader;
75
+ readonly revision: SessionPersistenceRevision;
76
+ readonly inheritedEventCount: SessionLogOffset;
77
+ /** 输入空间 cursor(全 delta 批次也推进)。 */
78
+ readonly cursor: number;
79
+ /** 是否调用过 append(close 时保留 pending)。 */
80
+ readonly everAppended: boolean;
81
+ }
82
+ /** 会话级写所有权与 live 路由簿记。 */
83
+ declare class RdbBackendTracker {
84
+ private readonly name;
85
+ /** 每个 id 的活跃 write handle;`null` 表示 claim 构造中。 */
86
+ private readonly writers;
87
+ private readonly pending;
88
+ private readonly openHandles;
89
+ private counter;
90
+ constructor(name: string);
91
+ registerCreated(header: SessionHeader, inheritedEventCount: SessionLogOffset): void;
92
+ /** 更新 pending 的 cursor / everAppended(handle append 后同步)。 */
93
+ updatePending(id: SessionId, cursor: number, everAppended: boolean): void;
94
+ claimWrite(id: SessionId): void;
95
+ releaseClaim(id: SessionId): void;
96
+ pendingOf(id: SessionId): PendingSession | undefined;
97
+ hasPending(id: SessionId): boolean;
98
+ pendingEntries(): IterableIterator<[SessionId, PendingSession]>;
99
+ materialized(id: SessionId): void;
100
+ adopt(handle: RdbSessionHandle): RdbSessionHandle;
101
+ release(handle: RdbSessionHandle, materialized: boolean): void;
102
+ writerOf(id: SessionId): RdbSessionHandle | undefined;
103
+ flushAll(): Promise<void>;
104
+ closeAll(): Promise<void>;
105
+ }
106
+ /** 一个打开会话的存储句柄:read / append / flush / close。 */
107
+ declare class RdbSessionHandle implements SessionHandle {
108
+ private readonly persistence;
109
+ readonly id: SessionId;
110
+ readonly header: SessionHeader;
111
+ readonly access: SessionAccess;
112
+ private readonly state;
113
+ private chain;
114
+ private closing;
115
+ /** 稠密 next-seq(输入空间计数,与旧版 coordinator cursor 同语义)。 */
116
+ private cursor;
117
+ private materialized;
118
+ /** live 路由缓冲(上游 seq 事件,drain 时过滤 delta 后重编号稠密)。 */
119
+ private buffered;
120
+ private batchTimer;
121
+ private drainPaused;
122
+ private draining;
123
+ /** write open 时发现的 torn tail 起点(append 前先截断)。 */
124
+ private tornTruncateTo;
125
+ /** 是否调用过 append(即使全 delta 未落库)——close 时保留 pending。 */
126
+ private everAppended;
127
+ constructor(persistence: SessionPersistenceRdb, id: SessionId, header: SessionHeader, access: SessionAccess, state: {
128
+ cursor: number;
129
+ materialized: boolean;
130
+ inheritedEventCount: SessionLogOffset;
131
+ tornTruncateTo?: number;
132
+ });
133
+ get inheritedEventCount(): SessionLogOffset;
134
+ /** 输入空间 cursor(下一个待落库的上游 seq)。 */
135
+ get cursorValue(): number;
136
+ read(offset?: number, length?: number, options?: SessionHandleReadOptions): Promise<SessionHandleReadResult>;
137
+ append(events: readonly SessionEvent[], options?: SessionHandleAppendOptions): Promise<void>;
138
+ flush(options?: SessionHandleFlushOptions): Promise<void>;
139
+ close(): Promise<void>;
140
+ [Symbol.asyncDispose](): Promise<void>;
141
+ /** live 路由:缓冲一个已发布事件(持久化自有副本),并启动批量窗口。 */
142
+ enqueueLive(event: SessionEvent, reportBackgroundFailure: (error: unknown) => void): void;
143
+ /** rewind 截断后对齐 handle 的稠密 cursor 与继承前缀(DB 已截断)。 */
144
+ resetAfterRewind(cursor: number, inheritedEventCount?: number): void;
145
+ /** 排空 live 缓冲(过滤 delta + 重编号稠密 + append)。 */
146
+ drainLive(): Promise<void>;
147
+ private drainBuffered;
148
+ private enqueueChain;
149
+ private run;
150
+ private assertOpen;
151
+ static readonly LIVE_WRITE_BATCH_MAX_DELAY_MS = 200;
152
+ }
153
+ declare class SessionPersistenceRdb extends SessionPersistence {
154
+ config: Config;
155
+ static inject: string[];
156
+ static Config: z<Config>;
157
+ static readonly settingsNs = "session-rdb";
158
+ readonly name = "session-rdb";
159
+ readonly tracker: RdbBackendTracker;
160
+ private readonly backend;
161
+ private storeIdentity;
162
+ private readonly ready;
163
+ private readonly writeGuard;
164
+ private readonly reuseEventIds;
165
+ /** live 路由:session/created 后 handle 就绪前的缓冲。 */
166
+ private readonly liveBuffers;
167
+ private readonly liveReady;
168
+ constructor(ctx: Context, config: Config, injectedBackend?: Backend);
169
+ private init;
170
+ create(header: SessionHeader, options?: SessionPersistenceCreateOptions): Promise<SessionHandle>;
171
+ open(id: SessionId, access: SessionAccess, options?: SessionPersistenceOpenOptions): Promise<SessionHandle>;
172
+ flush(): Promise<void>;
173
+ stat(id: SessionId, options?: SessionPersistenceStatOptions): Promise<SessionPersistenceSnapshot | undefined>;
174
+ list(options?: SessionPersistenceListOptions): Promise<readonly SessionPersistenceSnapshot[]>;
175
+ /** 导出 artifact(jsonl v2 文本),视图只读不落库。 */
176
+ readRaw(id: SessionId, signal?: AbortSignal): Promise<{
177
+ meta: SessionHeader;
178
+ inheritedEventCount: number;
179
+ filename: string;
180
+ content: string;
181
+ } | undefined>;
182
+ /** 空会话 materialize(flush 的持久化屏障)。 */
183
+ materializeEmpty(meta: SessionHeader, inheritedEventCount: SessionLogOffset): Promise<void>;
184
+ /** 原样 append 落库(handle 已校验 contiguity;torn tail 先截断)。
185
+ * 与上游 JSONL 一致:ignorable 事件原样存储,不做过滤。写路径校验 v2
186
+ * 形状(fail-closed):未知类型(非 ignorable)与非法消息形状拒绝入库;
187
+ * 旧格式(v0/v1)数据只在读取时经 legacy 转换链动态转换,不落新库。 */
188
+ appendBatch(meta: SessionHeader, inheritedEventCount: SessionLogOffset, events: readonly SessionEvent[], tornTruncateTo?: number): Promise<boolean>;
189
+ /** 读取一个会话的稠密 log(含 torn tail 检测,不含修复改写)。 */
190
+ readLog(id: SessionId, options?: {
191
+ fromSeq?: number;
192
+ }, signal?: AbortSignal): Promise<{
193
+ meta: SessionHeader;
194
+ inheritedEventCount: number;
195
+ events: SessionEvent[];
196
+ tornFrom?: number;
197
+ incarnation: string;
198
+ revision: number;
199
+ } | undefined>;
200
+ listSnapshots(signal?: AbortSignal): Promise<Array<SessionPersistenceSnapshot & {
201
+ inheritedEventCount: number;
202
+ }>>;
203
+ readStoredRevision(id: SessionId, signal?: AbortSignal): Promise<SessionPersistenceRevision | undefined>;
204
+ /** 便捷:create + append + close(测试与导入路径共用)。 */
205
+ createAndAppend(header: SessionHeader, events: readonly SessionEvent[], inheritedEventCount?: number): Promise<void>;
206
+ /** 便捷:open(read) + read 全量 + close。 */
207
+ load(id: SessionId, signal?: AbortSignal): Promise<import("@deepseek-ai/dsh-session-persistence").SessionInspection>;
208
+ /** 便捷:open(write) + append + close。 */
209
+ append(id: SessionId, events: readonly SessionEvent[]): Promise<void>;
210
+ /** 便捷:readFrom(稠密后缀)。 */
211
+ readFrom(id: SessionId, fromSeq: number, signal?: AbortSignal): Promise<{
212
+ meta: SessionHeader;
213
+ inheritedEventCount: number;
214
+ events: readonly SessionEvent[];
215
+ }>;
216
+ close(): Promise<void>;
217
+ registerReuseEventIds(childId: SessionId, map: ReadonlyMap<number, string>): void;
218
+ internals(): SessionPersistenceRdbInternals;
219
+ private rowRevision;
220
+ private installLiveRouting;
221
+ /** session/created 后为 live 会话建立 write handle(create 或 adopt)。 */
222
+ private ensureLiveHandle;
223
+ }
224
+ //#endregion
225
+ export { SessionBranchRdbProvider as a, SessionBranchRdb as i, SessionPersistenceRdb as n, locateTurnEnd as o, SessionPersistenceRdbInternals as r, Config as t };
package/dist/index.d.mts CHANGED
@@ -1,3 +1,3 @@
1
- import { n as EPHEMERAL_EVENT_TYPES, s as SCHEMA_VERSION } from "./schema-CFXZURX7.mjs";
2
- import { a as SessionBranchRdbProvider, i as SessionBranchRdb, n as SessionPersistenceRdb, o as locateTurnEnd, r as SessionPersistenceRdbInternals, t as Config } from "./index-uUvn6gYp.mjs";
3
- export { Config, EPHEMERAL_EVENT_TYPES, SCHEMA_VERSION, SessionBranchRdb, SessionBranchRdbProvider, SessionPersistenceRdb, SessionPersistenceRdb as default, SessionPersistenceRdbInternals, locateTurnEnd };
1
+ import { o as SCHEMA_VERSION } from "./schema-COK7-wRV.mjs";
2
+ import { a as SessionBranchRdbProvider, i as SessionBranchRdb, n as SessionPersistenceRdb, o as locateTurnEnd, r as SessionPersistenceRdbInternals, t as Config } from "./index-CgGDHQSK.mjs";
3
+ export { Config, SCHEMA_VERSION, SessionBranchRdb, SessionBranchRdbProvider, SessionPersistenceRdb, SessionPersistenceRdb as default, SessionPersistenceRdbInternals, locateTurnEnd };