@morlay/session-rdb 0.0.12 → 0.0.14
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/dist/artifact.d.mts +77 -0
- package/dist/artifact.mjs +3 -0
- package/dist/import-B-tf5vQn.mjs +438 -0
- package/dist/import.d.mts +24 -0
- package/dist/import.mjs +2 -0
- package/dist/index-uUvn6gYp.d.mts +111 -0
- package/dist/index.d.mts +3 -203
- package/dist/index.mjs +7 -1571
- package/dist/invariant.d.mts +1 -2
- package/dist/invariant.mjs +0 -2
- package/dist/magic-string.es-BWtG0AyA.mjs +1017 -0
- package/dist/schema-BfPVmr1X.mjs +875 -0
- package/dist/schema-CFXZURX7.d.mts +116 -0
- package/dist/sqlite-BUWnS0so.mjs +356 -0
- package/dist/storage.d.mts +44 -0
- package/dist/storage.mjs +3 -0
- package/dist/testing.d.mts +31 -0
- package/dist/testing.mjs +16051 -0
- package/package.json +19 -2
- package/src/artifact.ts +4 -0
- package/src/index.ts +4 -0
- package/src/log.ts +129 -3
- package/src/storage.ts +5 -0
- package/src/testing/contract.ts +562 -0
- package/src/testing/coordinator-contract.ts +1723 -0
- package/src/testing/helpers.ts +20 -0
- package/src/testing.ts +12 -0
- package/dist/index.d.mts.map +0 -1
- package/dist/index.mjs.map +0 -1
- package/dist/invariant.d.mts.map +0 -1
- package/dist/invariant.mjs.map +0 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@morlay/session-rdb",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.14",
|
|
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.
|
|
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
|
}
|
package/src/artifact.ts
ADDED
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
|
@@ -90,6 +90,13 @@ export function remapShadowedRange(
|
|
|
90
90
|
return { start: remap(range.start), end: remap(range.end) };
|
|
91
91
|
}
|
|
92
92
|
|
|
93
|
+
export function remapShadowedSeqs(
|
|
94
|
+
seqs: readonly number[],
|
|
95
|
+
remap: (seq: number) => number,
|
|
96
|
+
): number[] {
|
|
97
|
+
return seqs.map(remap);
|
|
98
|
+
}
|
|
99
|
+
|
|
93
100
|
export function rowToEvent(row: EventRow, seqMap: ReadonlyMap<number, number>): SessionEvent {
|
|
94
101
|
const remap = (seq: number): number => seqMap.get(seq) ?? seq;
|
|
95
102
|
const surfaceOp =
|
|
@@ -97,12 +104,21 @@ export function rowToEvent(row: EventRow, seqMap: ReadonlyMap<number, number>):
|
|
|
97
104
|
? remapSurfaceOp(JSON.parse(row.fSurfaceOp) as SurfaceOp, remap)
|
|
98
105
|
: undefined;
|
|
99
106
|
const data = JSON.parse(row.fData) as SessionEvent["data"];
|
|
100
|
-
// compaction 事件的 shadowedRange
|
|
107
|
+
// compaction 事件的 shadowedRange / shadowedSeqs 是插件合并字段,经结构化
|
|
108
|
+
// 视图重映射到稠密坐标。shadowedSeqs 是权威的被遮蔽节点列表(range 只是
|
|
109
|
+
// 首尾边界对,压缩竞态下可能漏掉并发落地的节点),replace 的 provenance
|
|
110
|
+
// 重计算依赖它,必须与 range 同空间。
|
|
101
111
|
if (row.fType === "compaction/summary" || row.fType === "compaction/prune") {
|
|
102
|
-
const metering = data as unknown as {
|
|
112
|
+
const metering = data as unknown as {
|
|
113
|
+
shadowedRange?: { start: number; end: number };
|
|
114
|
+
shadowedSeqs?: number[];
|
|
115
|
+
};
|
|
103
116
|
if (metering.shadowedRange !== undefined) {
|
|
104
117
|
metering.shadowedRange = remapShadowedRange(metering.shadowedRange, remap);
|
|
105
118
|
}
|
|
119
|
+
if (metering.shadowedSeqs !== undefined) {
|
|
120
|
+
metering.shadowedSeqs = remapShadowedSeqs(metering.shadowedSeqs, remap);
|
|
121
|
+
}
|
|
106
122
|
}
|
|
107
123
|
return {
|
|
108
124
|
type: row.fType as SessionEvent["type"],
|
|
@@ -125,14 +141,38 @@ export function buildSeqMap(
|
|
|
125
141
|
|
|
126
142
|
const SURFACE_EVENT_TYPES = new Set(["user/message", "assistant/message", "tool/result"]);
|
|
127
143
|
|
|
144
|
+
const METERING_EVENT_TYPES = new Set(["compaction/summary", "compaction/prune"]);
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* 读取时重计算 replace 的 sourceEventSeqs(sourceEventSeqs 不落库)。
|
|
148
|
+
*
|
|
149
|
+
* 优先采用紧邻 metering 事件(compaction/summary | compaction/prune)的
|
|
150
|
+
* shadowedSeqs:它是压缩事务落库的**权威被遮蔽节点列表**(range 只是首尾
|
|
151
|
+
* 边界对,压缩竞态下可能漏掉并发落地的节点——range 数值扫描会漏掉这些
|
|
152
|
+
* 节点,使上游 assertProvenance 报 missing)。shadowedSeqs 已由 rowToEvent
|
|
153
|
+
* 重映射到稠密坐标,与 replace 的 surfaceOp range 同空间。
|
|
154
|
+
*
|
|
155
|
+
* 无紧邻 metering 事件时回退到 range 数值扫描(历史数据 / 非压缩 replace,
|
|
156
|
+
* 如 tool-result pruner 的旧样式),保证既有行为不变。
|
|
157
|
+
*/
|
|
128
158
|
export function recomputeReplaceProvenance(events: SessionEvent[]): void {
|
|
129
|
-
for (
|
|
159
|
+
for (let i = 0; i < events.length; i++) {
|
|
160
|
+
const event = events[i]!;
|
|
130
161
|
const raw = event as SessionEvent & { surfaceOp?: unknown; sourceEventSeqs?: number[] };
|
|
131
162
|
const op = raw.surfaceOp;
|
|
132
163
|
if (typeof op !== "object" || op === null || (op as { op?: string }).op !== "replace") {
|
|
133
164
|
continue;
|
|
134
165
|
}
|
|
135
166
|
const { start, end } = op as { start: number; end: number };
|
|
167
|
+
const metering = i > 0 ? events[i - 1] : undefined;
|
|
168
|
+
const meteringData =
|
|
169
|
+
metering !== undefined && METERING_EVENT_TYPES.has(metering.type)
|
|
170
|
+
? (metering.data as unknown as { shadowedSeqs?: number[] })
|
|
171
|
+
: undefined;
|
|
172
|
+
if (meteringData?.shadowedSeqs !== undefined) {
|
|
173
|
+
raw.sourceEventSeqs = meteringData.shadowedSeqs;
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
136
176
|
const refs: number[] = [];
|
|
137
177
|
for (const candidate of events) {
|
|
138
178
|
if (
|
|
@@ -283,6 +323,92 @@ export function repairSurfaceOps(events: SessionEvent[]): void {
|
|
|
283
323
|
}
|
|
284
324
|
}
|
|
285
325
|
|
|
326
|
+
interface InboxSpliceLike {
|
|
327
|
+
target?: unknown;
|
|
328
|
+
start?: unknown;
|
|
329
|
+
removedCount?: unknown;
|
|
330
|
+
inserted?: Array<{ id?: unknown }>;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* 定位无法从空状态增量重放的 agent/inbox/spliced 事件(孤儿操作)。
|
|
335
|
+
*
|
|
336
|
+
* 上游 Inbox 每次构造都从会话起点重放全部 inbox splice,失败即拒绝整个
|
|
337
|
+
* 会话(resume / 编辑重放不可用)。rewind 截断历史轮次后若残留 splice 引用
|
|
338
|
+
* 已被截断的排队消息(插入被删、消费保留,或反之),重放时越界或重复——
|
|
339
|
+
* 无法独立重放。
|
|
340
|
+
*/
|
|
341
|
+
export function orphanInboxSpliceSeqs(events: readonly SessionEvent[]): Set<number> {
|
|
342
|
+
const inbox: Record<string, Array<{ id: string }>> = { "next-turn": [], "next-step": [] };
|
|
343
|
+
const orphan = new Set<number>();
|
|
344
|
+
for (const raw of events) {
|
|
345
|
+
// agent/inbox/spliced 属 dsh-agent 类型扩展,不在本包 SessionEventMap——
|
|
346
|
+
// 用宽类型 duck-type 读取,避免判别联合窄化到 never。
|
|
347
|
+
const event = raw as unknown as { type: string; seq: number; data: InboxSpliceLike };
|
|
348
|
+
if (event.type !== "agent/inbox/spliced") continue;
|
|
349
|
+
const { target, start, removedCount, inserted } = event.data;
|
|
350
|
+
const list = typeof target === "string" ? inbox[target] : undefined;
|
|
351
|
+
if (list === undefined) {
|
|
352
|
+
orphan.add(event.seq);
|
|
353
|
+
continue;
|
|
354
|
+
}
|
|
355
|
+
const removed = removedCount ?? 0;
|
|
356
|
+
const parsedInserted = (inserted ?? []).map((m) => ({
|
|
357
|
+
id: typeof m.id === "string" ? m.id : "",
|
|
358
|
+
}));
|
|
359
|
+
if (
|
|
360
|
+
!Number.isSafeInteger(start as number) ||
|
|
361
|
+
(start as number) < 0 ||
|
|
362
|
+
(start as number) > list.length ||
|
|
363
|
+
!Number.isSafeInteger(removed as number) ||
|
|
364
|
+
(removed as number) < 0 ||
|
|
365
|
+
(start as number) + (removed as number) > list.length
|
|
366
|
+
) {
|
|
367
|
+
orphan.add(event.seq);
|
|
368
|
+
continue;
|
|
369
|
+
}
|
|
370
|
+
const candidate = [
|
|
371
|
+
...list.slice(0, start as number),
|
|
372
|
+
...parsedInserted,
|
|
373
|
+
...list.slice((start as number) + (removed as number)),
|
|
374
|
+
];
|
|
375
|
+
const other = (target === "next-turn" ? inbox["next-step"] : inbox["next-turn"]) ?? [];
|
|
376
|
+
const seen = new Set<string>();
|
|
377
|
+
let dup = false;
|
|
378
|
+
for (const m of [...candidate, ...other]) {
|
|
379
|
+
if (m.id === "") continue;
|
|
380
|
+
if (seen.has(m.id)) {
|
|
381
|
+
dup = true;
|
|
382
|
+
break;
|
|
383
|
+
}
|
|
384
|
+
seen.add(m.id);
|
|
385
|
+
}
|
|
386
|
+
if (dup) {
|
|
387
|
+
orphan.add(event.seq);
|
|
388
|
+
continue;
|
|
389
|
+
}
|
|
390
|
+
list.splice(start as number, removed as number, ...parsedInserted);
|
|
391
|
+
}
|
|
392
|
+
return orphan;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/** 内存修复:把孤儿 inbox splice 改写为 no-op(调用方负责持久化)。 */
|
|
396
|
+
export function repairOrphanInboxSplices(events: SessionEvent[]): void {
|
|
397
|
+
const orphan = orphanInboxSpliceSeqs(events);
|
|
398
|
+
if (orphan.size === 0) return;
|
|
399
|
+
for (const event of events) {
|
|
400
|
+
if (!orphan.has(event.seq)) continue;
|
|
401
|
+
const data = event.data as unknown as InboxSpliceLike;
|
|
402
|
+
const target = data.target;
|
|
403
|
+
(event as { data: unknown }).data = {
|
|
404
|
+
...(typeof target === "string" ? { target } : { target: "next-turn" }),
|
|
405
|
+
start: 0,
|
|
406
|
+
removedCount: 0,
|
|
407
|
+
inserted: [],
|
|
408
|
+
};
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
286
412
|
export function scanRows(
|
|
287
413
|
rows: readonly EventRow[],
|
|
288
414
|
base = 0,
|