@morlay/session-rdb 0.0.16-alpha.1 → 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.
@@ -54,6 +54,14 @@ declare function repairSurfaceOps(events: SessionEvent[]): void;
54
54
  * 三者齐备,缺失会让整个会话加载失败。
55
55
  */
56
56
  declare function repairAssistantSettlement(events: SessionEvent[]): void;
57
+ /**
58
+ * 读取时把旧格式 `request/header` 归一为当前格式:v3 起 system prompt 由
59
+ * surface 上的 `system/message` 承载,header 必须省略 `system`,空 `tools` /
60
+ * `adapterDefaults` 也必须省略(上游 v3 事件校验 fail loud)。混合世代回退
61
+ * 视图直接采用存储行,不归一会让整个会话加载失败。system prompt 因此不再
62
+ * 进入模型请求;会话下次运行由 v3 的 system/message 机制重建。
63
+ */
64
+ declare function repairRequestHeaders(events: SessionEvent[]): void;
57
65
  /**
58
66
  * 把紧邻 replace 的 metering 事件的 `shadowedRange` / `shadowedSeqs` 对齐到
59
67
  * replace 最终的稠密 range。
@@ -67,8 +75,19 @@ declare function repairAssistantSettlement(events: SessionEvent[]): void;
67
75
  */
68
76
  declare function syncMeteringRanges(events: SessionEvent[]): void;
69
77
  /**
70
- * 读取视图修复总入口:结算字段补全 surface 语义修复 → metering 对齐 →
71
- * provenance 重算 → 孤儿 inbox splice 改写。全部只作用于内存视图,不落库。
78
+ * 读取时把 v2 时代的 PTC 词汇归一为当前词汇。
79
+ *
80
+ * 上游 v2→v3 迁移把 `tool/code-dispatch(-start)` 改名为
81
+ * `tool/ptc-dispatch(-start)`、把 `tools-code-mode` 插件来源改名为 `tools-ptc`、
82
+ * 把 agent preset 值 `code` 改名为 `ptc`。混合世代回退视图直接采用存储行,
83
+ * 不重命名会被上游 v3 校验以「unknown event type」拒绝整个会话。词汇改写
84
+ * 与上游迁移链的 renamePtcEvent 对齐。
85
+ */
86
+ declare function renameLegacyPtcEvents(events: SessionEvent[]): void;
87
+ /**
88
+ * 读取视图修复总入口:形状补全(结算字段 / request header)→ PTC 词汇归一
89
+ * → surface 语义修复 → metering 对齐 → provenance 重算 → 孤儿 inbox splice
90
+ * 改写。全部只作用于内存视图,不落库。
72
91
  */
73
92
  declare function repairReadView(events: SessionEvent[]): void;
74
93
  /**
@@ -88,4 +107,4 @@ declare function scanRows(rows: readonly EventRow[], base?: number): {
88
107
  };
89
108
  declare function toJsonlArtifact(meta: SessionHeader, inheritedEventCount: number, events: readonly SessionEvent[]): string;
90
109
  //#endregion
91
- export { SESSION_IMPORT_PATH, SESSION_LOG_ARTIFACT_FILENAME, findSurfaceRepairs, orphanInboxSpliceSeqs, parseImportZip, parseJsonlArtifact, persistImport, recomputeReplaceProvenance, registerSessionImport, repairAssistantSettlement, repairOrphanInboxSplices, repairReadView, repairSurfaceOps, rowToEvent, rowToMeta, scanRows, sessionConflictRow, sessionInsertRow, syncMeteringRanges, toJsonlArtifact };
110
+ export { SESSION_IMPORT_PATH, SESSION_LOG_ARTIFACT_FILENAME, findSurfaceRepairs, orphanInboxSpliceSeqs, parseImportZip, parseJsonlArtifact, persistImport, recomputeReplaceProvenance, registerSessionImport, renameLegacyPtcEvents, repairAssistantSettlement, repairOrphanInboxSplices, repairReadView, repairRequestHeaders, repairSurfaceOps, rowToEvent, rowToMeta, scanRows, sessionConflictRow, sessionInsertRow, syncMeteringRanges, toJsonlArtifact };
package/dist/artifact.mjs CHANGED
@@ -1,3 +1,3 @@
1
- import { a as repairOrphanInboxSplices, c as rowToEvent, d as sessionConflictRow, f as sessionInsertRow, i as repairAssistantSettlement, l as rowToMeta, m as toJsonlArtifact, n as orphanInboxSpliceSeqs, o as repairReadView, p as syncMeteringRanges, r as recomputeReplaceProvenance, s as repairSurfaceOps, t as findSurfaceRepairs, u as scanRows } from "./log-DBFUBPhv.mjs";
2
- import { a as persistImport, i as parseJsonlArtifact, n as SESSION_LOG_ARTIFACT_FILENAME, o as registerSessionImport, r as parseImportZip, t as SESSION_IMPORT_PATH } from "./import-mZZgDBLd.mjs";
3
- export { SESSION_IMPORT_PATH, SESSION_LOG_ARTIFACT_FILENAME, findSurfaceRepairs, orphanInboxSpliceSeqs, parseImportZip, parseJsonlArtifact, persistImport, recomputeReplaceProvenance, registerSessionImport, repairAssistantSettlement, repairOrphanInboxSplices, repairReadView, repairSurfaceOps, rowToEvent, rowToMeta, scanRows, sessionConflictRow, sessionInsertRow, syncMeteringRanges, toJsonlArtifact };
1
+ import { a as repairAssistantSettlement, c as repairRequestHeaders, d as rowToMeta, f as scanRows, g as toJsonlArtifact, h as syncMeteringRanges, i as renameLegacyPtcEvents, l as repairSurfaceOps, m as sessionInsertRow, n as orphanInboxSpliceSeqs, o as repairOrphanInboxSplices, p as sessionConflictRow, r as recomputeReplaceProvenance, s as repairReadView, t as findSurfaceRepairs, u as rowToEvent } from "./log-BIuXv09P.mjs";
2
+ import { a as persistImport, i as parseJsonlArtifact, n as SESSION_LOG_ARTIFACT_FILENAME, o as registerSessionImport, r as parseImportZip, t as SESSION_IMPORT_PATH } from "./import-eBSoCrxl.mjs";
3
+ export { SESSION_IMPORT_PATH, SESSION_LOG_ARTIFACT_FILENAME, findSurfaceRepairs, orphanInboxSpliceSeqs, parseImportZip, parseJsonlArtifact, persistImport, recomputeReplaceProvenance, registerSessionImport, renameLegacyPtcEvents, repairAssistantSettlement, repairOrphanInboxSplices, repairReadView, repairRequestHeaders, repairSurfaceOps, rowToEvent, rowToMeta, scanRows, sessionConflictRow, sessionInsertRow, syncMeteringRanges, toJsonlArtifact };
@@ -1,4 +1,4 @@
1
- import { l as rowToMeta } from "./log-DBFUBPhv.mjs";
1
+ import { d as rowToMeta } from "./log-BIuXv09P.mjs";
2
2
  import { randomUUID } from "node:crypto";
3
3
  import { SESSION_FORMAT_VERSION, SessionLogOffset } from "@deepseek-ai/dsh-session";
4
4
  import { sessionFormatCatalog } from "@deepseek-ai/dsh-session-format-catalog";
package/dist/import.d.mts CHANGED
@@ -1,4 +1,4 @@
1
- import { n as SessionPersistenceRdb } from "./index-GdKkSSb-.mjs";
1
+ import { n as SessionPersistenceRdb } from "./index-DfS0PzGM.mjs";
2
2
  import { SessionStorageMetadata } from "@deepseek-ai/dsh-session-persistence";
3
3
  import { Session, SessionEvent, SessionId } from "@deepseek-ai/dsh-session";
4
4
  import { Context } from "@deepseek-ai/cordis";
package/dist/import.mjs CHANGED
@@ -1,2 +1,2 @@
1
- import { a as persistImport, i as parseJsonlArtifact, n as SESSION_LOG_ARTIFACT_FILENAME, o as registerSessionImport, r as parseImportZip, t as SESSION_IMPORT_PATH } from "./import-mZZgDBLd.mjs";
1
+ import { a as persistImport, i as parseJsonlArtifact, n as SESSION_LOG_ARTIFACT_FILENAME, o as registerSessionImport, r as parseImportZip, t as SESSION_IMPORT_PATH } from "./import-eBSoCrxl.mjs";
2
2
  export { SESSION_IMPORT_PATH, SESSION_LOG_ARTIFACT_FILENAME, parseImportZip, parseJsonlArtifact, persistImport, registerSessionImport };
@@ -182,9 +182,9 @@ declare class SessionPersistenceRdb extends SessionPersistence {
182
182
  /** 空会话 materialize(flush 的持久化屏障)。 */
