@morlay/session-rdb 0.0.16-alpha.0 → 0.0.16-alpha.2

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.
@@ -1,5 +1,27 @@
1
+ import { SessionSeq } from "@deepseek-ai/dsh-session";
1
2
  import { sessionFormatCatalog } from "@deepseek-ai/dsh-session-format-catalog";
2
3
  //#region src/log.ts
4
+ /**
5
+ * 把桥接行列里的 replace surfaceOp 归一到当前字段名。
6
+ *
7
+ * v2 时代落库的 JSON 用 `start`/`end`,当前格式用 `startSeq`/`endSeq`;
8
+ * 混合世代回退路径直接采用存储行,必须在此归一,否则上游 v3 surface 校验
9
+ * 以「invalid replace surfaceOp」拒绝整个会话。非 replace 形状原样返回,
10
+ * 由读取视图修复决定降级。
11
+ */
12
+ function normalizeSurfaceOp(value) {
13
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return value;
14
+ const record = value;
15
+ if (record["op"] !== "replace") return value;
16
+ const startSeq = record["startSeq"] ?? record["start"];
17
+ const endSeq = record["endSeq"] ?? record["end"];
18
+ if (typeof startSeq !== "number" || typeof endSeq !== "number") return value;
19
+ return {
20
+ op: "replace",
21
+ startSeq: SessionSeq(startSeq),
22
+ endSeq: SessionSeq(endSeq)
23
+ };
24
+ }
3
25
  function rowToMeta(row) {
4
26
  if (!Number.isSafeInteger(row.fCreatedAt) || row.fCreatedAt < 0) throw new Error("stored session createdAt must be a non-negative safe integer");
5
27
  return {
@@ -43,7 +65,7 @@ function sessionConflictRow(storage) {
43
65
  };
44
66
  }
45
67
  function rowToEvent(row) {
46
- const surfaceOp = row.fSurfaceOp !== null ? JSON.parse(row.fSurfaceOp) : void 0;
68
+ const surfaceOp = row.fSurfaceOp !== null ? normalizeSurfaceOp(JSON.parse(row.fSurfaceOp)) : void 0;
47
69
  const record = JSON.parse(row.fData);
48
70
  if (typeof record === "object" && record !== null && !Array.isArray(record) && typeof record["type"] === "string" && typeof record["seq"] === "number" && typeof record["time"] === "number" && "data" in record) return {
49
71
  ...record,
@@ -60,6 +82,7 @@ function rowToEvent(row) {
60
82
  };
61
83
  }
62
84
  const SURFACE_EVENT_TYPES = /* @__PURE__ */ new Set([
85
+ "system/message",
63
86
  "user/message",
64
87
  "assistant/message",
65
88
  "tool/result"
@@ -82,7 +105,7 @@ function recomputeReplaceProvenance(events) {
82
105
  const raw = events[i];
83
106
  const op = raw.surfaceOp;
84
107
  if (typeof op !== "object" || op === null || op.op !== "replace") continue;
85
- const { start, end } = op;
108
+ const { startSeq, endSeq } = op;
86
109
  const metering = i > 0 ? events[i - 1] : void 0;
87
110
  const meteringData = metering !== void 0 && METERING_EVENT_TYPES.has(metering.type) ? metering.data : void 0;
88
111
  if (meteringData?.shadowedSeqs !== void 0) {
@@ -90,7 +113,7 @@ function recomputeReplaceProvenance(events) {
90
113
  continue;
91
114
  }
92
115
  const refs = [];
93
- for (const candidate of events) if (candidate.seq >= start && candidate.seq <= end && SURFACE_EVENT_TYPES.has(candidate.type)) refs.push(candidate.seq);
116
+ for (const candidate of events) if (candidate.seq >= startSeq && candidate.seq <= endSeq && SURFACE_EVENT_TYPES.has(candidate.type)) refs.push(candidate.seq);
94
117
  raw.sourceEventSeqs = refs;
95
118
  }
96
119
  }
@@ -142,7 +165,9 @@ function findSurfaceRepairs(events) {
142
165
  const degradeToAppend = /* @__PURE__ */ new Set();
143
166
  const addAppendMarker = /* @__PURE__ */ new Set();
144
167
  const clearSurfaceOp = /* @__PURE__ */ new Set();
145
- for (const event of events) {
168
+ const clampEnd = /* @__PURE__ */ new Map();
169
+ for (let index = 0; index < events.length; index++) {
170
+ const event = events[index];
146
171
  const op = event.surfaceOp;
147
172
  if (op === void 0) {
148
173
  if (SURFACE_EVENT_TYPES.has(event.type)) {
@@ -161,12 +186,18 @@ function findSurfaceRepairs(events) {
161
186
  continue;
162
187
  }
163
188
  const replace = typeof op === "object" && op !== null && !Array.isArray(op) ? op : void 0;
164
- const start = replace?.["start"];
165
- const end = replace?.["end"];
189
+ const start = replace?.["startSeq"];
190
+ const end = replace?.["endSeq"];
166
191
  const shapeOk = replace !== void 0 && replace["op"] === "replace" && isEventSeqLike(start) && isEventSeqLike(end);
167
192
  const startIdx = shapeOk ? nodes.indexOf(start) : -1;
168
- const endIdx = shapeOk ? nodes.indexOf(end) : -1;
193
+ let endIdx = shapeOk ? nodes.indexOf(end) : -1;
194
+ let clamped;
195
+ if (shapeOk && startIdx !== -1 && endIdx === -1) {
196
+ clamped = clampReplaceEnd(events, index, nodes, startIdx);
197
+ if (clamped !== void 0) endIdx = nodes.indexOf(clamped);
198
+ }
169
199
  const rangeOk = shapeOk && startIdx !== -1 && endIdx !== -1 && startIdx <= endIdx;
200
+ const provenanceOk = event.type !== "assistant/message";
170
201
  let rewriteOk = true;
171
202
  if (rangeOk && event.type === "tool/result") {
172
203
  const shadowed = nodes.slice(startIdx, endIdx + 1);
@@ -176,30 +207,202 @@ function findSurfaceRepairs(events) {
176
207
  rewriteOk = original?.type === "tool/result" && toolResultRewriteContentOnly(original, event);
177
208
  }
178
209
  }
179
- if (!rangeOk || !rewriteOk) {
210
+ if (!rangeOk || !rewriteOk || !provenanceOk) {
180
211
  degradeToAppend.add(event.seq);
181
212
  nodes.push(event.seq);
182
213
  continue;
183
214
  }
215
+ if (clamped !== void 0) clampEnd.set(event.seq, clamped);
184
216
  nodes.splice(startIdx, endIdx - startIdx + 1, event.seq);
185
217
  }
186
218
  return {
187
219
  degradeToAppend,
188
220
  addAppendMarker,
189
- clearSurfaceOp
221
+ clearSurfaceOp,
222
+ clampEnd
190
223
  };
191
224
  }
225
+ /**
226
+ * 夹取一个 end 落在旧坐标空间的 replace 的结尾。
227
+ *
228
+ * 仅当 replace 紧邻一个 metering 事件且其 `shadowedSeqs` 是权威数量时成立;
229
+ * 夹取点是从 `start` 起的第 `shadowedSeqs.length` 个当前 surface 节点(不足
230
+ * 则取当前 surface 末尾)。无法夹取(start 也不在当前 surface)时返回
231
+ * `undefined`,由调用方降级为 append。
232
+ */
233
+ function clampReplaceEnd(events, index, nodes, startIdx) {
234
+ const metering = index > 0 ? events[index - 1] : void 0;
235
+ if (metering === void 0 || !METERING_EVENT_TYPES.has(metering.type)) return void 0;
236
+ const shadowedSeqs = metering.data.shadowedSeqs;
237
+ if (!Array.isArray(shadowedSeqs) || shadowedSeqs.length === 0) return void 0;
238
+ return nodes[Math.min(startIdx + shadowedSeqs.length - 1, nodes.length - 1)];
239
+ }
192
240
  function repairSurfaceOps(events) {
193
241
  const repairs = findSurfaceRepairs(events);
194
- if (repairs.degradeToAppend.size === 0 && repairs.addAppendMarker.size === 0 && repairs.clearSurfaceOp.size === 0) return;
242
+ if (repairs.degradeToAppend.size === 0 && repairs.addAppendMarker.size === 0 && repairs.clearSurfaceOp.size === 0 && repairs.clampEnd.size === 0) return;
195
243
  for (const event of events) {
196
244
  const raw = event;
245
+ const clamped = repairs.clampEnd.get(event.seq);
197
246
  if (repairs.degradeToAppend.has(event.seq)) raw.surfaceOp = "append";
198
- else if (repairs.addAppendMarker.has(event.seq)) raw.surfaceOp = "append";
247
+ else if (clamped !== void 0) {
248
+ const op = raw.surfaceOp;
249
+ raw.surfaceOp = {
250
+ op: "replace",
251
+ startSeq: SessionSeq(op.startSeq),
252
+ endSeq: SessionSeq(clamped)
253
+ };
254
+ } else if (repairs.addAppendMarker.has(event.seq)) raw.surfaceOp = "append";
199
255
  else if (repairs.clearSurfaceOp.has(event.seq)) delete raw.surfaceOp;
200
256
  }
201
257
  }
202
258
  /**
259
+ * 读取时补全 assistant 结算字段:旧写入器不落库 `stream`(v2 才把流式记录
260
+ * 嵌入消息),读回时缺失即补空数组——上游 seed 校验要求 turn/step/stream
261
+ * 三者齐备,缺失会让整个会话加载失败。
262
+ */
263
+ function repairAssistantSettlement(events) {
264
+ for (const event of events) {
265
+ if (event.type !== "assistant/message" && event.type !== "assistant/attempt") continue;
266
+ const data = event.data;
267
+ if (!Array.isArray(data["stream"])) data["stream"] = [];
268
+ }
269
+ }
270
+ /**
271
+ * 读取时把旧格式 `request/header` 归一为当前格式:v3 起 system prompt 由
272
+ * surface 上的 `system/message` 承载,header 必须省略 `system`,空 `tools` /
273
+ * `adapterDefaults` 也必须省略(上游 v3 事件校验 fail loud)。混合世代回退
274
+ * 视图直接采用存储行,不归一会让整个会话加载失败。system prompt 因此不再
275
+ * 进入模型请求;会话下次运行由 v3 的 system/message 机制重建。
276
+ */
277
+ function repairRequestHeaders(events) {
278
+ for (const event of events) {
279
+ if (event.type !== "request/header") continue;
280
+ const header = event.data["header"];
281
+ if (typeof header !== "object" || header === null || Array.isArray(header)) continue;
282
+ const record = header;
283
+ delete record["system"];
284
+ if (Array.isArray(record["tools"]) && record["tools"].length === 0) delete record["tools"];
285
+ const defaults = record["adapterDefaults"];
286
+ if (typeof defaults === "object" && defaults !== null && !Array.isArray(defaults) && Object.keys(defaults).length === 0) delete record["adapterDefaults"];
287
+ }
288
+ }
289
+ /**
290
+ * 把紧邻 replace 的 metering 事件的 `shadowedRange` / `shadowedSeqs` 对齐到
291
+ * replace 最终的稠密 range。
292
+ *
293
+ * 旧写入器重编号事件后,metering 与 replace 一起落在旧坐标空间:二者数值
294
+ * 相等但与当前 surface 无关。夹取(或 range 本身已落在稠密空间而 metering
295
+ * 仍是旧值)后二者不再相等,而上游 token-meter 契约要求紧邻的 metering
296
+ * range 与 replace range 完全一致(否则报 no adjacent shadow price)。range
297
+ * 已一致时不改写——当前写入器保证一致,shadowedSeqs 的额外并发节点因此
298
+ * 原样保留。
299
+ */
300
+ function syncMeteringRanges(events) {
301
+ for (let i = 1; i < events.length; i++) {
302
+ const metering = events[i - 1];
303
+ if (!METERING_EVENT_TYPES.has(metering.type)) continue;
304
+ const op = events[i].surfaceOp;
305
+ if (typeof op !== "object" || op === null || op.op !== "replace") continue;
306
+ const { startSeq, endSeq } = op;
307
+ const data = metering.data;
308
+ if (data.shadowedRange?.start === startSeq && data.shadowedRange.end === endSeq) continue;
309
+ data.shadowedRange = {
310
+ start: startSeq,
311
+ end: endSeq
312
+ };
313
+ data.shadowedSeqs = events.filter((candidate) => candidate.seq >= startSeq && candidate.seq <= endSeq && SURFACE_EVENT_TYPES.has(candidate.type)).map((candidate) => candidate.seq);
314
+ }
315
+ }
316
+ const PTC_EVENT_RENAMES = {
317
+ "tool/code-dispatch-start": "tool/ptc-dispatch-start",
318
+ "tool/code-dispatch": "tool/ptc-dispatch"
319
+ };
320
+ /** 把消息的 `tools-code-mode` 插件来源改写为 `tools-ptc`(非该来源原样返回)。 */
321
+ function renamePtcMessageSource(message) {
322
+ if (typeof message !== "object" || message === null || Array.isArray(message)) return message;
323
+ const record = message;
324
+ const source = record["source"];
325
+ if (typeof source !== "object" || source === null || Array.isArray(source)) return message;
326
+ const sourceRecord = source;
327
+ if (sourceRecord["kind"] !== "plugin" || sourceRecord["plugin"] !== "tools-code-mode") return message;
328
+ return {
329
+ ...record,
330
+ source: {
331
+ ...sourceRecord,
332
+ plugin: "tools-ptc"
333
+ }
334
+ };
335
+ }
336
+ /**
337
+ * 读取时把 v2 时代的 PTC 词汇归一为当前词汇。
338
+ *
339
+ * 上游 v2→v3 迁移把 `tool/code-dispatch(-start)` 改名为
340
+ * `tool/ptc-dispatch(-start)`、把 `tools-code-mode` 插件来源改名为 `tools-ptc`、
341
+ * 把 agent preset 值 `code` 改名为 `ptc`。混合世代回退视图直接采用存储行,
342
+ * 不重命名会被上游 v3 校验以「unknown event type」拒绝整个会话。词汇改写
343
+ * 与上游迁移链的 renamePtcEvent 对齐。
344
+ */
345
+ function renameLegacyPtcEvents(events) {
346
+ for (let index = 0; index < events.length; index++) {
347
+ const event = events[index];
348
+ const renamedType = PTC_EVENT_RENAMES[event.type];
349
+ if (renamedType !== void 0) {
350
+ events[index] = {
351
+ ...event,
352
+ type: renamedType
353
+ };
354
+ continue;
355
+ }
356
+ if (event.type === "agent-preset/selected") {
357
+ const data = event.data;
358
+ if (data["agentPreset"] === "code") events[index] = {
359
+ ...event,
360
+ data: {
361
+ ...data,
362
+ agentPreset: "ptc"
363
+ }
364
+ };
365
+ continue;
366
+ }
367
+ if (event.type === "user/message") {
368
+ const renamed = renamePtcMessageSource(event.data);
369
+ if (renamed !== event.data) events[index] = {
370
+ ...event,
371
+ data: renamed
372
+ };
373
+ continue;
374
+ }
375
+ if (event.type === "agent/inbox/spliced" || event.type === "session/title-llm-request") {
376
+ const data = event.data;
377
+ const key = event.type === "agent/inbox/spliced" ? "inserted" : "messages";
378
+ const list = data[key];
379
+ if (!Array.isArray(list)) continue;
380
+ const renamed = list.map(renamePtcMessageSource);
381
+ if (renamed.some((message, position) => message !== list[position])) events[index] = {
382
+ ...event,
383
+ data: {
384
+ ...data,
385
+ [key]: renamed
386
+ }
387
+ };
388
+ }
389
+ }
390
+ }
391
+ /**
392
+ * 读取视图修复总入口:形状补全(结算字段 / request header)→ PTC 词汇归一
393
+ * → surface 语义修复 → metering 对齐 → provenance 重算 → 孤儿 inbox splice
394
+ * 改写。全部只作用于内存视图,不落库。
395
+ */
396
+ function repairReadView(events) {
397
+ repairAssistantSettlement(events);
398
+ repairRequestHeaders(events);
399
+ renameLegacyPtcEvents(events);
400
+ repairSurfaceOps(events);
401
+ syncMeteringRanges(events);
402
+ recomputeReplaceProvenance(events);
403
+ repairOrphanInboxSplices(events);
404
+ }
405
+ /**
203
406
  * 定位无法从空状态增量重放的 agent/inbox/spliced 事件(孤儿操作)。
204
407
  *
205
408
  * 上游 Inbox 每次构造都从会话起点重放全部 inbox splice,失败即拒绝整个
@@ -311,4 +514,4 @@ function toJsonlArtifact(meta, inheritedEventCount, events) {
311
514
  return lines.join("\n");
312
515
  }
313
516
  //#endregion
314
- export { repairSurfaceOps as a, scanRows as c, toJsonlArtifact as d, repairOrphanInboxSplices as i, sessionConflictRow as l, orphanInboxSpliceSeqs as n, rowToEvent as o, recomputeReplaceProvenance as r, rowToMeta as s, findSurfaceRepairs as t, sessionInsertRow as u };
517
+ export { repairAssistantSettlement as a, repairRequestHeaders as c, rowToMeta as d, scanRows as f, toJsonlArtifact as g, syncMeteringRanges as h, renameLegacyPtcEvents as i, repairSurfaceOps as l, sessionInsertRow as m, orphanInboxSpliceSeqs as n, repairOrphanInboxSplices as o, sessionConflictRow as p, recomputeReplaceProvenance as r, repairReadView as s, findSurfaceRepairs as t, rowToEvent as u };
@@ -1,4 +1,4 @@
1
- import { l as sessionConflictRow, u as sessionInsertRow } from "./log-DTJsxMud.mjs";
1
+ import { m as sessionInsertRow, p as sessionConflictRow } from "./log-BIuXv09P.mjs";
2
2
  import { randomUUID } from "node:crypto";
3
3
  import { and, eq, gte, sql } from "drizzle-orm";
4
4
  import { check, index, integer, sqliteTable, text, unique } from "drizzle-orm/sqlite-core";
package/dist/storage.mjs CHANGED
@@ -1,2 +1,2 @@
1
- import { a as SCHEMA_VERSION, c as eventKind, d as tSchemaMeta, f as tSessionEvents, g as WriteGuard, i as EVENT_ENCODING, l as tEvents, n as openDatabase, o as SESSION_PERSISTENCE_SQLITE_APPLICATION_ID, p as tSessions, r as DEFAULT_BUSY_TIMEOUT_MS, s as eventDimensions, t as SqliteBackend, u as tPersistenceState } from "./sqlite-DCy4VTD8.mjs";
1
+ import { a as SCHEMA_VERSION, c as eventKind, d as tSchemaMeta, f as tSessionEvents, g as WriteGuard, i as EVENT_ENCODING, l as tEvents, n as openDatabase, o as SESSION_PERSISTENCE_SQLITE_APPLICATION_ID, p as tSessions, r as DEFAULT_BUSY_TIMEOUT_MS, s as eventDimensions, t as SqliteBackend, u as tPersistenceState } from "./sqlite-Dpd17YhI.mjs";
2
2
  export { DEFAULT_BUSY_TIMEOUT_MS, EVENT_ENCODING, SCHEMA_VERSION, SESSION_PERSISTENCE_SQLITE_APPLICATION_ID, SqliteBackend, WriteGuard, eventDimensions, eventKind, openDatabase, tEvents, tPersistenceState, tSchemaMeta, tSessionEvents, tSessions };
package/dist/testing.mjs CHANGED
@@ -5116,7 +5116,7 @@ function manageArtifactAttachment(attachment) {
5116
5116
  if (attachment.body != null) attachment.bodyEncoding ??= "base64";
5117
5117
  }
5118
5118
  //#endregion
5119
- //#region ../../node_modules/.pnpm/vitest@4.1.11_@opentelemetry+api@1.9.1_@types+node@26.5.0_happy-dom@20.13.2_vite@8.2.2__9bde3b9553ba3388013323e9f29b314e/node_modules/vitest/dist/chunks/utils.BX5Fg8C4.js
5119
+ //#region ../../node_modules/.pnpm/vitest@4.1.11_@opentelemetry+api@1.9.1_@types+node@26.5.0_happy-dom@20.14.0_vite@8.2.2__b2fde82745a4512744fb5cfb49e8ecf4/node_modules/vitest/dist/chunks/utils.BX5Fg8C4.js
5120
5120
  const NAME_WORKER_STATE = "__vitest_worker__";
5121
5121
  function getWorkerState() {
5122
5122
  const workerState = globalThis[NAME_WORKER_STATE];
@@ -10019,10 +10019,10 @@ function offsetToLineNumber(source, offset) {
10019
10019
  return line + 1;
10020
10020
  }
10021
10021
  //#endregion
10022
- //#region ../../node_modules/.pnpm/vitest@4.1.11_@opentelemetry+api@1.9.1_@types+node@26.5.0_happy-dom@20.13.2_vite@8.2.2__9bde3b9553ba3388013323e9f29b314e/node_modules/vitest/dist/chunks/_commonjsHelpers.D26ty3Ew.js
10022
+ //#region ../../node_modules/.pnpm/vitest@4.1.11_@opentelemetry+api@1.9.1_@types+node@26.5.0_happy-dom@20.14.0_vite@8.2.2__b2fde82745a4512744fb5cfb49e8ecf4/node_modules/vitest/dist/chunks/_commonjsHelpers.D26ty3Ew.js
10023
10023
  var commonjsGlobal = typeof globalThis !== "undefined" ? globalThis : typeof window !== "undefined" ? window : typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : {};
10024
10024
  //#endregion
10025
- //#region ../../node_modules/.pnpm/vitest@4.1.11_@opentelemetry+api@1.9.1_@types+node@26.5.0_happy-dom@20.13.2_vite@8.2.2__9bde3b9553ba3388013323e9f29b314e/node_modules/vitest/dist/chunks/rpc.MzXet3jl.js
10025
+ //#region ../../node_modules/.pnpm/vitest@4.1.11_@opentelemetry+api@1.9.1_@types+node@26.5.0_happy-dom@20.14.0_vite@8.2.2__b2fde82745a4512744fb5cfb49e8ecf4/node_modules/vitest/dist/chunks/rpc.MzXet3jl.js
10026
10026
  const RealDate = Date;
10027
10027
  let now = null;
10028
10028
  var MockDate = class MockDate extends RealDate {
@@ -10987,7 +10987,7 @@ function raceWith(promise, other) {
10987
10987
  }))]);
10988
10988
  }
10989
10989
  //#endregion
10990
- //#region ../../node_modules/.pnpm/vitest@4.1.11_@opentelemetry+api@1.9.1_@types+node@26.5.0_happy-dom@20.13.2_vite@8.2.2__9bde3b9553ba3388013323e9f29b314e/node_modules/vitest/dist/chunks/test.DNmyFkvJ.js
10990
+ //#region ../../node_modules/.pnpm/vitest@4.1.11_@opentelemetry+api@1.9.1_@types+node@26.5.0_happy-dom@20.14.0_vite@8.2.2__b2fde82745a4512744fb5cfb49e8ecf4/node_modules/vitest/dist/chunks/test.DNmyFkvJ.js
10991
10991
  var fakeTimersSrc = {};
10992
10992
  var global$1;
10993
10993
  var hasRequiredGlobal;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@morlay/session-rdb",
3
- "version": "0.0.16-alpha.0",
3
+ "version": "0.0.16-alpha.2",
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",
@@ -35,27 +35,27 @@
35
35
  "./cordis.patch.yml": "./cordis.patch.yml"
36
36
  },
37
37
  "dependencies": {
38
- "@morlay/session-branch": "^0.0.4-alpha.0",
38
+ "@morlay/session-branch": "^0.0.4-alpha.2",
39
39
  "drizzle-orm": "^1.0.0-rc.5-ab785fc",
40
40
  "fflate": "^0.8.2",
41
41
  "pg": "^8.23.0"
42
42
  },
43
43
  "devDependencies": {
44
- "@deepseek-ai/dsh-session-projection": "^0.1.3-alpha.2",
45
- "@deepseek-ai/dsh-token-meter": "^0.1.3-alpha.2",
44
+ "@deepseek-ai/dsh-session-projection": "^0.1.5-alpha.1",
45
+ "@deepseek-ai/dsh-token-meter": "^0.1.5-alpha.1",
46
46
  "@types/pg": "^8.23.1",
47
47
  "drizzle-kit": "1.0.0-rc.5-ab785fc"
48
48
  },
49
49
  "peerDependencies": {
50
50
  "@deepseek-ai/cordis": "^4.0.2",
51
- "@deepseek-ai/dsh-invariants": "^0.1.3-alpha.2",
52
- "@deepseek-ai/dsh-llm": "^0.1.3-alpha.2",
53
- "@deepseek-ai/dsh-scope": "^0.1.3-alpha.2",
54
- "@deepseek-ai/dsh-session": "^0.1.3-alpha.2",
55
- "@deepseek-ai/dsh-session-format": "^0.1.3-alpha.2",
56
- "@deepseek-ai/dsh-session-format-catalog": "^0.1.3-alpha.2",
57
- "@deepseek-ai/dsh-session-persistence": "^0.1.3-alpha.2",
58
- "@deepseek-ai/dsh-settings": "^0.1.3-alpha.2",
51
+ "@deepseek-ai/dsh-invariants": "^0.1.5-alpha.1",
52
+ "@deepseek-ai/dsh-llm": "^0.1.5-alpha.1",
53
+ "@deepseek-ai/dsh-scope": "^0.1.5-alpha.1",
54
+ "@deepseek-ai/dsh-session": "^0.1.5-alpha.1",
55
+ "@deepseek-ai/dsh-session-format": "^0.1.5-alpha.1",
56
+ "@deepseek-ai/dsh-session-format-catalog": "^0.1.5-alpha.1",
57
+ "@deepseek-ai/dsh-session-persistence": "^0.1.5-alpha.1",
58
+ "@deepseek-ai/dsh-settings": "^0.1.5-alpha.1",
59
59
  "@deepseek-ai/schemastery": "^3.18.2"
60
60
  },
61
61
  "dsh": {
package/src/index.ts CHANGED
@@ -39,14 +39,7 @@ import {
39
39
  } from "@deepseek-ai/dsh-session";
40
40
  import { type Backend, type BackendTx, type EventInsert } from "./backend.ts";
41
41
  import { WriteGuard } from "./write-guard.ts";
42
- import {
43
- recomputeReplaceProvenance,
44
- repairOrphanInboxSplices,
45
- repairSurfaceOps,
46
- rowToMeta,
47
- scanRows,
48
- toJsonlArtifact,
49
- } from "./log.ts";
42
+ import { repairReadView, rowToMeta, scanRows, toJsonlArtifact } from "./log.ts";
50
43
  import {
51
44
  DEFAULT_BUSY_TIMEOUT_MS,
52
45
  eventDimensions,
@@ -57,7 +50,7 @@ import { SqliteBackend } from "./sqlite.ts";
57
50
  import { PostgresBackend } from "./postgres.ts";
58
51
  import { SessionBranchRdb } from "./branch.ts";
59
52
  import { registerSessionImport } from "./import.ts";
60
- import { convertLegacyRows, isLegacyVersion } from "./legacy.ts";
53
+ import { adoptLegacyRows, convertLegacyRows, isLegacyVersion } from "./legacy.ts";
61
54
 
62
55
  export { SCHEMA_VERSION } from "./schema.ts";
63
56
  export { SessionBranchRdb, SessionBranchRdbProvider, locateTurnEnd } from "./branch.ts";
@@ -278,11 +271,10 @@ class RdbSessionHandle implements SessionHandle {
278
271
  }
279
272
  throw new SessionPersistenceNotFoundError(this.id);
280
273
  }
281
- // 读取时修复(视图只读,不落库):surface 替换 provenance 重计算、孤儿
282
- // inbox splice 改写、非法 surface 替换降级。
283
- repairSurfaceOps(log.events);
284
- recomputeReplaceProvenance(log.events);
285
- repairOrphanInboxSplices(log.events);
274
+ // 读取时修复(视图只读,不落库):结算字段补全、非法 surface 替换降级
275
+ // 或按 metering 数量夹取、metering range 对齐、provenance 重算、孤儿
276
+ // inbox splice 改写。
277
+ repairReadView(log.events);
286
278
  return { eventState: "detached", events: log.events.slice(offset, offset + length) };
287
279
  }
288
280
 
@@ -568,6 +560,10 @@ export class SessionPersistenceRdb extends SessionPersistence {
568
560
  }
569
561
  const log = await this.readLog(id, {}, options?.signal);
570
562
  if (log === undefined) throw new SessionPersistenceNotFoundError(id);
563
+ // 读取视图修复必须先于校验:越界 replace(旧写入器重编号遗留的旧坐标)
564
+ // 降级/夹取、request/header 归一,否则上游事件校验 fail loud 拒绝整个
565
+ // 会话(load / open 是历史会话的加载入口,不能只依赖 handle.read)。
566
+ repairReadView(log.events);
571
567
  // fail-closed:未知事件类型(非 ignorable)拒绝解释。
572
568
  validateStoredEvents(log.meta, log.events);
573
569
  return this.tracker.adopt(
@@ -592,7 +588,14 @@ export class SessionPersistenceRdb extends SessionPersistence {
592
588
  }
593
589
  const log = await this.readLog(id, {}, options?.signal);
594
590
  if (log === undefined) throw new SessionPersistenceNotFoundError(id);
591
+ repairReadView(log.events);
595
592
  validateStoredEvents(log.meta, log.events);
593
+ // 迁移链会生成/合并事件(end-seed / attempt / chunk 合并),事件坐标与
594
+ // 存储桥接行数不再相等——写打开时把迁移视图整体落库,使读写同坐标;
595
+ // 否则 append 按存储 head 重编号会撞上已有行或写坏 log。
596
+ if (log.migrated && log.events.length !== log.storedCount) {
597
+ await this.rewriteMigratedLog(id, log);
598
+ }
596
599
  // 确认 head:本实例已读该会话,后续 append 的并发校验以此为基准。
597
600
  this.writeGuard.confirmHead(id, log.events.at(-1)?.seq ?? -1);
598
601
  return this.tracker.adopt(
@@ -669,8 +672,7 @@ export class SessionPersistenceRdb extends SessionPersistence {
669
672
  signal?.throwIfAborted();
670
673
  const log = await this.readLog(id, {}, signal);
671
674
  if (log === undefined) return undefined;
672
- repairSurfaceOps(log.events);
673
- recomputeReplaceProvenance(log.events);
675
+ repairReadView(log.events);
674
676
  const inheritedEventCount = Math.min(log.inheritedEventCount, log.events.length);
675
677
  return {
676
678
  meta: log.meta,
@@ -695,9 +697,9 @@ export class SessionPersistenceRdb extends SessionPersistence {
695
697
  }
696
698
 
697
699
  /** 原样 append 落库(handle 已校验 contiguity;torn tail 先截断)。
698
- * 与上游 JSONL 一致:ignorable 事件原样存储,不做过滤。写路径校验 v2
699
- * 形状(fail-closed):未知类型(非 ignorable)与非法消息形状拒绝入库;
700
- * 旧格式(v0/v1)数据只在读取时经 legacy 转换链动态转换,不落新库。 */
700
+ * 与上游 JSONL 一致:ignorable 事件原样存储,不做过滤。写路径校验当前
701
+ * 格式形状(fail-closed):未知类型(非 ignorable)与非法消息形状拒绝入库;
702
+ * 非当前格式(v0/v1/v2)数据只在读取时经 legacy 转换链动态转换,不落新库。 */
701
703
  async appendBatch(
702
704
  meta: SessionHeader,
703
705
  inheritedEventCount: SessionLogOffset,
@@ -706,8 +708,8 @@ export class SessionPersistenceRdb extends SessionPersistence {
706
708
  ): Promise<boolean> {
707
709
  await this.ready;
708
710
  if (events.length === 0) return false;
709
- // 写路径 v2 校验:与读路径同契约(validateStoredEvents),保证新入库
710
- // 数据只能是 v2 形状。拷贝避免 adopt 替换污染调用方数组。
711
+ // 写路径当前格式校验:与读路径同契约(validateStoredEvents),保证新入库
712
+ // 数据只能是当前格式形状。拷贝避免 adopt 替换污染调用方数组。
711
713
  validateStoredEvents(meta, [...events]);
712
714
  // fork 派生会话的 seed 复用源会话事件行(不复制);消费后清除。
713
715
  const reuse = this.reuseEventIds.get(meta.id);
@@ -762,6 +764,12 @@ export class SessionPersistenceRdb extends SessionPersistence {
762
764
  incarnation: string;
763
765
 
764
766
  revision: number;
767
+
768
+ /** 存储桥接行数(迁移链可能生成/合并事件,与 `events.length` 不同)。 */
769
+ storedCount: number;
770
+
771
+ /** 是否经上游迁移链转换(非当前格式 → 当前格式)。 */
772
+ migrated: boolean;
765
773
  }
766
774
  | undefined
767
775
  > {
@@ -776,17 +784,39 @@ export class SessionPersistenceRdb extends SessionPersistence {
776
784
  ? await this.backend.getEventRows(id)
777
785
  : await this.backend.getEventRows(id, options.fromSeq);
778
786
  signal?.throwIfAborted();
779
- // 旧格式(v0/v1)历史数据:行重建为物理记录,经上游迁移链转 v2 逻辑事件。
780
- // 迁移链自带 seq gap / torn tail 校验(strict recovery),无需 scanRows。
787
+ // 非当前格式(v0/v1/v2)历史数据:行重建为物理记录,经上游迁移链转
788
+ // 当前逻辑事件。迁移链自带 seq gap / torn tail 校验(strict recovery),
789
+ // 无需 scanRows。混合世代 log(旧写入器跨上游版本追加)不是任何单一已
790
+ // 发布格式,迁移链必然拒绝——回退为当前格式视图(header 版本归一 +
791
+ // 读取视图修复)。
781
792
  if (isLegacyVersion(row.fVersion)) {
782
- const converted = convertLegacyRows(row, eventRows);
783
- return {
784
- meta: converted.meta,
785
- inheritedEventCount: converted.inheritedEventCount,
786
- events: converted.events,
787
- incarnation: row.fIncarnation,
788
- revision: row.fRevision,
789
- };
793
+ try {
794
+ const converted = convertLegacyRows(row, eventRows);
795
+ return {
796
+ meta: converted.meta,
797
+ inheritedEventCount: converted.inheritedEventCount,
798
+ events: converted.events,
799
+ incarnation: row.fIncarnation,
800
+ revision: row.fRevision,
801
+ storedCount: eventRows.length,
802
+ migrated: true,
803
+ };
804
+ } catch (error: unknown) {
805
+ this.ctx.logger.warn(
806
+ `session-rdb: session "${id}" is not a single released format; adopting its stored rows as current-format data (${error instanceof Error ? error.message : String(error)})`,
807
+ );
808
+ const adopted = adoptLegacyRows(row, eventRows);
809
+ return {
810
+ meta: adopted.meta,
811
+ inheritedEventCount: adopted.inheritedEventCount,
812
+ events: adopted.events,
813
+ incarnation: row.fIncarnation,
814
+ revision: row.fRevision,
815
+ storedCount: eventRows.length,
816
+ migrated: false,
817
+ ...(adopted.tornFrom !== undefined ? { tornFrom: adopted.tornFrom } : {}),
818
+ };
819
+ }
790
820
  }
791
821
  const { preserved, tornFrom } = scanRows(eventRows, options.fromSeq ?? 0);
792
822
  return {
@@ -795,10 +825,40 @@ export class SessionPersistenceRdb extends SessionPersistence {
795
825
  events: preserved,
796
826
  incarnation: row.fIncarnation,
797
827
  revision: row.fRevision,
828
+ storedCount: eventRows.length,
829
+ migrated: false,
798
830
  ...(tornFrom !== undefined ? { tornFrom } : {}),
799
831
  };
800
832
  }
801
833
 
834
+ /**
835
+ * 把迁移链读出的当前格式视图整体落库(非当前格式会话写打开时的一次性迁移)。
836
+ *
837
+ * 迁移链会生成/合并事件(end-seed / attempt / chunk 合并),事件 seq 空间
838
+ * 与存储桥接行数不再相等;写路径以存储 head 为锚点重编号,二者不一致会让
839
+ * append 撞上已有行。这里在同一事务内删光本会话桥接行、按迁移视图重建
840
+ * (新事件行,完整信封),并更新 head 与 revision;旧事件行保留(可能被
841
+ * fork 子会话引用,孤儿由惰性 GC 处理)。
842
+ */
843
+ private async rewriteMigratedLog(
844
+ id: SessionId,
845
+ log: { meta: SessionHeader; inheritedEventCount: number; events: SessionEvent[] },
846
+ ): Promise<void> {
847
+ await this.backend.transaction(async (tx) => {
848
+ await tx.deleteBridgeTail(id, 0);
849
+ await tx.upsertSession(
850
+ { meta: log.meta, inheritedEventCount: SessionLogOffset(log.inheritedEventCount) },
851
+ randomUUID(),
852
+ );
853
+ const { headEventId, headSequence } = await appendEventTail(tx, log.meta, log.events, {
854
+ parentId: "",
855
+ nextSeq: 0,
856
+ });
857
+ await tx.updateHead(id, headEventId, headSequence);
858
+ await tx.bumpRevision(id);
859
+ });
860
+ }
861
+
802
862
  async listSnapshots(
803
863
  signal?: AbortSignal,
804
864
  ): Promise<Array<SessionPersistenceSnapshot & { inheritedEventCount: number }>> {
@@ -873,8 +933,8 @@ export class SessionPersistenceRdb extends SessionPersistence {
873
933
  );
874
934
  const row = await this.backend.getSession(id);
875
935
  if (row === undefined) throw new SessionPersistenceNotFoundError(id);
876
- // 旧格式(v0/v1)会话:meta 与继承前缀来自转换链(handle.header 已是
877
- // 转换后的 v2 header);v2 会话用存储行。
936
+ // 非当前格式会话:meta 与继承前缀来自转换链(handle.header 已是转换后的
937
+ // 当前格式 header);当前格式会话用存储行。
878
938
  if (isLegacyVersion(row.fVersion)) {
879
939
  return {
880
940
  meta: handle.header,
@@ -1068,6 +1128,9 @@ export class SessionPersistenceRdb extends SessionPersistence {
1068
1128
  );
1069
1129
  }
1070
1130
  assertVersion(stored.meta);
1131
+ // adopt 比较必须与读取视图同源:live seed 来自修复后的读取视图(补
1132
+ // stream、surface 修复),未修复的存储视图会把修复差异误判为 id 冲突。
1133
+ repairReadView(stored.events);
1071
1134
  const seed = session.snapshotEvents();
1072
1135
  if (!seedCoversPrefix(seed, stored.events)) {
1073
1136
  throw new Error(