@morlay/session-rdb 0.0.15 → 0.0.16-alpha.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -13
- package/dist/artifact.d.mts +30 -16
- package/dist/artifact.mjs +3 -3
- package/dist/{import-DFed8DBt.mjs → import-mZZgDBLd.mjs} +73 -83
- package/dist/import.d.mts +2 -3
- package/dist/import.mjs +2 -2
- package/dist/index-GdKkSSb-.d.mts +239 -0
- package/dist/index.d.mts +3 -3
- package/dist/index.mjs +763 -168
- package/dist/log-DBFUBPhv.mjs +394 -0
- package/dist/{schema-CFXZURX7.d.mts → schema-COK7-wRV.d.mts} +3 -15
- package/dist/sqlite-CnWwGToZ.mjs +698 -0
- package/dist/storage.d.mts +2 -7
- package/dist/storage.mjs +2 -3
- package/dist/testing.d.mts +30 -1
- package/dist/testing.mjs +676 -1991
- package/drizzle/postgres/20260908072805_v2_initial/migration.sql +58 -0
- package/drizzle/postgres/20260908072805_v2_initial/snapshot.json +686 -0
- package/drizzle/postgres/20260908072806_v3_drop_original_seq/migration.sql +1 -0
- package/drizzle/postgres/20260908072806_v3_drop_original_seq/snapshot.json +673 -0
- package/drizzle/sqlite/20260908072751_v2_initial/migration.sql +53 -0
- package/drizzle/sqlite/20260908072751_v2_initial/snapshot.json +492 -0
- package/drizzle/sqlite/20260908072752_v3_drop_original_seq/migration.sql +6 -0
- package/drizzle/sqlite/20260908072752_v3_drop_original_seq/snapshot.json +513 -0
- package/package.json +17 -12
- package/src/adapters/index.ts +10 -2
- package/src/adapters/to-postgres.ts +19 -15
- package/src/adapters/to-sqlite.ts +20 -14
- package/src/{entities → adapters}/types.ts +7 -8
- package/src/backend.ts +0 -7
- package/src/branch.ts +29 -110
- package/src/drizzle/postgres-v2.ts +11 -0
- package/src/drizzle/postgres-v3.ts +11 -0
- package/src/drizzle/sqlite-v2.ts +10 -0
- package/src/drizzle/sqlite-v3.ts +11 -0
- package/src/entities/index.ts +2 -30
- package/src/entities/v2/index.ts +20 -0
- package/src/entities/v2/session-events.ts +11 -0
- package/src/entities/v3/events.ts +27 -0
- package/src/entities/v3/index.ts +27 -0
- package/src/entities/v3/persistence-state.ts +10 -0
- package/src/entities/v3/schema-meta.ts +9 -0
- package/src/entities/v3/session-events.ts +26 -0
- package/src/entities/v3/sessions.ts +20 -0
- package/src/import.ts +65 -57
- package/src/index.ts +929 -227
- package/src/invariant.ts +0 -2
- package/src/legacy.ts +110 -0
- package/src/log.ts +143 -91
- package/src/postgres.ts +75 -93
- package/src/schema.ts +3 -14
- package/src/sqlite.ts +59 -46
- package/src/testing/contract.ts +460 -375
- package/src/testing/coordinator-contract.ts +108 -1374
- package/src/testing.ts +1 -6
- package/dist/index-uUvn6gYp.d.mts +0 -111
- package/dist/schema-vwzwEXNE.mjs +0 -878
- package/dist/sqlite-CG_Qcx5C.mjs +0 -356
- package/src/adapters/ddl.ts +0 -77
- package/src/entities/events.ts +0 -27
- package/src/entities/persistence-state.ts +0 -10
- package/src/entities/schema-meta.ts +0 -9
- package/src/entities/session-events.ts +0 -29
- package/src/entities/sessions.ts +0 -20
- package/src/migrate.ts +0 -137
package/src/invariant.ts
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
/* jscpd:ignore-start */
|
|
2
1
|
import type { Context } from "@deepseek-ai/cordis";
|
|
3
2
|
import type { InvariantInstaller } from "@deepseek-ai/dsh-invariants";
|
|
4
3
|
|
|
@@ -12,4 +11,3 @@ const install: InvariantInstaller = () => {};
|
|
|
12
11
|
|
|
13
12
|
export const apply = (ctx: Context): Promise<() => void> =>
|
|
14
13
|
Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install));
|
|
15
|
-
/* jscpd:ignore-end */
|
package/src/legacy.ts
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
// 旧格式(v0/v1)历史数据读取转换:RDB 行 → v0/v1 物理记录 → 上游迁移链 →
|
|
2
|
+
// v2 逻辑事件。RDB 写路径从不持久化 sourceEventSeqs(读取时重计算),且
|
|
3
|
+
// delta(assistant/chunk)落盘被过滤——v1→v2 迁移对无 chunk 引用的
|
|
4
|
+
// assistant/message 走「无 pending 直接 emit」路径,生成 stream: [] 的 v2 事件。
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
SESSION_FORMAT_VERSION,
|
|
8
|
+
type SessionEvent,
|
|
9
|
+
type SessionHeader,
|
|
10
|
+
type SessionId,
|
|
11
|
+
} from "@deepseek-ai/dsh-session";
|
|
12
|
+
import { sessionFormatCatalog } from "@deepseek-ai/dsh-session-format-catalog";
|
|
13
|
+
import type { EventRow, SessionRow } from "./backend.ts";
|
|
14
|
+
import { rowToMeta, scanRows } from "./log.ts";
|
|
15
|
+
|
|
16
|
+
/** 重建 v0/v1 物理 header 记录(RDB 行 → 物理 JSON 对象)。 */
|
|
17
|
+
function physicalHeader(row: SessionRow): Record<string, unknown> {
|
|
18
|
+
return {
|
|
19
|
+
type: "session",
|
|
20
|
+
version: row.fVersion,
|
|
21
|
+
id: row.fSessionId,
|
|
22
|
+
createdAt: row.fCreatedAt,
|
|
23
|
+
...(row.fCwd !== null ? { cwd: row.fCwd } : {}),
|
|
24
|
+
...(row.fParentSession !== null ? { parentSession: row.fParentSession } : {}),
|
|
25
|
+
...(row.fSeedLength !== null ? { seedLength: row.fSeedLength } : {}),
|
|
26
|
+
...(row.fOrigin !== null ? { origin: row.fOrigin } : {}),
|
|
27
|
+
delegationDepth: row.fDelegationDepth ?? 0,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** 重建一条 v0/v1 物理事件记录(桥接行 + 事件行 → 物理 JSON 对象)。 */
|
|
32
|
+
function physicalEvent(row: EventRow): Record<string, unknown> {
|
|
33
|
+
return {
|
|
34
|
+
type: row.fType,
|
|
35
|
+
seq: row.fSequence,
|
|
36
|
+
time: row.fCreatedAt,
|
|
37
|
+
data: JSON.parse(row.fData) as unknown,
|
|
38
|
+
...(row.fSurfaceOp !== null ? { surfaceOp: JSON.parse(row.fSurfaceOp) as unknown } : {}),
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* 把旧格式(v0/v1)行转换为 v2 逻辑事件与 header。
|
|
44
|
+
* @param row - 会话行(f_version < 2)。
|
|
45
|
+
* @param eventRows - 已扫描保留的桥接行(稠密 seq,torn tail 已剔除)。
|
|
46
|
+
* @returns v2 逻辑 header、继承前缀长度与事件。
|
|
47
|
+
* @throws 迁移链拒绝(格式损坏 / 无法迁移)时原样抛出。
|
|
48
|
+
*/
|
|
49
|
+
export function convertLegacyRows(
|
|
50
|
+
row: SessionRow,
|
|
51
|
+
eventRows: readonly EventRow[],
|
|
52
|
+
): { meta: SessionHeader; inheritedEventCount: number; events: SessionEvent[] } {
|
|
53
|
+
const restore = sessionFormatCatalog.createRestore(physicalHeader(row), {
|
|
54
|
+
recovery: "strict",
|
|
55
|
+
validation: "transformed",
|
|
56
|
+
});
|
|
57
|
+
for (const eventRow of eventRows) {
|
|
58
|
+
restore.decodeRow(physicalEvent(eventRow));
|
|
59
|
+
}
|
|
60
|
+
const artifact = restore.finish();
|
|
61
|
+
return {
|
|
62
|
+
meta: artifact.header as unknown as SessionHeader,
|
|
63
|
+
inheritedEventCount: artifact.inheritedEventCount,
|
|
64
|
+
events: artifact.events as unknown as SessionEvent[],
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** 会话行是否携带旧格式(v0/v1)数据。 */
|
|
69
|
+
export function isLegacyVersion(version: number): boolean {
|
|
70
|
+
return version < 2;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* 迁移链拒绝后,把旧格式行直接当**当前格式**事件视图采用。
|
|
75
|
+
*
|
|
76
|
+
* 旧写入器只在建会话时落一次 header version,之后跟随上游演进继续追加
|
|
77
|
+
* 事件——同一个 log 因此可能是混合世代(v0 时代的行 + 新版本字段),不是
|
|
78
|
+
* 任何单一已发布格式,严格迁移链必然拒绝。这些行的数据形状(消息包装、
|
|
79
|
+
* 类型名)已经是当前格式,只需 header 版本归一到当前、按稠密 seq 读取;
|
|
80
|
+
* 事件数据的补全与 surface 修复由读取视图修复(repairReadView)负责。
|
|
81
|
+
*
|
|
82
|
+
* 调用方须在此之后执行 `validateStoredEvents`:真正属于旧格式的类型
|
|
83
|
+
* (`assistant/chunk` / `compact/*` 等)仍会被 fail-loud 拒绝。
|
|
84
|
+
*
|
|
85
|
+
* @param row - 会话行(f_version < 2)。
|
|
86
|
+
* @param eventRows - 该会话全部桥接行(按 f_sequence 升序)。
|
|
87
|
+
* @returns 当前格式 header(version 归一到 SESSION_FORMAT_VERSION)、收缩后的
|
|
88
|
+
* 继承前缀长度、稠密事件与可选 torn tail 起点。
|
|
89
|
+
* @throws scanRows 的已提交区损坏错误(seq gap / 不可解析行)。
|
|
90
|
+
*/
|
|
91
|
+
export function adoptLegacyRows(
|
|
92
|
+
row: SessionRow,
|
|
93
|
+
eventRows: readonly EventRow[],
|
|
94
|
+
): {
|
|
95
|
+
meta: SessionHeader;
|
|
96
|
+
inheritedEventCount: number;
|
|
97
|
+
events: SessionEvent[];
|
|
98
|
+
tornFrom?: number;
|
|
99
|
+
} {
|
|
100
|
+
const { preserved, tornFrom } = scanRows(eventRows, 0);
|
|
101
|
+
return {
|
|
102
|
+
meta: { ...rowToMeta(row), version: SESSION_FORMAT_VERSION },
|
|
103
|
+
// 继承前缀不得超过事件数(历史 rewind 未收缩的损坏样式),上游 load 判损坏。
|
|
104
|
+
inheritedEventCount: Math.min(row.fSeedLength ?? 0, preserved.length),
|
|
105
|
+
events: preserved,
|
|
106
|
+
...(tornFrom !== undefined ? { tornFrom } : {}),
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export type { SessionId };
|
package/src/log.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { SessionEvent, SessionHeader, SessionId, SurfaceOp } from "@deepseek-ai/dsh-session";
|
|
2
|
-
import {
|
|
2
|
+
import type { SessionFormatEvent, SessionFormatHeader } from "@deepseek-ai/dsh-session-format";
|
|
3
|
+
import { sessionFormatCatalog } from "@deepseek-ai/dsh-session-format-catalog";
|
|
3
4
|
import type { SessionStorageMetadata } from "@deepseek-ai/dsh-session-persistence";
|
|
4
5
|
import type { EventRow, SessionRow } from "./backend.ts";
|
|
5
6
|
|
|
@@ -8,7 +9,7 @@ export function rowToMeta(row: SessionRow): SessionHeader {
|
|
|
8
9
|
throw new Error("stored session createdAt must be a non-negative safe integer");
|
|
9
10
|
}
|
|
10
11
|
return {
|
|
11
|
-
version: row.fVersion,
|
|
12
|
+
version: row.fVersion as SessionHeader["version"],
|
|
12
13
|
id: row.fSessionId as SessionId,
|
|
13
14
|
createdAt: row.fCreatedAt,
|
|
14
15
|
...(row.fCwd !== null ? { cwd: row.fCwd } : {}),
|
|
@@ -74,77 +75,38 @@ export function sessionConflictRow(storage: SessionStorageMetadata): {
|
|
|
74
75
|
};
|
|
75
76
|
}
|
|
76
77
|
|
|
77
|
-
export function
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
// assertProvenance 会以 "must reference earlier events" 拒绝整个会话。
|
|
100
|
-
return seqs.flatMap((seq) => {
|
|
101
|
-
const dense = seqMap.get(seq);
|
|
102
|
-
return dense === undefined ? [] : [dense];
|
|
103
|
-
});
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
export function rowToEvent(row: EventRow, seqMap: ReadonlyMap<number, number>): SessionEvent {
|
|
107
|
-
const remap = (seq: number): number => seqMap.get(seq) ?? seq;
|
|
108
|
-
const surfaceOp =
|
|
109
|
-
row.fSurfaceOp !== null
|
|
110
|
-
? remapSurfaceOp(JSON.parse(row.fSurfaceOp) as SurfaceOp, remap)
|
|
111
|
-
: undefined;
|
|
112
|
-
const data = JSON.parse(row.fData) as SessionEvent["data"];
|
|
113
|
-
// compaction 事件的 shadowedRange / shadowedSeqs 是插件合并字段,经结构化
|
|
114
|
-
// 视图重映射到稠密坐标。shadowedSeqs 是权威的被遮蔽节点列表(range 只是
|
|
115
|
-
// 首尾边界对,压缩竞态下可能漏掉并发落地的节点),replace 的 provenance
|
|
116
|
-
// 重计算依赖它,必须与 range 同空间。
|
|
117
|
-
if (row.fType === "compaction/summary" || row.fType === "compaction/prune") {
|
|
118
|
-
const metering = data as unknown as {
|
|
119
|
-
shadowedRange?: { start: number; end: number };
|
|
120
|
-
shadowedSeqs?: number[];
|
|
121
|
-
};
|
|
122
|
-
if (metering.shadowedRange !== undefined) {
|
|
123
|
-
metering.shadowedRange = remapShadowedRange(metering.shadowedRange, remap);
|
|
124
|
-
}
|
|
125
|
-
if (metering.shadowedSeqs !== undefined) {
|
|
126
|
-
metering.shadowedSeqs = remapShadowedSeqs(metering.shadowedSeqs, seqMap);
|
|
127
|
-
}
|
|
78
|
+
export function rowToEvent(row: EventRow): SessionEvent {
|
|
79
|
+
const surfaceOp = row.fSurfaceOp !== null ? (JSON.parse(row.fSurfaceOp) as SurfaceOp) : undefined;
|
|
80
|
+
const record = JSON.parse(row.fData) as unknown;
|
|
81
|
+
// fData 形状判别:新写入的完整事件(含 ignorable 信封,与 JSONL 每行
|
|
82
|
+
// 同构)vs 旧 v2 库的纯 data 部分。完整事件必有 type/seq/time/data 四键。
|
|
83
|
+
const full =
|
|
84
|
+
typeof record === "object" &&
|
|
85
|
+
record !== null &&
|
|
86
|
+
!Array.isArray(record) &&
|
|
87
|
+
typeof (record as Record<string, unknown>)["type"] === "string" &&
|
|
88
|
+
typeof (record as Record<string, unknown>)["seq"] === "number" &&
|
|
89
|
+
typeof (record as Record<string, unknown>)["time"] === "number" &&
|
|
90
|
+
"data" in (record as Record<string, unknown>);
|
|
91
|
+
if (full) {
|
|
92
|
+
// seq/time 以桥接行/事件列为权威(rewind 截断后仍稠密连续);
|
|
93
|
+
// surfaceOp 走桥接行列,sourceEventSeqs 不落库(读取时重计算)。
|
|
94
|
+
return {
|
|
95
|
+
...(record as SessionEvent),
|
|
96
|
+
seq: row.fSequence,
|
|
97
|
+
time: row.fCreatedAt,
|
|
98
|
+
...(surfaceOp === undefined ? {} : { surfaceOp }),
|
|
99
|
+
} as SessionEvent;
|
|
128
100
|
}
|
|
129
101
|
return {
|
|
130
102
|
type: row.fType as SessionEvent["type"],
|
|
131
103
|
seq: row.fSequence,
|
|
132
104
|
time: row.fCreatedAt,
|
|
133
|
-
data,
|
|
105
|
+
data: record as SessionEvent["data"],
|
|
134
106
|
...(surfaceOp === undefined ? {} : { surfaceOp }),
|
|
135
107
|
} as SessionEvent;
|
|
136
108
|
}
|
|
137
109
|
|
|
138
|
-
export function buildSeqMap(
|
|
139
|
-
rows: readonly Pick<EventRow, "fSequence" | "fOriginalSeq">[],
|
|
140
|
-
): Map<number, number> {
|
|
141
|
-
const map = new Map<number, number>();
|
|
142
|
-
for (const row of rows) {
|
|
143
|
-
if (!map.has(row.fOriginalSeq)) map.set(row.fOriginalSeq, row.fSequence);
|
|
144
|
-
}
|
|
145
|
-
return map;
|
|
146
|
-
}
|
|
147
|
-
|
|
148
110
|
const SURFACE_EVENT_TYPES = new Set(["user/message", "assistant/message", "tool/result"]);
|
|
149
111
|
|
|
150
112
|
const METERING_EVENT_TYPES = new Set(["compaction/summary", "compaction/prune"]);
|
|
@@ -249,12 +211,15 @@ export function findSurfaceRepairs(events: readonly SessionEvent[]): {
|
|
|
249
211
|
degradeToAppend: Set<number>;
|
|
250
212
|
addAppendMarker: Set<number>;
|
|
251
213
|
clearSurfaceOp: Set<number>;
|
|
214
|
+
clampEnd: Map<number, number>;
|
|
252
215
|
} {
|
|
253
216
|
const nodes: number[] = [];
|
|
254
217
|
const degradeToAppend = new Set<number>();
|
|
255
218
|
const addAppendMarker = new Set<number>();
|
|
256
219
|
const clearSurfaceOp = new Set<number>();
|
|
257
|
-
|
|
220
|
+
const clampEnd = new Map<number, number>();
|
|
221
|
+
for (let index = 0; index < events.length; index++) {
|
|
222
|
+
const event = events[index]!;
|
|
258
223
|
const raw = event as SessionEvent & { surfaceOp?: unknown };
|
|
259
224
|
const op = raw.surfaceOp;
|
|
260
225
|
if (op === undefined) {
|
|
@@ -285,7 +250,16 @@ export function findSurfaceRepairs(events: readonly SessionEvent[]): {
|
|
|
285
250
|
isEventSeqLike(start) &&
|
|
286
251
|
isEventSeqLike(end);
|
|
287
252
|
const startIdx = shapeOk ? nodes.indexOf(start as number) : -1;
|
|
288
|
-
|
|
253
|
+
let endIdx = shapeOk ? nodes.indexOf(end as number) : -1;
|
|
254
|
+
// 旧写入器重编号事件后,存储的 range 端点可能落在旧坐标空间:end 不在
|
|
255
|
+
// 当前 surface 中,但紧邻 metering 事件的 shadowedSeqs 仍给出被遮蔽
|
|
256
|
+
// 节点数量。按数量从 start 起夹取到当前 surface 的同长区间,保住压缩
|
|
257
|
+
// 语义(否则只能降级 append,被压缩历史全部回到派生历史)。
|
|
258
|
+
let clamped: number | undefined;
|
|
259
|
+
if (shapeOk && startIdx !== -1 && endIdx === -1) {
|
|
260
|
+
clamped = clampReplaceEnd(events, index, nodes, startIdx);
|
|
261
|
+
if (clamped !== undefined) endIdx = nodes.indexOf(clamped);
|
|
262
|
+
}
|
|
289
263
|
const rangeOk = shapeOk && startIdx !== -1 && endIdx !== -1 && startIdx <= endIdx;
|
|
290
264
|
let rewriteOk = true;
|
|
291
265
|
if (rangeOk && event.type === "tool/result") {
|
|
@@ -303,9 +277,31 @@ export function findSurfaceRepairs(events: readonly SessionEvent[]): {
|
|
|
303
277
|
nodes.push(event.seq);
|
|
304
278
|
continue;
|
|
305
279
|
}
|
|
280
|
+
if (clamped !== undefined) clampEnd.set(event.seq, clamped);
|
|
306
281
|
nodes.splice(startIdx, endIdx - startIdx + 1, event.seq);
|
|
307
282
|
}
|
|
308
|
-
return { degradeToAppend, addAppendMarker, clearSurfaceOp };
|
|
283
|
+
return { degradeToAppend, addAppendMarker, clearSurfaceOp, clampEnd };
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* 夹取一个 end 落在旧坐标空间的 replace 的结尾。
|
|
288
|
+
*
|
|
289
|
+
* 仅当 replace 紧邻一个 metering 事件且其 `shadowedSeqs` 是权威数量时成立;
|
|
290
|
+
* 夹取点是从 `start` 起的第 `shadowedSeqs.length` 个当前 surface 节点(不足
|
|
291
|
+
* 则取当前 surface 末尾)。无法夹取(start 也不在当前 surface)时返回
|
|
292
|
+
* `undefined`,由调用方降级为 append。
|
|
293
|
+
*/
|
|
294
|
+
function clampReplaceEnd(
|
|
295
|
+
events: readonly SessionEvent[],
|
|
296
|
+
index: number,
|
|
297
|
+
nodes: readonly number[],
|
|
298
|
+
startIdx: number,
|
|
299
|
+
): number | undefined {
|
|
300
|
+
const metering = index > 0 ? events[index - 1] : undefined;
|
|
301
|
+
if (metering === undefined || !METERING_EVENT_TYPES.has(metering.type)) return undefined;
|
|
302
|
+
const shadowedSeqs = (metering.data as unknown as { shadowedSeqs?: unknown }).shadowedSeqs;
|
|
303
|
+
if (!Array.isArray(shadowedSeqs) || shadowedSeqs.length === 0) return undefined;
|
|
304
|
+
return nodes[Math.min(startIdx + shadowedSeqs.length - 1, nodes.length - 1)];
|
|
309
305
|
}
|
|
310
306
|
|
|
311
307
|
export function repairSurfaceOps(events: SessionEvent[]): void {
|
|
@@ -313,14 +309,19 @@ export function repairSurfaceOps(events: SessionEvent[]): void {
|
|
|
313
309
|
if (
|
|
314
310
|
repairs.degradeToAppend.size === 0 &&
|
|
315
311
|
repairs.addAppendMarker.size === 0 &&
|
|
316
|
-
repairs.clearSurfaceOp.size === 0
|
|
312
|
+
repairs.clearSurfaceOp.size === 0 &&
|
|
313
|
+
repairs.clampEnd.size === 0
|
|
317
314
|
) {
|
|
318
315
|
return;
|
|
319
316
|
}
|
|
320
317
|
for (const event of events) {
|
|
321
318
|
const raw = event as SessionEvent & { surfaceOp?: unknown };
|
|
319
|
+
const clamped = repairs.clampEnd.get(event.seq);
|
|
322
320
|
if (repairs.degradeToAppend.has(event.seq)) {
|
|
323
321
|
raw.surfaceOp = "append";
|
|
322
|
+
} else if (clamped !== undefined) {
|
|
323
|
+
const op = raw.surfaceOp as { start: number };
|
|
324
|
+
raw.surfaceOp = { op: "replace", start: op.start, end: clamped };
|
|
324
325
|
} else if (repairs.addAppendMarker.has(event.seq)) {
|
|
325
326
|
raw.surfaceOp = "append";
|
|
326
327
|
} else if (repairs.clearSurfaceOp.has(event.seq)) {
|
|
@@ -329,6 +330,65 @@ export function repairSurfaceOps(events: SessionEvent[]): void {
|
|
|
329
330
|
}
|
|
330
331
|
}
|
|
331
332
|
|
|
333
|
+
/**
|
|
334
|
+
* 读取时补全 assistant 结算字段:旧写入器不落库 `stream`(v2 才把流式记录
|
|
335
|
+
* 嵌入消息),读回时缺失即补空数组——上游 seed 校验要求 turn/step/stream
|
|
336
|
+
* 三者齐备,缺失会让整个会话加载失败。
|
|
337
|
+
*/
|
|
338
|
+
export function repairAssistantSettlement(events: SessionEvent[]): void {
|
|
339
|
+
for (const event of events) {
|
|
340
|
+
if (event.type !== "assistant/message" && event.type !== "assistant/attempt") continue;
|
|
341
|
+
const data = event.data as unknown as Record<string, unknown>;
|
|
342
|
+
if (!Array.isArray(data["stream"])) data["stream"] = [];
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* 把紧邻 replace 的 metering 事件的 `shadowedRange` / `shadowedSeqs` 对齐到
|
|
348
|
+
* replace 最终的稠密 range。
|
|
349
|
+
*
|
|
350
|
+
* 旧写入器重编号事件后,metering 与 replace 一起落在旧坐标空间:二者数值
|
|
351
|
+
* 相等但与当前 surface 无关。夹取(或 range 本身已落在稠密空间而 metering
|
|
352
|
+
* 仍是旧值)后二者不再相等,而上游 token-meter 契约要求紧邻的 metering
|
|
353
|
+
* range 与 replace range 完全一致(否则报 no adjacent shadow price)。range
|
|
354
|
+
* 已一致时不改写——当前写入器保证一致,shadowedSeqs 的额外并发节点因此
|
|
355
|
+
* 原样保留。
|
|
356
|
+
*/
|
|
357
|
+
export function syncMeteringRanges(events: SessionEvent[]): void {
|
|
358
|
+
for (let i = 1; i < events.length; i++) {
|
|
359
|
+
const metering = events[i - 1]!;
|
|
360
|
+
if (!METERING_EVENT_TYPES.has(metering.type)) continue;
|
|
361
|
+
const event = events[i]!;
|
|
362
|
+
const op = (event as SessionEvent & { surfaceOp?: unknown }).surfaceOp;
|
|
363
|
+
if (typeof op !== "object" || op === null || (op as { op?: string }).op !== "replace") continue;
|
|
364
|
+
const { start, end } = op as { start: number; end: number };
|
|
365
|
+
const data = metering.data as unknown as {
|
|
366
|
+
shadowedRange?: { start: number; end: number };
|
|
367
|
+
shadowedSeqs?: number[];
|
|
368
|
+
};
|
|
369
|
+
if (data.shadowedRange?.start === start && data.shadowedRange.end === end) continue;
|
|
370
|
+
data.shadowedRange = { start, end };
|
|
371
|
+
data.shadowedSeqs = events
|
|
372
|
+
.filter(
|
|
373
|
+
(candidate) =>
|
|
374
|
+
candidate.seq >= start && candidate.seq <= end && SURFACE_EVENT_TYPES.has(candidate.type),
|
|
375
|
+
)
|
|
376
|
+
.map((candidate) => candidate.seq);
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
/**
|
|
381
|
+
* 读取视图修复总入口:结算字段补全 → surface 语义修复 → metering 对齐 →
|
|
382
|
+
* provenance 重算 → 孤儿 inbox splice 改写。全部只作用于内存视图,不落库。
|
|
383
|
+
*/
|
|
384
|
+
export function repairReadView(events: SessionEvent[]): void {
|
|
385
|
+
repairAssistantSettlement(events);
|
|
386
|
+
repairSurfaceOps(events);
|
|
387
|
+
syncMeteringRanges(events);
|
|
388
|
+
recomputeReplaceProvenance(events);
|
|
389
|
+
repairOrphanInboxSplices(events);
|
|
390
|
+
}
|
|
391
|
+
|
|
332
392
|
interface InboxSpliceLike {
|
|
333
393
|
target?: unknown;
|
|
334
394
|
start?: unknown;
|
|
@@ -418,7 +478,6 @@ export function repairOrphanInboxSplices(events: SessionEvent[]): void {
|
|
|
418
478
|
export function scanRows(
|
|
419
479
|
rows: readonly EventRow[],
|
|
420
480
|
base = 0,
|
|
421
|
-
seqMap: ReadonlyMap<number, number> = new Map(),
|
|
422
481
|
): { preserved: SessionEvent[]; tornFrom?: number } {
|
|
423
482
|
// Pass 1:解析每行 data;JSON 非法的行是洞(seq/type 列即使在 data 损坏
|
|
424
483
|
// 时也存在)。
|
|
@@ -428,7 +487,7 @@ export function scanRows(
|
|
|
428
487
|
}
|
|
429
488
|
const parsed: Parsed[] = rows.map((row) => {
|
|
430
489
|
try {
|
|
431
|
-
return { ok: true, event: rowToEvent(row
|
|
490
|
+
return { ok: true, event: rowToEvent(row) };
|
|
432
491
|
} catch {
|
|
433
492
|
return { ok: false };
|
|
434
493
|
}
|
|
@@ -471,32 +530,25 @@ export function scanRows(
|
|
|
471
530
|
: { preserved };
|
|
472
531
|
}
|
|
473
532
|
|
|
474
|
-
function toStorageRecord(record: import("@deepseek-ai/dsh-session").StorageRecord): unknown {
|
|
475
|
-
const withSeqs = record as import("@deepseek-ai/dsh-session").StorageRecord & {
|
|
476
|
-
sourceEventSeqs?: unknown;
|
|
477
|
-
};
|
|
478
|
-
if (withSeqs.sourceEventSeqs === undefined) return record;
|
|
479
|
-
return { ...record, sourceEventSeqs: encodeSeqRanges(withSeqs.sourceEventSeqs as never) };
|
|
480
|
-
}
|
|
481
|
-
|
|
482
533
|
export function toJsonlArtifact(
|
|
483
534
|
meta: SessionHeader,
|
|
484
535
|
inheritedEventCount: number,
|
|
485
536
|
events: readonly SessionEvent[],
|
|
486
537
|
): string {
|
|
487
|
-
const header =
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
...(meta.isSeeded ? { seedLength: inheritedEventCount } : {}),
|
|
495
|
-
...(meta.origin === undefined ? {} : { origin: meta.origin }),
|
|
496
|
-
delegationDepth: meta.delegationDepth ?? 0,
|
|
497
|
-
...(meta.agentPreset === undefined ? {} : { agentPreset: meta.agentPreset }),
|
|
498
|
-
};
|
|
538
|
+
const header = sessionFormatCatalog.encodeCurrentHeader(
|
|
539
|
+
{
|
|
540
|
+
...meta,
|
|
541
|
+
delegationDepth: meta.delegationDepth ?? 0,
|
|
542
|
+
} as unknown as SessionFormatHeader,
|
|
543
|
+
inheritedEventCount,
|
|
544
|
+
);
|
|
499
545
|
const lines = [JSON.stringify(header)];
|
|
500
|
-
for (const
|
|
546
|
+
for (const event of events) {
|
|
547
|
+
lines.push(
|
|
548
|
+
JSON.stringify(
|
|
549
|
+
sessionFormatCatalog.encodeCurrentEvent(event as unknown as SessionFormatEvent),
|
|
550
|
+
),
|
|
551
|
+
);
|
|
552
|
+
}
|
|
501
553
|
return lines.join("\n");
|
|
502
554
|
}
|