@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.
Files changed (65) hide show
  1. package/README.md +11 -13
  2. package/dist/artifact.d.mts +30 -16
  3. package/dist/artifact.mjs +3 -3
  4. package/dist/{import-DFed8DBt.mjs → import-mZZgDBLd.mjs} +73 -83
  5. package/dist/import.d.mts +2 -3
  6. package/dist/import.mjs +2 -2
  7. package/dist/index-GdKkSSb-.d.mts +239 -0
  8. package/dist/index.d.mts +3 -3
  9. package/dist/index.mjs +763 -168
  10. package/dist/log-DBFUBPhv.mjs +394 -0
  11. package/dist/{schema-CFXZURX7.d.mts → schema-COK7-wRV.d.mts} +3 -15
  12. package/dist/sqlite-CnWwGToZ.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 +929 -227
  47. package/src/invariant.ts +0 -2
  48. package/src/legacy.ts +110 -0
  49. package/src/log.ts +143 -91
  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/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 { SessionSeq, encodeSeqRanges, packChunkRuns } from "@deepseek-ai/dsh-session";
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 remapSurfaceOp(op: SurfaceOp, remap: (seq: number) => number): SurfaceOp {
78
- if (op === "append") return op;
79
- return {
80
- op: "replace",
81
- start: SessionSeq(remap(op.start)),
82
- end: SessionSeq(remap(op.end)),
83
- };
84
- }
85
-
86
- export function remapShadowedRange(
87
- range: { start: number; end: number },
88
- remap: (seq: number) => number,
89
- ): { start: number; end: number } {
90
- return { start: remap(range.start), end: remap(range.end) };
91
- }
92
-
93
- export function remapShadowedSeqs(
94
- seqs: readonly number[],
95
- seqMap: ReadonlyMap<number, number>,
96
- ): number[] {
97
- // 未持久化的引用(分支裁剪等)直接丢弃:恒等回退会留下陈旧的上游 seq,
98
- // 稠密坐标通常被压缩,它可能大于 replace 事件的稠密 seq,上游
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
- for (const event of events) {
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
- const endIdx = shapeOk ? nodes.indexOf(end as number) : -1;
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, seqMap) };
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
- type: "session",
489
- version: meta.version,
490
- id: meta.id,
491
- createdAt: meta.createdAt,
492
- ...(meta.cwd === undefined ? {} : { cwd: meta.cwd }),
493
- ...(meta.parentSession === undefined ? {} : { parentSession: meta.parentSession }),
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 record of packChunkRuns(events)) lines.push(JSON.stringify(toStorageRecord(record)));
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
  }