@morlay/session-rdb 0.0.12 → 0.0.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@morlay/session-rdb",
3
- "version": "0.0.12",
3
+ "version": "0.0.13",
4
4
  "description": "RDB durable session backend for DeepSeek Harness: implements both session-persistence and session-branch (rewind / retry / fork) providers.",
5
5
  "keywords": [
6
6
  "branch",
@@ -25,12 +25,16 @@
25
25
  "type": "module",
26
26
  "exports": {
27
27
  ".": "./dist/index.mjs",
28
+ "./artifact": "./dist/artifact.mjs",
29
+ "./import": "./dist/import.mjs",
28
30
  "./invariant": "./dist/invariant.mjs",
31
+ "./storage": "./dist/storage.mjs",
32
+ "./testing": "./dist/testing.mjs",
29
33
  "./package.json": "./package.json",
30
34
  "./cordis.patch.yml": "./cordis.patch.yml"
31
35
  },
32
36
  "dependencies": {
33
- "@morlay/session-branch": "^0.0.2",
37
+ "@morlay/session-branch": "^0.0.3",
34
38
  "drizzle-orm": "^1.0.0-rc.5-ab785fc",
35
39
  "fflate": "^0.8.2",
36
40
  "pg": "^8.23.0"
@@ -55,6 +59,19 @@
55
59
  "patch": "./cordis.patch.yml"
56
60
  }
57
61
  },
62
+ "inlinedDependencies": {
63
+ "@jridgewell/sourcemap-codec": "1.6.0",
64
+ "@vitest/expect": "4.1.11",
65
+ "@vitest/pretty-format": "4.1.11",
66
+ "@vitest/runner": "4.1.11",
67
+ "@vitest/snapshot": "4.1.11",
68
+ "@vitest/spy": "4.1.11",
69
+ "@vitest/utils": "4.1.11",
70
+ "chai": "6.2.2",
71
+ "magic-string": "0.30.21",
72
+ "tinyrainbow": "3.1.1",
73
+ "vitest": "4.1.11"
74
+ },
58
75
  "scripts": {
59
76
  "build": "pnpm exec tsdown"
60
77
  }