183
183
  materializeEmpty(meta: SessionHeader, inheritedEventCount: SessionLogOffset): Promise<void>;
184
184
  /** 原样 append 落库(handle 已校验 contiguity;torn tail 先截断)。
185
- * 与上游 JSONL 一致:ignorable 事件原样存储,不做过滤。写路径校验 v2
186
- * 形状(fail-closed):未知类型(非 ignorable)与非法消息形状拒绝入库;
187
- * 旧格式(v0/v1)数据只在读取时经 legacy 转换链动态转换,不落新库。 */
185
+ * 与上游 JSONL 一致:ignorable 事件原样存储,不做过滤。写路径校验当前
186
+ * 格式形状(fail-closed):未知类型(非 ignorable)与非法消息形状拒绝入库;
187
+ * 非当前格式(v0/v1/v2)数据只在读取时经 legacy 转换链动态转换,不落新库。 */
188
188
  appendBatch(meta: SessionHeader, inheritedEventCount: SessionLogOffset, events: readonly SessionEvent[], tornTruncateTo?: number): Promise<boolean>;
189
189
  /** 读取一个会话的稠密 log(含 torn tail 检测,不含修复改写)。 */
190
190
  readLog(id: SessionId, options?: {
@@ -198,11 +198,11 @@ declare class SessionPersistenceRdb extends SessionPersistence {
198
198
  revision: number;
199
199
  /** 存储桥接行数(迁移链可能生成/合并事件,与 `events.length` 不同)。 */
200
200
  storedCount: number;
201
- /** 是否经上游迁移链转换(v0/v1 → 当前格式)。 */
201
+ /** 是否经上游迁移链转换(非当前格式 → 当前格式)。 */
202
202
  migrated: boolean;
203
203
  } | undefined>;
204
204
  /**
205
- * 把迁移链读出的 v2 视图整体落库(旧格式会话写打开时的一次性迁移)。
205
+ * 把迁移链读出的当前格式视图整体落库(非当前格式会话写打开时的一次性迁移)。
206
206
  *
207
207
  * 迁移链会生成/合并事件(end-seed / attempt / chunk 合并),事件 seq 空间
208
208
  * 与存储桥接行数不再相等;写路径以存储 head 为锚点重编号,二者不一致会让
package/dist/index.d.mts CHANGED
@@ -1,3 +1,3 @@
1
1
  import { o as SCHEMA_VERSION } from "./schema-COK7-wRV.mjs";
2
- import { a as SessionBranchRdbProvider, i as SessionBranchRdb, n as SessionPersistenceRdb, o as locateTurnEnd, r as SessionPersistenceRdbInternals, t as Config } from "./index-GdKkSSb-.mjs";
2
+ import { a as SessionBranchRdbProvider, i as SessionBranchRdb, n as SessionPersistenceRdb, o as locateTurnEnd, r as SessionPersistenceRdbInternals, t as Config } from "./index-DfS0PzGM.mjs";
3
3
  export { Config, SCHEMA_VERSION, SessionBranchRdb, SessionBranchRdbProvider, SessionPersistenceRdb, SessionPersistenceRdb as default, SessionPersistenceRdbInternals, locateTurnEnd };
package/dist/index.mjs CHANGED
@@ -1,6 +1,6 @@
1
- import { a as SCHEMA_VERSION, g as WriteGuard, h as toPostgresSchema, i as EVENT_ENCODING, m as postgresTableDefs, r as DEFAULT_BUSY_TIMEOUT_MS, s as eventDimensions, t as SqliteBackend } from "./sqlite-CnWwGToZ.mjs";
2
- import { d as sessionConflictRow, f as sessionInsertRow, l as rowToMeta, m as toJsonlArtifact, o as repairReadView, u as scanRows } from "./log-DBFUBPhv.mjs";
3
- import { c as SessionBranchRdbProvider, l as locateTurnEnd, o as registerSessionImport, s as SessionBranchRdb } from "./import-mZZgDBLd.mjs";
1
+ import { a as SCHEMA_VERSION, g as WriteGuard, h as toPostgresSchema, i as EVENT_ENCODING, m as postgresTableDefs, r as DEFAULT_BUSY_TIMEOUT_MS, s as eventDimensions, t as SqliteBackend } from "./sqlite-Dpd17YhI.mjs";
2
+ import { c as repairRequestHeaders, d as rowToMeta, f as scanRows, g as toJsonlArtifact, m as sessionInsertRow, p as sessionConflictRow, s as repairReadView } from "./log-BIuXv09P.mjs";
3
+ import { c as SessionBranchRdbProvider, l as locateTurnEnd, o as registerSessionImport, s as SessionBranchRdb } from "./import-eBSoCrxl.mjs";
4
4
  import z from "@deepseek-ai/schemastery";
5
5
  import { randomUUID } from "node:crypto";
6
6
  import { Pool } from "pg";
@@ -161,35 +161,43 @@ var PostgresBackend = class PostgresBackend {
161
161
  };
162
162
  //#endregion
163
163
  //#region src/legacy.ts
164
- /** 重建 v0/v1 物理 header 记录(RDB 行 → 物理 JSON 对象)。 */
164
+ /** 重建 v0/v1/v2 物理 header 记录(RDB 行 → 物理 JSON 对象)。 */
165
165
  function physicalHeader(row) {
166
- return {
166
+ const common = {
167
167
  type: "session",
168
168
  version: row.fVersion,
169
169
  id: row.fSessionId,
170
170
  createdAt: row.fCreatedAt,
171
171
  ...row.fCwd !== null ? { cwd: row.fCwd } : {},
172
172
  ...row.fParentSession !== null ? { parentSession: row.fParentSession } : {},
173
- ...row.fSeedLength !== null ? { seedLength: row.fSeedLength } : {},
174
173
  ...row.fOrigin !== null ? { origin: row.fOrigin } : {},
175
174
  delegationDepth: row.fDelegationDepth ?? 0
176
175
  };
176
+ return row.fVersion < 2 ? {
177
+ ...common,
178
+ ...row.fSeedLength !== null ? { seedLength: row.fSeedLength } : {}
179
+ } : {
180
+ ...common,
181
+ isSeeded: row.fSeedLength !== null
182
+ };
177
183
  }
178
- /** 重建一条 v0/v1 物理事件记录(桥接行 + 事件行 → 物理 JSON 对象)。 */
184
+ /** 重建一条 v0/v1/v2 物理事件记录(桥接行 + 事件行 → 物理 JSON 对象)。 */
179
185
  function physicalEvent(row) {
186
+ const stored = JSON.parse(row.fData);
187
+ const full = typeof stored === "object" && stored !== null && !Array.isArray(stored) && typeof stored["type"] === "string" && "data" in stored;
180
188
  return {
181
189
  type: row.fType,
182
190
  seq: row.fSequence,
183
191
  time: row.fCreatedAt,
184
- data: JSON.parse(row.fData),
192
+ data: full ? stored["data"] : stored,
185
193
  ...row.fSurfaceOp !== null ? { surfaceOp: JSON.parse(row.fSurfaceOp) } : {}
186
194
  };
187
195
  }
188
196
  /**
189
- * 把旧格式(v0/v1)行转换为 v2 逻辑事件与 header。
190
- * @param row - 会话行(f_version < 2)。
197
+ * 把非当前格式(v0/v1/v2)行转换为当前逻辑事件与 header。
198
+ * @param row - 会话行(f_version < SESSION_FORMAT_VERSION)。
191
199
  * @param eventRows - 已扫描保留的桥接行(稠密 seq,torn tail 已剔除)。
192
- * @returns v2 逻辑 header、继承前缀长度与事件。
200
+ * @returns 当前逻辑 header、继承前缀长度与事件。
193
201
  * @throws 迁移链拒绝(格式损坏 / 无法迁移)时原样抛出。
194
202
  */
195
203
  function convertLegacyRows(row, eventRows) {
@@ -205,9 +213,9 @@ function convertLegacyRows(row, eventRows) {
205
213
  events: artifact.events
206
214
  };
207
215
  }
208
- /** 会话行是否携带旧格式(v0/v1)数据。 */
216
+ /** 会话行是否携带非当前格式(f_version < SESSION_FORMAT_VERSION)数据。 */
209
217
  function isLegacyVersion(version) {
210
- return version < 2;
218
+ return version < SESSION_FORMAT_VERSION;
211
219
  }
212
220
  /**
213
221
  * 迁移链拒绝后,把旧格式行直接当**当前格式**事件视图采用。
@@ -221,7 +229,7 @@ function isLegacyVersion(version) {
221
229
  * 调用方须在此之后执行 `validateStoredEvents`:真正属于旧格式的类型
222
230
  * (`assistant/chunk` / `compact/*` 等)仍会被 fail-loud 拒绝。
223
231
  *
224
- * @param row - 会话行(f_version < 2)。
232
+ * @param row - 会话行(f_version < SESSION_FORMAT_VERSION)。
225
233
  * @param eventRows - 该会话全部桥接行(按 f_sequence 升序)。
226
234
  * @returns 当前格式 header(version 归一到 SESSION_FORMAT_VERSION)、收缩后的
227
235
  * 继承前缀长度、稠密事件与可选 torn tail 起点。
@@ -229,6 +237,7 @@ function isLegacyVersion(version) {
229
237
  */
230
238
  function adoptLegacyRows(row, eventRows) {
231
239
  const { preserved, tornFrom } = scanRows(eventRows, 0);
240
+ repairRequestHeaders(preserved);
232
241
  return {
233
242
  meta: {
234
243
  ...rowToMeta(row),
@@ -578,6 +587,7 @@ var SessionPersistenceRdb = class SessionPersistenceRdb extends SessionPersisten
578
587
  }));
579
588
  const log = await this.readLog(id, {}, options?.signal);
580
589
  if (log === void 0) throw new SessionPersistenceNotFoundError(id);
590
+ repairReadView(log.events);
581
591
  validateStoredEvents(log.meta, log.events);
582
592
  return this.tracker.adopt(new RdbSessionHandle(this, id, log.meta, "read", {
583
593
  cursor: log.events.length,
@@ -594,6 +604,7 @@ var SessionPersistenceRdb = class SessionPersistenceRdb extends SessionPersisten
594
604
  }));
595
605
  const log = await this.readLog(id, {}, options?.signal);
596
606
  if (log === void 0) throw new SessionPersistenceNotFoundError(id);
607
+ repairReadView(log.events);
597
608
  validateStoredEvents(log.meta, log.events);
598
609
  if (log.migrated && log.events.length !== log.storedCount) await this.rewriteMigratedLog(id, log);
599
610
  this.writeGuard.confirmHead(id, log.events.at(-1)?.seq ?? -1);
@@ -682,9 +693,9 @@ var SessionPersistenceRdb = class SessionPersistenceRdb extends SessionPersisten
682
693
  this.writeGuard.confirmHead(meta.id, -1);
683
694
  }
684
695
  /** 原样 append 落库(handle 已校验 contiguity;torn tail 先截断)。
685
- * 与上游 JSONL 一致:ignorable 事件原样存储,不做过滤。写路径校验 v2
686
- * 形状(fail-closed):未知类型(非 ignorable)与非法消息形状拒绝入库;
687
- * 旧格式(v0/v1)数据只在读取时经 legacy 转换链动态转换,不落新库。 */
696
+ * 与上游 JSONL 一致:ignorable 事件原样存储,不做过滤。写路径校验当前
697
+ * 格式形状(fail-closed):未知类型(非 ignorable)与非法消息形状拒绝入库;
698
+ * 非当前格式(v0/v1/v2)数据只在读取时经 legacy 转换链动态转换,不落新库。 */
688
699
  async appendBatch(meta, inheritedEventCount, events, tornTruncateTo) {
689
700
  await this.ready;
690
701
  if (events.length === 0) return false;
@@ -765,7 +776,7 @@ var SessionPersistenceRdb = class SessionPersistenceRdb extends SessionPersisten
765
776
  };
766
777
  }
767
778
  /**
768
- * 把迁移链读出的 v2 视图整体落库(旧格式会话写打开时的一次性迁移)。
779
+ * 把迁移链读出的当前格式视图整体落库(非当前格式会话写打开时的一次性迁移)。
769
780
  *
770
781
  * 迁移链会生成/合并事件(end-seed / attempt / chunk 合并),事件 seq 空间
771
782
  * 与存储桥接行数不再相等;写路径以存储 head 为锚点重编号,二者不一致会让
@@ -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
  }
@@ -163,8 +186,8 @@ function findSurfaceRepairs(events) {
163
186
  continue;
164
187
  }
165
188
  const replace = typeof op === "object" && op !== null && !Array.isArray(op) ? op : void 0;
166
- const start = replace?.["start"];
167
- const end = replace?.["end"];
189
+ const start = replace?.["startSeq"];
190
+ const end = replace?.["endSeq"];
168
191
  const shapeOk = replace !== void 0 && replace["op"] === "replace" && isEventSeqLike(start) && isEventSeqLike(end);
169
192
  const startIdx = shapeOk ? nodes.indexOf(start) : -1;
170
193
  let endIdx = shapeOk ? nodes.indexOf(end) : -1;
@@ -174,6 +197,7 @@ function findSurfaceRepairs(events) {
174
197
  if (clamped !== void 0) endIdx = nodes.indexOf(clamped);
175
198
  }
176
199
  const rangeOk = shapeOk && startIdx !== -1 && endIdx !== -1 && startIdx <= endIdx;
200
+ const provenanceOk = event.type !== "assistant/message";
177
201
  let rewriteOk = true;
178
202
  if (rangeOk && event.type === "tool/result") {
179
203
  const shadowed = nodes.slice(startIdx, endIdx + 1);
@@ -183,7 +207,7 @@ function findSurfaceRepairs(events) {
183
207
  rewriteOk = original?.type === "tool/result" && toolResultRewriteContentOnly(original, event);
184
208
  }
185
209
  }
186
- if (!rangeOk || !rewriteOk) {
210
+ if (!rangeOk || !rewriteOk || !provenanceOk) {
187
211
  degradeToAppend.add(event.seq);
188
212
  nodes.push(event.seq);
189
213
  continue;
@@ -220,12 +244,14 @@ function repairSurfaceOps(events) {
220
244
  const raw = event;
221
245
  const clamped = repairs.clampEnd.get(event.seq);
222
246
  if (repairs.degradeToAppend.has(event.seq)) raw.surfaceOp = "append";
223
- else if (clamped !== void 0) raw.surfaceOp = {
224
- op: "replace",
225
- start: raw.surfaceOp.start,
226
- end: clamped
227
- };
228
- 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";
229
255
  else if (repairs.clearSurfaceOp.has(event.seq)) delete raw.surfaceOp;
230
256
  }
231
257
  }
@@ -242,6 +268,25 @@ function repairAssistantSettlement(events) {
242
268
  }
243
269
  }
244
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
+ /**
245
290
  * 把紧邻 replace 的 metering 事件的 `shadowedRange` / `shadowedSeqs` 对齐到
246
291
  * replace 最终的稠密 range。
247
292
  *
@@ -258,22 +303,100 @@ function syncMeteringRanges(events) {
258
303
  if (!METERING_EVENT_TYPES.has(metering.type)) continue;
259
304
  const op = events[i].surfaceOp;
260
305
  if (typeof op !== "object" || op === null || op.op !== "replace") continue;
261
- const { start, end } = op;
306
+ const { startSeq, endSeq } = op;
262
307
  const data = metering.data;
263
- if (data.shadowedRange?.start === start && data.shadowedRange.end === end) continue;
308
+ if (data.shadowedRange?.start === startSeq && data.shadowedRange.end === endSeq) continue;
264
309
  data.shadowedRange = {
265
- start,
266
- end
310
+ start: startSeq,
311
+ end: endSeq
267
312
  };
268
- data.shadowedSeqs = events.filter((candidate) => candidate.seq >= start && candidate.seq <= end && SURFACE_EVENT_TYPES.has(candidate.type)).map((candidate) => candidate.seq);
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
+ }
269
389
  }
270
390
  }
271
391
  /**
272
- * 读取视图修复总入口:结算字段补全 surface 语义修复 metering 对齐 →
273
- * provenance 重算 → 孤儿 inbox splice 改写。全部只作用于内存视图,不落库。
392
+ * 读取视图修复总入口:形状补全(结算字段 / request header)→ PTC 词汇归一
393
+ * → surface 语义修复 → metering 对齐 → provenance 重算 → 孤儿 inbox splice
394
+ * 改写。全部只作用于内存视图,不落库。
274
395
  */
275
396
  function repairReadView(events) {
276
397
  repairAssistantSettlement(events);
398
+ repairRequestHeaders(events);
399
+ renameLegacyPtcEvents(events);
277
400
  repairSurfaceOps(events);
278
401
  syncMeteringRanges(events);
279
402
  recomputeReplaceProvenance(events);
@@ -391,4 +514,4 @@ function toJsonlArtifact(meta, inheritedEventCount, events) {
391
514
  return lines.join("\n");
392
515
  }
393
516
  //#endregion
394
- export { repairOrphanInboxSplices as a, rowToEvent as c, sessionConflictRow as d, sessionInsertRow as f, repairAssistantSettlement as i, rowToMeta as l, toJsonlArtifact as m, orphanInboxSpliceSeqs as n, repairReadView as o, syncMeteringRanges as p, recomputeReplaceProvenance as r, repairSurfaceOps as s, findSurfaceRepairs as t, scanRows 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 { d as sessionConflictRow, f as sessionInsertRow } from "./log-DBFUBPhv.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-CnWwGToZ.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.1",
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.1",
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
@@ -560,6 +560,10 @@ export class SessionPersistenceRdb extends SessionPersistence {
560
560
  }
561
561
  const log = await this.readLog(id, {}, options?.signal);
562
562
  if (log === undefined) throw new SessionPersistenceNotFoundError(id);
563
+ // 读取视图修复必须先于校验:越界 replace(旧写入器重编号遗留的旧坐标)
564
+ // 降级/夹取、request/header 归一,否则上游事件校验 fail loud 拒绝整个
565
+ // 会话(load / open 是历史会话的加载入口,不能只依赖 handle.read)。
566
+ repairReadView(log.events);
563
567
  // fail-closed:未知事件类型(非 ignorable)拒绝解释。
564
568
  validateStoredEvents(log.meta, log.events);
565
569
  return this.tracker.adopt(
@@ -584,6 +588,7 @@ export class SessionPersistenceRdb extends SessionPersistence {
584
588
  }
585
589
  const log = await this.readLog(id, {}, options?.signal);
586
590
  if (log === undefined) throw new SessionPersistenceNotFoundError(id);
591
+ repairReadView(log.events);
587
592
  validateStoredEvents(log.meta, log.events);
588
593
  // 迁移链会生成/合并事件(end-seed / attempt / chunk 合并),事件坐标与
589
594
  // 存储桥接行数不再相等——写打开时把迁移视图整体落库,使读写同坐标;
@@ -692,9 +697,9 @@ export class SessionPersistenceRdb extends SessionPersistence {
692
697
  }
693
698
 
694
699
  /** 原样 append 落库(handle 已校验 contiguity;torn tail 先截断)。
695
- * 与上游 JSONL 一致:ignorable 事件原样存储,不做过滤。写路径校验 v2
696
- * 形状(fail-closed):未知类型(非 ignorable)与非法消息形状拒绝入库;
697
- * 旧格式(v0/v1)数据只在读取时经 legacy 转换链动态转换,不落新库。 */
700
+ * 与上游 JSONL 一致:ignorable 事件原样存储,不做过滤。写路径校验当前
701
+ * 格式形状(fail-closed):未知类型(非 ignorable)与非法消息形状拒绝入库;
702
+ * 非当前格式(v0/v1/v2)数据只在读取时经 legacy 转换链动态转换,不落新库。 */
698
703
  async appendBatch(
699
704
  meta: SessionHeader,
700
705
  inheritedEventCount: SessionLogOffset,
@@ -703,8 +708,8 @@ export class SessionPersistenceRdb extends SessionPersistence {
703
708
  ): Promise<boolean> {
704
709
  await this.ready;
705
710
  if (events.length === 0) return false;
706
- // 写路径 v2 校验:与读路径同契约(validateStoredEvents),保证新入库
707
- // 数据只能是 v2 形状。拷贝避免 adopt 替换污染调用方数组。
711
+ // 写路径当前格式校验:与读路径同契约(validateStoredEvents),保证新入库
712
+ // 数据只能是当前格式形状。拷贝避免 adopt 替换污染调用方数组。
708
713
  validateStoredEvents(meta, [...events]);
709
714
  // fork 派生会话的 seed 复用源会话事件行(不复制);消费后清除。
710
715
  const reuse = this.reuseEventIds.get(meta.id);
@@ -763,7 +768,7 @@ export class SessionPersistenceRdb extends SessionPersistence {
763
768
  /** 存储桥接行数(迁移链可能生成/合并事件,与 `events.length` 不同)。 */
764
769
  storedCount: number;
765
770
 
766
- /** 是否经上游迁移链转换(v0/v1 → 当前格式)。 */
771
+ /** 是否经上游迁移链转换(非当前格式 → 当前格式)。 */
767
772
  migrated: boolean;
768
773
  }
769
774
  | undefined
@@ -779,10 +784,11 @@ export class SessionPersistenceRdb extends SessionPersistence {
779
784
  ? await this.backend.getEventRows(id)
780
785
  : await this.backend.getEventRows(id, options.fromSeq);
781
786
  signal?.throwIfAborted();
782
- // 旧格式(v0/v1)历史数据:行重建为物理记录,经上游迁移链转 v2 逻辑事件。
783
- // 迁移链自带 seq gap / torn tail 校验(strict recovery),无需 scanRows。
784
- // 混合世代 log(旧写入器跨上游版本追加)不是任何单一已发布格式,迁移链
785
- // 必然拒绝——回退为当前格式视图(header 版本归一 + 读取视图修复)。
787
+ // 非当前格式(v0/v1/v2)历史数据:行重建为物理记录,经上游迁移链转
788
+ // 当前逻辑事件。迁移链自带 seq gap / torn tail 校验(strict recovery),
789
+ // 无需 scanRows。混合世代 log(旧写入器跨上游版本追加)不是任何单一已
790
+ // 发布格式,迁移链必然拒绝——回退为当前格式视图(header 版本归一 +
791
+ // 读取视图修复)。
786
792
  if (isLegacyVersion(row.fVersion)) {
787
793
  try {
788
794
  const converted = convertLegacyRows(row, eventRows);
@@ -826,7 +832,7 @@ export class SessionPersistenceRdb extends SessionPersistence {
826
832
  }
827
833
 
828
834
  /**
829
- * 把迁移链读出的 v2 视图整体落库(旧格式会话写打开时的一次性迁移)。
835
+ * 把迁移链读出的当前格式视图整体落库(非当前格式会话写打开时的一次性迁移)。
830
836
  *
831
837
  * 迁移链会生成/合并事件(end-seed / attempt / chunk 合并),事件 seq 空间
832
838
  * 与存储桥接行数不再相等;写路径以存储 head 为锚点重编号,二者不一致会让
@@ -927,8 +933,8 @@ export class SessionPersistenceRdb extends SessionPersistence {
927
933
  );
928
934
  const row = await this.backend.getSession(id);
929
935
  if (row === undefined) throw new SessionPersistenceNotFoundError(id);
930
- // 旧格式(v0/v1)会话:meta 与继承前缀来自转换链(handle.header 已是
931
- // 转换后的 v2 header);v2 会话用存储行。
936
+ // 非当前格式会话:meta 与继承前缀来自转换链(handle.header 已是转换后的
937
+ // 当前格式 header);当前格式会话用存储行。
932
938
  if (isLegacyVersion(row.fVersion)) {
933
939
  return {
934
940
  meta: handle.header,
package/src/legacy.ts CHANGED
@@ -1,5 +1,5 @@
1
- // 旧格式(v0/v1)历史数据读取转换:RDB 行 → v0/v1 物理记录 → 上游迁移链 →
2
- // v2 逻辑事件。RDB 写路径从不持久化 sourceEventSeqs(读取时重计算),且
1
+ // 非当前格式(v0/v1/v2)历史数据读取转换:RDB 行 → 物理记录 → 上游迁移链 →
2
+ // 当前逻辑事件。RDB 写路径从不持久化 sourceEventSeqs(读取时重计算),且
3
3
  // delta(assistant/chunk)落盘被过滤——v1→v2 迁移对无 chunk 引用的
4
4
  // assistant/message 走「无 pending 直接 emit」路径,生成 stream: [] 的 v2 事件。
5
5
 
@@ -11,39 +11,52 @@ import {
11
11
  } from "@deepseek-ai/dsh-session";
12
12
  import { sessionFormatCatalog } from "@deepseek-ai/dsh-session-format-catalog";
13
13
  import type { EventRow, SessionRow } from "./backend.ts";
14
- import { rowToMeta, scanRows } from "./log.ts";
14
+ import { repairRequestHeaders, rowToMeta, scanRows } from "./log.ts";
15
15
 
16
- /** 重建 v0/v1 物理 header 记录(RDB 行 → 物理 JSON 对象)。 */
16
+ /** 重建 v0/v1/v2 物理 header 记录(RDB 行 → 物理 JSON 对象)。 */
17
17
  function physicalHeader(row: SessionRow): Record<string, unknown> {
18
- return {
18
+ const common = {
19
19
  type: "session",
20
20
  version: row.fVersion,
21
21
  id: row.fSessionId,
22
22
  createdAt: row.fCreatedAt,
23
23
  ...(row.fCwd !== null ? { cwd: row.fCwd } : {}),
24
24
  ...(row.fParentSession !== null ? { parentSession: row.fParentSession } : {}),
25
- ...(row.fSeedLength !== null ? { seedLength: row.fSeedLength } : {}),
26
25
  ...(row.fOrigin !== null ? { origin: row.fOrigin } : {}),
27
26
  delegationDepth: row.fDelegationDepth ?? 0,
28
27
  };
28
+ // v0/v1 物理 header 用 seedLength 表达继承前缀;v2 起改用 isSeeded,
29
+ // 前缀长度由 log 里的 session/end-seed 标记推导。
30
+ return row.fVersion < 2
31
+ ? { ...common, ...(row.fSeedLength !== null ? { seedLength: row.fSeedLength } : {}) }
32
+ : { ...common, isSeeded: row.fSeedLength !== null };
29
33
  }
30
34
 
31
- /** 重建一条 v0/v1 物理事件记录(桥接行 + 事件行 → 物理 JSON 对象)。 */
35
+ /** 重建一条 v0/v1/v2 物理事件记录(桥接行 + 事件行 → 物理 JSON 对象)。 */
32
36
  function physicalEvent(row: EventRow): Record<string, unknown> {
37
+ const stored = JSON.parse(row.fData) as unknown;
38
+ // 两种存储形状:当前写入器落完整事件信封(与 JSONL 每行同构),schema v2
39
+ // 时代只落 data 部分。迁移链要的是 payload,故信封形状取 `data`。
40
+ const full =
41
+ typeof stored === "object" &&
42
+ stored !== null &&
43
+ !Array.isArray(stored) &&
44
+ typeof (stored as Record<string, unknown>)["type"] === "string" &&
45
+ "data" in (stored as Record<string, unknown>);
33
46
  return {
34
47
  type: row.fType,
35
48
  seq: row.fSequence,
36
49
  time: row.fCreatedAt,
37
- data: JSON.parse(row.fData) as unknown,
50
+ data: full ? (stored as Record<string, unknown>)["data"] : stored,
38
51
  ...(row.fSurfaceOp !== null ? { surfaceOp: JSON.parse(row.fSurfaceOp) as unknown } : {}),
39
52
  };
40
53
  }
41
54
 
42
55
  /**
43
- * 把旧格式(v0/v1)行转换为 v2 逻辑事件与 header。
44
- * @param row - 会话行(f_version < 2)。
56
+ * 把非当前格式(v0/v1/v2)行转换为当前逻辑事件与 header。
57
+ * @param row - 会话行(f_version < SESSION_FORMAT_VERSION)。
45
58
  * @param eventRows - 已扫描保留的桥接行(稠密 seq,torn tail 已剔除)。
46
- * @returns v2 逻辑 header、继承前缀长度与事件。
59
+ * @returns 当前逻辑 header、继承前缀长度与事件。
47
60
  * @throws 迁移链拒绝(格式损坏 / 无法迁移)时原样抛出。
48
61
  */
49
62
  export function convertLegacyRows(
@@ -65,9 +78,9 @@ export function convertLegacyRows(
65
78
  };
66
79
  }
67
80
 
68
- /** 会话行是否携带旧格式(v0/v1)数据。 */
81
+ /** 会话行是否携带非当前格式(f_version < SESSION_FORMAT_VERSION)数据。 */
69
82
  export function isLegacyVersion(version: number): boolean {
70
- return version < 2;
83
+ return version < SESSION_FORMAT_VERSION;
71
84
  }
72
85
 
73
86
  /**
@@ -82,7 +95,7 @@ export function isLegacyVersion(version: number): boolean {
82
95
  * 调用方须在此之后执行 `validateStoredEvents`:真正属于旧格式的类型
83
96
  * (`assistant/chunk` / `compact/*` 等)仍会被 fail-loud 拒绝。
84
97
  *
85
- * @param row - 会话行(f_version < 2)。
98
+ * @param row - 会话行(f_version < SESSION_FORMAT_VERSION)。
86
99
  * @param eventRows - 该会话全部桥接行(按 f_sequence 升序)。
87
100
  * @returns 当前格式 header(version 归一到 SESSION_FORMAT_VERSION)、收缩后的
88
101
  * 继承前缀长度、稠密事件与可选 torn tail 起点。
@@ -98,6 +111,11 @@ export function adoptLegacyRows(
98
111
  tornFrom?: number;
99
112
  } {
100
113
  const { preserved, tornFrom } = scanRows(eventRows, 0);
114
+ // 旧格式 request/header 归一为当前格式:v3 起 system prompt 由 surface 上的
115
+ // system/message 承载,header 必须省略 system / 空 tools / 空 adapterDefaults
116
+ // (上游事件校验 fail loud)。归一必须发生在调用方 `validateStoredEvents`
117
+ // 之前,否则整个会话加载失败。
118
+ repairRequestHeaders(preserved);
101
119
  return {
102
120
  meta: { ...rowToMeta(row), version: SESSION_FORMAT_VERSION },
103
121
  // 继承前缀不得超过事件数(历史 rewind 未收缩的损坏样式),上游 load 判损坏。
package/src/log.ts CHANGED
@@ -1,9 +1,30 @@
1
+ import { SessionSeq } from "@deepseek-ai/dsh-session";
1
2
  import type { SessionEvent, SessionHeader, SessionId, SurfaceOp } from "@deepseek-ai/dsh-session";
2
3
  import type { SessionFormatEvent, SessionFormatHeader } from "@deepseek-ai/dsh-session-format";
3
4
  import { sessionFormatCatalog } from "@deepseek-ai/dsh-session-format-catalog";
4
5
  import type { SessionStorageMetadata } from "@deepseek-ai/dsh-session-persistence";
5
6
  import type { EventRow, SessionRow } from "./backend.ts";
6
7
 
8
+ /**
9
+ * 把桥接行列里的 replace surfaceOp 归一到当前字段名。
10
+ *
11
+ * v2 时代落库的 JSON 用 `start`/`end`,当前格式用 `startSeq`/`endSeq`;
12
+ * 混合世代回退路径直接采用存储行,必须在此归一,否则上游 v3 surface 校验
13
+ * 以「invalid replace surfaceOp」拒绝整个会话。非 replace 形状原样返回,
14
+ * 由读取视图修复决定降级。
15
+ */
16
+ function normalizeSurfaceOp(value: unknown): SurfaceOp {
17
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
18
+ return value as SurfaceOp;
19
+ }
20
+ const record = value as Record<string, unknown>;
21
+ if (record["op"] !== "replace") return value as SurfaceOp;
22
+ const startSeq = record["startSeq"] ?? record["start"];
23
+ const endSeq = record["endSeq"] ?? record["end"];
24
+ if (typeof startSeq !== "number" || typeof endSeq !== "number") return value as SurfaceOp;
25
+ return { op: "replace", startSeq: SessionSeq(startSeq), endSeq: SessionSeq(endSeq) };
26
+ }
27
+
7
28
  export function rowToMeta(row: SessionRow): SessionHeader {
8
29
  if (!Number.isSafeInteger(row.fCreatedAt) || row.fCreatedAt < 0) {
9
30
  throw new Error("stored session createdAt must be a non-negative safe integer");
@@ -76,7 +97,8 @@ export function sessionConflictRow(storage: SessionStorageMetadata): {
76
97
  }
77
98
 
78
99
  export function rowToEvent(row: EventRow): SessionEvent {
79
- const surfaceOp = row.fSurfaceOp !== null ? (JSON.parse(row.fSurfaceOp) as SurfaceOp) : undefined;
100
+ const surfaceOp =
101
+ row.fSurfaceOp !== null ? normalizeSurfaceOp(JSON.parse(row.fSurfaceOp) as unknown) : undefined;
80
102
  const record = JSON.parse(row.fData) as unknown;
81
103
  // fData 形状判别:新写入的完整事件(含 ignorable 信封,与 JSONL 每行
82
104
  // 同构)vs 旧 v2 库的纯 data 部分。完整事件必有 type/seq/time/data 四键。
@@ -107,7 +129,12 @@ export function rowToEvent(row: EventRow): SessionEvent {
107
129
  } as SessionEvent;
108
130
  }
109
131
 
110
- const SURFACE_EVENT_TYPES = new Set(["user/message", "assistant/message", "tool/result"]);
132
+ const SURFACE_EVENT_TYPES = new Set([
133
+ "system/message",
134
+ "user/message",
135
+ "assistant/message",
136
+ "tool/result",
137
+ ]);
111
138
 
112
139
  const METERING_EVENT_TYPES = new Set(["compaction/summary", "compaction/prune"]);
113
140
 
@@ -126,12 +153,12 @@ const METERING_EVENT_TYPES = new Set(["compaction/summary", "compaction/prune"])
126
153
  export function recomputeReplaceProvenance(events: SessionEvent[]): void {
127
154
  for (let i = 0; i < events.length; i++) {
128
155
  const event = events[i]!;
129
- const raw = event as SessionEvent & { surfaceOp?: unknown; sourceEventSeqs?: number[] };
156
+ const raw = event as unknown as { surfaceOp?: unknown; sourceEventSeqs?: number[] };
130
157
  const op = raw.surfaceOp;
131
158
  if (typeof op !== "object" || op === null || (op as { op?: string }).op !== "replace") {
132
159
  continue;
133
160
  }
134
- const { start, end } = op as { start: number; end: number };
161
+ const { startSeq, endSeq } = op as { startSeq: number; endSeq: number };
135
162
  const metering = i > 0 ? events[i - 1] : undefined;
136
163
  const meteringData =
137
164
  metering !== undefined && METERING_EVENT_TYPES.has(metering.type)
@@ -144,8 +171,8 @@ export function recomputeReplaceProvenance(events: SessionEvent[]): void {
144
171
  const refs: number[] = [];
145
172
  for (const candidate of events) {
146
173
  if (
147
- candidate.seq >= start &&
148
- candidate.seq <= end &&
174
+ candidate.seq >= startSeq &&
175
+ candidate.seq <= endSeq &&
149
176
  SURFACE_EVENT_TYPES.has(candidate.type)
150
177
  ) {
151
178
  refs.push(candidate.seq);
@@ -242,8 +269,8 @@ export function findSurfaceRepairs(events: readonly SessionEvent[]): {
242
269
  typeof op === "object" && op !== null && !Array.isArray(op)
243
270
  ? (op as Record<string, unknown>)
244
271
  : undefined;
245
- const start = replace?.["start"];
246
- const end = replace?.["end"];
272
+ const start = replace?.["startSeq"];
273
+ const end = replace?.["endSeq"];
247
274
  const shapeOk =
248
275
  replace !== undefined &&
249
276
  replace["op"] === "replace" &&
@@ -261,6 +288,10 @@ export function findSurfaceRepairs(events: readonly SessionEvent[]): {
261
288
  if (clamped !== undefined) endIdx = nodes.indexOf(clamped);
262
289
  }
263
290
  const rangeOk = shapeOk && startIdx !== -1 && endIdx !== -1 && startIdx <= endIdx;
291
+ // assistant/message 的来源内嵌在 stream,上游禁止其携带 sourceEventSeqs,
292
+ // replace 的 provenance 因此永远无法满足 fold 校验——降级 append 是唯一
293
+ // 可加载形态。
294
+ const provenanceOk = event.type !== "assistant/message";
264
295
  let rewriteOk = true;
265
296
  if (rangeOk && event.type === "tool/result") {
266
297
  const shadowed = nodes.slice(startIdx, endIdx + 1);
@@ -272,7 +303,7 @@ export function findSurfaceRepairs(events: readonly SessionEvent[]): {
272
303
  original?.type === "tool/result" && toolResultRewriteContentOnly(original, event);
273
304
  }
274
305
  }
275
- if (!rangeOk || !rewriteOk) {
306
+ if (!rangeOk || !rewriteOk || !provenanceOk) {
276
307
  degradeToAppend.add(event.seq);
277
308
  nodes.push(event.seq);
278
309
  continue;
@@ -320,8 +351,12 @@ export function repairSurfaceOps(events: SessionEvent[]): void {
320
351
  if (repairs.degradeToAppend.has(event.seq)) {
321
352
  raw.surfaceOp = "append";
322
353
  } else if (clamped !== undefined) {
323
- const op = raw.surfaceOp as { start: number };
324
- raw.surfaceOp = { op: "replace", start: op.start, end: clamped };
354
+ const op = raw.surfaceOp as { startSeq: number };
355
+ raw.surfaceOp = {
356
+ op: "replace",
357
+ startSeq: SessionSeq(op.startSeq),
358
+ endSeq: SessionSeq(clamped),
359
+ };
325
360
  } else if (repairs.addAppendMarker.has(event.seq)) {
326
361
  raw.surfaceOp = "append";
327
362
  } else if (repairs.clearSurfaceOp.has(event.seq)) {
@@ -343,6 +378,34 @@ export function repairAssistantSettlement(events: SessionEvent[]): void {
343
378
  }
344
379
  }
345
380
 
381
+ /**
382
+ * 读取时把旧格式 `request/header` 归一为当前格式:v3 起 system prompt 由
383
+ * surface 上的 `system/message` 承载,header 必须省略 `system`,空 `tools` /
384
+ * `adapterDefaults` 也必须省略(上游 v3 事件校验 fail loud)。混合世代回退
385
+ * 视图直接采用存储行,不归一会让整个会话加载失败。system prompt 因此不再
386
+ * 进入模型请求;会话下次运行由 v3 的 system/message 机制重建。
387
+ */
388
+ export function repairRequestHeaders(events: SessionEvent[]): void {
389
+ for (const event of events) {
390
+ if (event.type !== "request/header") continue;
391
+ const data = event.data as unknown as Record<string, unknown>;
392
+ const header = data["header"];
393
+ if (typeof header !== "object" || header === null || Array.isArray(header)) continue;
394
+ const record = header as Record<string, unknown>;
395
+ delete record["system"];
396
+ if (Array.isArray(record["tools"]) && record["tools"].length === 0) delete record["tools"];
397
+ const defaults = record["adapterDefaults"];
398
+ if (
399
+ typeof defaults === "object" &&
400
+ defaults !== null &&
401
+ !Array.isArray(defaults) &&
402
+ Object.keys(defaults).length === 0
403
+ ) {
404
+ delete record["adapterDefaults"];
405
+ }
406
+ }
407
+ }
408
+
346
409
  /**
347
410
  * 把紧邻 replace 的 metering 事件的 `shadowedRange` / `shadowedSeqs` 对齐到
348
411
  * replace 最终的稠密 range。
@@ -361,28 +424,108 @@ export function syncMeteringRanges(events: SessionEvent[]): void {
361
424
  const event = events[i]!;
362
425
  const op = (event as SessionEvent & { surfaceOp?: unknown }).surfaceOp;
363
426
  if (typeof op !== "object" || op === null || (op as { op?: string }).op !== "replace") continue;
364
- const { start, end } = op as { start: number; end: number };
427
+ const { startSeq, endSeq } = op as { startSeq: number; endSeq: number };
365
428
  const data = metering.data as unknown as {
366
429
  shadowedRange?: { start: number; end: number };
367
430
  shadowedSeqs?: number[];
368
431
  };
369
- if (data.shadowedRange?.start === start && data.shadowedRange.end === end) continue;
370
- data.shadowedRange = { start, end };
432
+ if (data.shadowedRange?.start === startSeq && data.shadowedRange.end === endSeq) continue;
433
+ data.shadowedRange = { start: startSeq, end: endSeq };
371
434
  data.shadowedSeqs = events
372
435
  .filter(
373
436
  (candidate) =>
374
- candidate.seq >= start && candidate.seq <= end && SURFACE_EVENT_TYPES.has(candidate.type),
437
+ candidate.seq >= startSeq &&
438
+ candidate.seq <= endSeq &&
439
+ SURFACE_EVENT_TYPES.has(candidate.type),
375
440
  )
376
441
  .map((candidate) => candidate.seq);
377
442
  }
378
443
  }
379
444
 
445
+ const PTC_EVENT_RENAMES: Record<string, string> = {
446
+ "tool/code-dispatch-start": "tool/ptc-dispatch-start",
447
+ "tool/code-dispatch": "tool/ptc-dispatch",
448
+ };
449
+
450
+ /** 把消息的 `tools-code-mode` 插件来源改写为 `tools-ptc`(非该来源原样返回)。 */
451
+ function renamePtcMessageSource(message: unknown): unknown {
452
+ if (typeof message !== "object" || message === null || Array.isArray(message)) return message;
453
+ const record = message as Record<string, unknown>;
454
+ const source = record["source"];
455
+ if (typeof source !== "object" || source === null || Array.isArray(source)) return message;
456
+ const sourceRecord = source as Record<string, unknown>;
457
+ if (sourceRecord["kind"] !== "plugin" || sourceRecord["plugin"] !== "tools-code-mode") {
458
+ return message;
459
+ }
460
+ return { ...record, source: { ...sourceRecord, plugin: "tools-ptc" } };
461
+ }
462
+
463
+ /** 宽类型事件视图:PTC 词汇归一涉及本包 SessionEventMap 之外的插件类型。 */
464
+ interface LegacyPtcEvent {
465
+ type: string;
466
+ data: unknown;
467
+ [key: string]: unknown;
468
+ }
469
+
470
+ /**
471
+ * 读取时把 v2 时代的 PTC 词汇归一为当前词汇。
472
+ *
473
+ * 上游 v2→v3 迁移把 `tool/code-dispatch(-start)` 改名为
474
+ * `tool/ptc-dispatch(-start)`、把 `tools-code-mode` 插件来源改名为 `tools-ptc`、
475
+ * 把 agent preset 值 `code` 改名为 `ptc`。混合世代回退视图直接采用存储行,
476
+ * 不重命名会被上游 v3 校验以「unknown event type」拒绝整个会话。词汇改写
477
+ * 与上游迁移链的 renamePtcEvent 对齐。
478
+ */
479
+ export function renameLegacyPtcEvents(events: SessionEvent[]): void {
480
+ for (let index = 0; index < events.length; index++) {
481
+ const event = events[index] as unknown as LegacyPtcEvent;
482
+ const renamedType = PTC_EVENT_RENAMES[event.type];
483
+ if (renamedType !== undefined) {
484
+ events[index] = { ...event, type: renamedType } as unknown as SessionEvent;
485
+ continue;
486
+ }
487
+ if (event.type === "agent-preset/selected") {
488
+ const data = event.data as Record<string, unknown>;
489
+ if (data["agentPreset"] === "code") {
490
+ events[index] = {
491
+ ...event,
492
+ data: { ...data, agentPreset: "ptc" },
493
+ } as unknown as SessionEvent;
494
+ }
495
+ continue;
496
+ }
497
+ if (event.type === "user/message") {
498
+ const renamed = renamePtcMessageSource(event.data);
499
+ if (renamed !== event.data) {
500
+ events[index] = { ...event, data: renamed } as unknown as SessionEvent;
501
+ }
502
+ continue;
503
+ }
504
+ if (event.type === "agent/inbox/spliced" || event.type === "session/title-llm-request") {
505
+ const data = event.data as Record<string, unknown>;
506
+ const key = event.type === "agent/inbox/spliced" ? "inserted" : "messages";
507
+ const list = data[key];
508
+ if (!Array.isArray(list)) continue;
509
+ const renamed = list.map(renamePtcMessageSource);
510
+ if (renamed.some((message, position) => message !== list[position])) {
511
+ events[index] = {
512
+ ...event,
513
+ data: { ...data, [key]: renamed },
514
+ } as unknown as SessionEvent;
515
+ }
516
+ }
517
+ }
518
+ }
519
+
380
520
  /**
381
- * 读取视图修复总入口:结算字段补全 surface 语义修复 metering 对齐 →
382
- * provenance 重算 → 孤儿 inbox splice 改写。全部只作用于内存视图,不落库。
521
+ * 读取视图修复总入口:形状补全(结算字段 / request header)→ PTC 词汇归一
522
+ * → surface 语义修复 → metering 对齐 → provenance 重算 → 孤儿 inbox splice
523
+ * 改写。全部只作用于内存视图,不落库。
383
524
  */
384
525
  export function repairReadView(events: SessionEvent[]): void {
385
526
  repairAssistantSettlement(events);
527
+ repairRequestHeaders(events);
528
+ renameLegacyPtcEvents(events);
386
529
  repairSurfaceOps(events);
387
530
  syncMeteringRanges(events);
388
531
  recomputeReplaceProvenance(events);
@@ -535,7 +535,7 @@ export function runPersistenceContract(name: string, make: () => Promise<Contrac
535
535
  const m = meta("foreign-vocabulary");
536
536
  const handle = await persistence.create(m);
537
537
  await handle.append(oneTurnLog());
538
- // 写路径 v2 校验:未知类型(非 ignorable)拒绝入库,批次不落库。
538
+ // 写路径当前格式校验:未知类型(非 ignorable)拒绝入库,批次不落库。
539
539
  await expect(
540
540
  handle.append([
541
541
  { type: "mystery/event", seq: SessionSeq(6), time: 7, data: { payload: true } },