@@ -0,0 +1,4 @@
1
+ // 日志 / artifact 格式层:会话日志行处理(scanRows / rowToEvent / 修复)与
2
+ // 导入导出 artifact 解析(jsonl / zip)。文件物理分开,经本入口聚合导出。
3
+ export * from "./log.ts";
4
+ export * from "./import.ts";
package/src/index.ts CHANGED
@@ -27,6 +27,7 @@ import { WriteGuard } from "./write-guard.ts";
27
27
  import {
28
28
  buildSeqMap,
29
29
  recomputeReplaceProvenance,
30
+ repairOrphanInboxSplices,
30
31
  repairSurfaceOps,
31
32
  rowToMeta,
32
33
  scanRows,
@@ -261,6 +262,9 @@ export class SessionPersistenceRdb
261
262
  this.writeGuard.confirmHead(id, log.events.at(-1)?.seq ?? -1);
262
263
  // replace 的 provenance 读取时重计算(sourceEventSeqs 不落库)。
263
264
  recomputeReplaceProvenance(log.events);
265
+ // 孤儿 inbox splice(引用已被截断排队消息的插入/消费)会使上游 Inbox
266
+ // 增量重放失败、整个会话不可 resume——读取时改写为 no-op 修复。
267
+ repairOrphanInboxSplices(log.events);
264
268
  return {
265
269
  meta: log.meta,
266
270
  inheritedEventCount: SessionLogOffset(log.inheritedEventCount),
package/src/log.ts CHANGED
@@ -283,6 +283,92 @@ export function repairSurfaceOps(events: SessionEvent[]): void {
283
283
  }
284
284
  }
285
285
 
286
+ interface InboxSpliceLike {
287
+ target?: unknown;
288
+ start?: unknown;
289
+ removedCount?: unknown;
290
+ inserted?: Array<{ id?: unknown }>;
291
+ }
292
+
293
+ /**
294
+ * 定位无法从空状态增量重放的 agent/inbox/spliced 事件(孤儿操作)。
295
+ *
296
+ * 上游 Inbox 每次构造都从会话起点重放全部 inbox splice,失败即拒绝整个
297
+ * 会话(resume / 编辑重放不可用)。rewind 截断历史轮次后若残留 splice 引用
298
+ * 已被截断的排队消息(插入被删、消费保留,或反之),重放时越界或重复——
299
+ * 无法独立重放。
300
+ */
301
+ export function orphanInboxSpliceSeqs(events: readonly SessionEvent[]): Set<number> {
302
+ const inbox: Record<string, Array<{ id: string }>> = { "next-turn": [], "next-step": [] };
303
+ const orphan = new Set<number>();
304
+ for (const raw of events) {
305
+ // agent/inbox/spliced 属 dsh-agent 类型扩展,不在本包 SessionEventMap——
306
+ // 用宽类型 duck-type 读取,避免判别联合窄化到 never。
307
+ const event = raw as unknown as { type: string; seq: number; data: InboxSpliceLike };
308
+ if (event.type !== "agent/inbox/spliced") continue;
309
+ const { target, start, removedCount, inserted } = event.data;
310
+ const list = typeof target === "string" ? inbox[target] : undefined;
311
+ if (list === undefined) {
312
+ orphan.add(event.seq);
313
+ continue;
314
+ }
315
+ const removed = removedCount ?? 0;
316
+ const parsedInserted = (inserted ?? []).map((m) => ({
317
+ id: typeof m.id === "string" ? m.id : "",
318
+ }));
319
+ if (
320
+ !Number.isSafeInteger(start as number) ||
321
+ (start as number) < 0 ||
322
+ (start as number) > list.length ||
323
+ !Number.isSafeInteger(removed as number) ||
324
+ (removed as number) < 0 ||
325
+ (start as number) + (removed as number) > list.length
326
+ ) {
327
+ orphan.add(event.seq);
328
+ continue;
329
+ }
330
+ const candidate = [
331
+ ...list.slice(0, start as number),
332
+ ...parsedInserted,
333
+ ...list.slice((start as number) + (removed as number)),
334
+ ];
335
+ const other = (target === "next-turn" ? inbox["next-step"] : inbox["next-turn"]) ?? [];
336
+ const seen = new Set<string>();
337
+ let dup = false;
338
+ for (const m of [...candidate, ...other]) {
339
+ if (m.id === "") continue;
340
+ if (seen.has(m.id)) {
341
+ dup = true;
342
+ break;
343
+ }
344
+ seen.add(m.id);
345
+ }
346
+ if (dup) {
347
+ orphan.add(event.seq);
348
+ continue;
349
+ }
350
+ list.splice(start as number, removed as number, ...parsedInserted);
351
+ }
352
+ return orphan;
353
+ }
354
+
355
+ /** 内存修复:把孤儿 inbox splice 改写为 no-op(调用方负责持久化)。 */
356
+ export function repairOrphanInboxSplices(events: SessionEvent[]): void {
357
+ const orphan = orphanInboxSpliceSeqs(events);
358
+ if (orphan.size === 0) return;
359
+ for (const event of events) {
360
+ if (!orphan.has(event.seq)) continue;
361
+ const data = event.data as unknown as InboxSpliceLike;
362
+ const target = data.target;
363
+ (event as { data: unknown }).data = {
364
+ ...(typeof target === "string" ? { target } : { target: "next-turn" }),
365
+ start: 0,
366
+ removedCount: 0,
367
+ inserted: [],
368
+ };
369
+ }
370
+ }
371
+
286
372
  export function scanRows(
287
373
  rows: readonly EventRow[],
288
374
  base = 0,
package/src/storage.ts ADDED
@@ -0,0 +1,5 @@
1
+ // 存储引擎层:schema 常量/表定义、sqlite 后端(openDatabase / SqliteBackend)与
2
+ // 并发写保护(WriteGuard)。文件物理分开,经本入口聚合导出。
3
+ export * from "./schema.ts";
4
+ export * from "./sqlite.ts";
5
+ export * from "./write-guard.ts";