@morlay/session-rdb 0.0.16-alpha.1 → 0.0.16-alpha.4

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-BPEHfHNk.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";
@@ -142,7 +142,8 @@ var SessionBranchRdbProvider = class {
142
142
  if (!Number.isSafeInteger(toBoundary) || toBoundary < -1) throw new SessionBranchError(`rewind boundary must be a non-negative safe integer, got ${toBoundary}`, "INVALID_BOUNDARY");
143
143
  const live = this.live.getSession(id);
144
144
  if (live !== void 0) await this.live.flush(live);
145
- const raw = live === void 0 ? await this.readRawEvents(id, signal) : void 0;
145
+ const raw = live === void 0 ? await this.persistence.readLog(id, {}, signal) : void 0;
146
+ if (live === void 0 && raw === void 0) throw new SessionBranchError(`session "${id}" not found`, "SESSION_NOT_FOUND");
146
147
  const inspection = live === void 0 ? void 0 : await this.persistence.internals().inspect(id, signal);
147
148
  const events = live === void 0 ? raw.events : inspection.events;
148
149
  const meta = live === void 0 ? raw.meta : inspection.meta;
@@ -150,7 +151,8 @@ var SessionBranchRdbProvider = class {
150
151
  if (toBoundary === -1) {} else if (boundaryEvent === void 0) throw new SessionBranchError(`rewind boundary ${toBoundary} does not exist in session "${id}"`, "INVALID_BOUNDARY");
151
152
  else if (boundaryEvent.type !== "turn/end" && boundaryEvent.type !== "user/message") throw new SessionBranchError(`rewind boundary ${toBoundary} is not a turn/end or user/message (${boundaryEvent.type})`, "INVALID_BOUNDARY");
152
153
  const rawKeepLength = toBoundary === -1 ? 0 : boundaryEvent.type === "turn/end" ? toBoundary + 1 : toBoundary;
153
- const keepLength = balanceRewindPrefix(events.slice(0, rawKeepLength)).length;
154
+ const kept = balanceRewindPrefix(events.slice(0, rawKeepLength));
155
+ const keepLength = kept.length;
154
156
  const internals = this.persistence.internals();
155
157
  const denseBoundary = keepLength - 1;
156
158
  const newSeedLength = await internals.backend.transaction(async (tx) => {
@@ -175,6 +177,7 @@ var SessionBranchRdbProvider = class {
175
177
  internals.writeGuard.confirmHead(id, denseBoundary);
176
178
  if (live !== void 0) {
177
179
  truncateLiveSession(live, keepLength);
180
+ this.live.resetProjections?.(live);
178
181
  const agent = this.live.getAgent(id);
179
182
  if (agent !== void 0) {
180
183
  agent.requestHeaderLogged = false;
@@ -184,7 +187,15 @@ var SessionBranchRdbProvider = class {
184
187
  }
185
188
  const handle = this.persistence.tracker.writerOf(id);
186
189
  if (handle !== void 0) handle.resetAfterRewind(keepLength, newSeedLength === null ? void 0 : newSeedLength);
187
- }
190
+ agent?.inbox?.clear();
191
+ await this.live.flush(live);
192
+ await this.refreshProjectionCache(live);
193
+ } else await this.refreshProjectionCache({
194
+ id,
195
+ header: meta,
196
+ inheritedEventCount: SessionLogOffset(raw.inheritedEventCount),
197
+ snapshotEvents: () => kept
198
+ });
188
199
  const row = await internals.backend.getSession(id);
189
200
  if (row === void 0) return {
190
201
  header: {
@@ -205,6 +216,16 @@ var SessionBranchRdbProvider = class {
205
216
  revision: await internals.readStoredRevision(id)
206
217
  };
207
218
  }
219
+ /**
220
+ * 让持久化投影检查点跟随截断后的 log(见 {@link LiveSessionHooks.refreshProjectionCache})。
221
+ * 缓存是派生数据:刷新失败只让缓存多滞后一轮,不能让 rewind 失败。
222
+ */
223
+ async refreshProjectionCache(session) {
224
+ if (this.live.refreshProjectionCache === void 0) return;
225
+ try {
226
+ await this.live.refreshProjectionCache(session);
227
+ } catch {}
228
+ }
208
229
  };
209
230
  var SessionBranchRdb = class extends SessionBranch {
210
231
  static inject = ["sessionPersistence", "sessions"];
@@ -216,7 +237,21 @@ var SessionBranchRdb = class extends SessionBranch {
216
237
  getAgent: (id) => {
217
238
  return this.ctx.get("agents")?.get(id);
218
239
  },
219
- flush: (session) => this.ctx.sessions.flush(session)
240
+ flush: (session) => this.ctx.sessions.flush(session),
241
+ resetProjections: (session) => {
242
+ const registry = this.ctx.get("sessionProjections");
243
+ if (registry?.registrations === void 0) return;
244
+ for (const registration of registry.registrations.values()) registration.cells.delete(session);
245
+ },
246
+ refreshProjectionCache: async (session) => {
247
+ const cache = this.ctx.get("sessionProjectionCache");
248
+ if (cache === void 0) return;
249
+ try {
250
+ await cache.write(session);
251
+ } catch (error) {
252
+ this.ctx.logger.warn(`session-rdb: projection cache refresh after rewind for "${session.id}" failed (cache stays stale): ${String(error)}`);
253
+ }
254
+ }
220
255
  });
221
256
  readBranchPrefix(id, atSeq, mode, signal) {
222
257
  return this.provider.readBranchPrefix(id, atSeq, mode, signal);
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-CgAqCQAb.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-BPEHfHNk.mjs";
2
2
  export { SESSION_IMPORT_PATH, SESSION_LOG_ARTIFACT_FILENAME, parseImportZip, parseJsonlArtifact, persistImport, registerSessionImport };
@@ -10,10 +10,27 @@ interface LiveSessionHooks {
10
10
  getSession(id: SessionId): Session | undefined;
11
11
  getAgent(id: SessionId): LiveAgentLike | undefined;
12
12
  flush(session: Session): Promise<boolean>;
13
+ resetProjections?(session: Session): void;
14
+ refreshProjectionCache?(session: ProjectionCacheSession): Promise<void>;
15
+ }
16
+ /**
17
+ * 投影检查点刷新所需的最小会话面:cold rewind 没有 live Session 可复用,
18
+ * 而截断后的前缀未必是合法的独立会话(surface 引用可能悬空),不能走
19
+ * `Session.create` 的校验;缓存服务只读 id / header / inheritedEventCount
20
+ * 与事件前缀,用普通对象承载即可。
21
+ */
22
+ interface ProjectionCacheSession {
23
+ readonly id: SessionId;
24
+ readonly header: SessionHeader;
25
+ readonly inheritedEventCount: SessionLogOffset;
26
+ snapshotEvents(): readonly SessionEvent[];
13
27
  }
14
28
  interface LiveAgentLike {
15
29
  session: Session;
16
30
  requestHeaderLogged?: boolean;
31
+ inbox?: {
32
+ clear(): void;
33
+ };
17
34
  }
18
35
  declare class SessionBranchRdbProvider implements SessionBranchProvider {
19
36
  private readonly persistence;
@@ -27,6 +44,11 @@ declare class SessionBranchRdbProvider implements SessionBranchProvider {
27
44
  }>;
28
45
  forkFrom(sourceId: SessionId, options?: ForkFromOptions, signal?: AbortSignal): Promise<SessionId>;
29
46
  rewind(id: SessionId, toBoundary: number, signal?: AbortSignal): Promise<SessionPersistenceSnapshot>;
47
+ /**
48
+ * 让持久化投影检查点跟随截断后的 log(见 {@link LiveSessionHooks.refreshProjectionCache})。
49
+ * 缓存是派生数据:刷新失败只让缓存多滞后一轮,不能让 rewind 失败。
50
+ */
51
+ private refreshProjectionCache;
30
52
  }
31
53
  declare class SessionBranchRdb extends SessionBranch {
32
54
  static inject: string[];
@@ -182,9 +204,9 @@ declare class SessionPersistenceRdb extends SessionPersistence {
182
204
  /** 空会话 materialize(flush 的持久化屏障)。 */
183
205
  materializeEmpty(meta: SessionHeader, inheritedEventCount: SessionLogOffset): Promise<void>;
184
206
  /** 原样 append 落库(handle 已校验 contiguity;torn tail 先截断)。
185
- * 与上游 JSONL 一致:ignorable 事件原样存储,不做过滤。写路径校验 v2
186
- * 形状(fail-closed):未知类型(非 ignorable)与非法消息形状拒绝入库;
187
- * 旧格式(v0/v1)数据只在读取时经 legacy 转换链动态转换,不落新库。 */
207
+ * 与上游 JSONL 一致:ignorable 事件原样存储,不做过滤。写路径校验当前
208
+ * 格式形状(fail-closed):未知类型(非 ignorable)与非法消息形状拒绝入库;
209
+ * 非当前格式(v0/v1/v2)数据只在读取时经 legacy 转换链动态转换,不落新库。 */
188
210
  appendBatch(meta: SessionHeader, inheritedEventCount: SessionLogOffset, events: readonly SessionEvent[], tornTruncateTo?: number): Promise<boolean>;
189
211
  /** 读取一个会话的稠密 log(含 torn tail 检测,不含修复改写)。 */
190
212
  readLog(id: SessionId, options?: {
@@ -198,11 +220,11 @@ declare class SessionPersistenceRdb extends SessionPersistence {
198
220
  revision: number;
199
221
  /** 存储桥接行数(迁移链可能生成/合并事件,与 `events.length` 不同)。 */
200
222
  storedCount: number;
201
- /** 是否经上游迁移链转换(v0/v1 → 当前格式)。 */
223
+ /** 是否经上游迁移链转换(非当前格式 → 当前格式)。 */
202
224
  migrated: boolean;
203
225
  } | undefined>;
204
226
  /**
205
- * 把迁移链读出的 v2 视图整体落库(旧格式会话写打开时的一次性迁移)。
227
+ * 把迁移链读出的当前格式视图整体落库(非当前格式会话写打开时的一次性迁移)。
206
228
  *
207
229
  * 迁移链会生成/合并事件(end-seed / attempt / chunk 合并),事件 seq 空间
208
230
  * 与存储桥接行数不再相等;写路径以存储 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-CgAqCQAb.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-IH5I48aL.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-BPEHfHNk.mjs";
4
4
  import z from "@deepseek-ai/schemastery";
5
5
  import { randomUUID } from "node:crypto";
6
6
  import { Pool } from "pg";
@@ -10,10 +10,11 @@ import { SESSION_FORMAT_VERSION, SessionLogOffset } from "@deepseek-ai/dsh-sessi
10
10
  import { sessionFormatCatalog } from "@deepseek-ai/dsh-session-format-catalog";
11
11
  import { and, eq, gte, sql } from "drizzle-orm";
12
12
  import { readdirSync } from "node:fs";
13
+ import { fileURLToPath } from "node:url";
13
14
  import { migrate } from "drizzle-orm/node-postgres/migrator";
14
15
  //#region src/postgres.ts
15
16
  /** drizzle-kit 生成的迁移目录(随包根 drizzle/ 发布;src/dist 形态经相对 URL 统一解析)。 */
16
- const postgresMigrationsDir = new URL("../drizzle/postgres/", import.meta.url).pathname;
17
+ const postgresMigrationsDir = fileURLToPath(new URL("../drizzle/postgres/", import.meta.url));
17
18
  var PostgresBackend = class PostgresBackend {
18
19
  db;
19
20
  options;
@@ -161,35 +162,43 @@ var PostgresBackend = class PostgresBackend {
161
162
  };
162
163
  //#endregion
163
164
  //#region src/legacy.ts
164
- /** 重建 v0/v1 物理 header 记录(RDB 行 → 物理 JSON 对象)。 */
165
+ /** 重建 v0/v1/v2 物理 header 记录(RDB 行 → 物理 JSON 对象)。 */
165
166
  function physicalHeader(row) {
166
- return {
167
+ const common = {
167
168
  type: "session",
168
169
  version: row.fVersion,
169
170
  id: row.fSessionId,
170
171
  createdAt: row.fCreatedAt,
171
172
  ...row.fCwd !== null ? { cwd: row.fCwd } : {},
172
173
  ...row.fParentSession !== null ? { parentSession: row.fParentSession } : {},
173
- ...row.fSeedLength !== null ? { seedLength: row.fSeedLength } : {},
174
174
  ...row.fOrigin !== null ? { origin: row.fOrigin } : {},
175
175
  delegationDepth: row.fDelegationDepth ?? 0
176
176
  };
177
+ return row.fVersion < 2 ? {
178
+ ...common,
179
+ ...row.fSeedLength !== null ? { seedLength: row.fSeedLength } : {}
180
+ } : {
181
+ ...common,
182
+ isSeeded: row.fSeedLength !== null
183
+ };
177
184
  }
178
- /** 重建一条 v0/v1 物理事件记录(桥接行 + 事件行 → 物理 JSON 对象)。 */
185
+ /** 重建一条 v0/v1/v2 物理事件记录(桥接行 + 事件行 → 物理 JSON 对象)。 */
179
186
  function physicalEvent(row) {
187
+ const stored = JSON.parse(row.fData);
188
+ const full = typeof stored === "object" && stored !== null && !Array.isArray(stored) && typeof stored["type"] === "string" && "data" in stored;
180
189
  return {
181
190
  type: row.fType,
182
191
  seq: row.fSequence,
183
192
  time: row.fCreatedAt,
184
- data: JSON.parse(row.fData),
193
+ data: full ? stored["data"] : stored,
185
194
  ...row.fSurfaceOp !== null ? { surfaceOp: JSON.parse(row.fSurfaceOp) } : {}
186
195
  };
187
196
  }
188
197
  /**
189
- * 把旧格式(v0/v1)行转换为 v2 逻辑事件与 header。
190
- * @param row - 会话行(f_version < 2)。
198
+ * 把非当前格式(v0/v1/v2)行转换为当前逻辑事件与 header。
199
+ * @param row - 会话行(f_version < SESSION_FORMAT_VERSION)。
191
200
  * @param eventRows - 已扫描保留的桥接行(稠密 seq,torn tail 已剔除)。
192
- * @returns v2 逻辑 header、继承前缀长度与事件。
201
+ * @returns 当前逻辑 header、继承前缀长度与事件。
193
202
  * @throws 迁移链拒绝(格式损坏 / 无法迁移)时原样抛出。
194
203
  */
195
204
  function convertLegacyRows(row, eventRows) {
@@ -205,9 +214,9 @@ function convertLegacyRows(row, eventRows) {
205
214
  events: artifact.events
206
215
  };
207
216
  }
208
- /** 会话行是否携带旧格式(v0/v1)数据。 */
217
+ /** 会话行是否携带非当前格式(f_version < SESSION_FORMAT_VERSION)数据。 */
209
218
  function isLegacyVersion(version) {
210
- return version < 2;
219
+ return version < SESSION_FORMAT_VERSION;
211
220
  }
212
221
  /**
213
222
  * 迁移链拒绝后,把旧格式行直接当**当前格式**事件视图采用。
@@ -221,7 +230,7 @@ function isLegacyVersion(version) {
221
230
  * 调用方须在此之后执行 `validateStoredEvents`:真正属于旧格式的类型
222
231
  * (`assistant/chunk` / `compact/*` 等)仍会被 fail-loud 拒绝。
223
232
  *
224
- * @param row - 会话行(f_version < 2)。
233
+ * @param row - 会话行(f_version < SESSION_FORMAT_VERSION)。
225
234
  * @param eventRows - 该会话全部桥接行(按 f_sequence 升序)。
226
235
  * @returns 当前格式 header(version 归一到 SESSION_FORMAT_VERSION)、收缩后的
227
236
  * 继承前缀长度、稠密事件与可选 torn tail 起点。
@@ -229,6 +238,7 @@ function isLegacyVersion(version) {
229
238
  */
230
239
  function adoptLegacyRows(row, eventRows) {
231
240
  const { preserved, tornFrom } = scanRows(eventRows, 0);
241
+ repairRequestHeaders(preserved);
232
242
  return {
233
243
  meta: {
234
244
  ...rowToMeta(row),
@@ -578,6 +588,7 @@ var SessionPersistenceRdb = class SessionPersistenceRdb extends SessionPersisten
578
588
  }));
579
589
  const log = await this.readLog(id, {}, options?.signal);
580
590
  if (log === void 0) throw new SessionPersistenceNotFoundError(id);
591
+ repairReadView(log.events);
581
592
  validateStoredEvents(log.meta, log.events);
582
593
  return this.tracker.adopt(new RdbSessionHandle(this, id, log.meta, "read", {
583
594
  cursor: log.events.length,
@@ -594,6 +605,7 @@ var SessionPersistenceRdb = class SessionPersistenceRdb extends SessionPersisten
594
605
  }));
595
606
  const log = await this.readLog(id, {}, options?.signal);
596
607
  if (log === void 0) throw new SessionPersistenceNotFoundError(id);
608
+ repairReadView(log.events);
597
609
  validateStoredEvents(log.meta, log.events);
598
610
  if (log.migrated && log.events.length !== log.storedCount) await this.rewriteMigratedLog(id, log);
599
611
  this.writeGuard.confirmHead(id, log.events.at(-1)?.seq ?? -1);
@@ -682,9 +694,9 @@ var SessionPersistenceRdb = class SessionPersistenceRdb extends SessionPersisten
682
694
  this.writeGuard.confirmHead(meta.id, -1);
683
695
  }
684
696
  /** 原样 append 落库(handle 已校验 contiguity;torn tail 先截断)。
685
- * 与上游 JSONL 一致:ignorable 事件原样存储,不做过滤。写路径校验 v2
686
- * 形状(fail-closed):未知类型(非 ignorable)与非法消息形状拒绝入库;
687
- * 旧格式(v0/v1)数据只在读取时经 legacy 转换链动态转换,不落新库。 */
697
+ * 与上游 JSONL 一致:ignorable 事件原样存储,不做过滤。写路径校验当前
698
+ * 格式形状(fail-closed):未知类型(非 ignorable)与非法消息形状拒绝入库;
699
+ * 非当前格式(v0/v1/v2)数据只在读取时经 legacy 转换链动态转换,不落新库。 */
688
700
  async appendBatch(meta, inheritedEventCount, events, tornTruncateTo) {
689
701
  await this.ready;
690
702
  if (events.length === 0) return false;
@@ -765,7 +777,7 @@ var SessionPersistenceRdb = class SessionPersistenceRdb extends SessionPersisten
765
777
  };
766
778
  }
767
779
  /**
768
- * 把迁移链读出的 v2 视图整体落库(旧格式会话写打开时的一次性迁移)。
780
+ * 把迁移链读出的当前格式视图整体落库(非当前格式会话写打开时的一次性迁移)。
769
781
  *
770
782
  * 迁移链会生成/合并事件(end-seed / attempt / chunk 合并),事件 seq 空间
771
783
  * 与存储桥接行数不再相等;写路径以存储 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";
@@ -6,6 +6,7 @@ import { bigint, check as check$1, index as index$1, integer as integer$1, pgSch
6
6
  import { readdirSync, statSync } from "node:fs";
7
7
  import { mkdir, open } from "node:fs/promises";
8
8
  import { dirname, resolve } from "node:path";
9
+ import { fileURLToPath } from "node:url";
9
10
  import { DatabaseSync } from "node:sqlite";
10
11
  import { drizzle } from "drizzle-orm/node-sqlite";
11
12
  import { migrate } from "drizzle-orm/node-sqlite/migrator";
@@ -482,7 +483,7 @@ function eventDimensions(event) {
482
483
  //#endregion
483
484
  //#region src/sqlite.ts
484
485
  /** drizzle-kit 生成的迁移目录(随包根 drizzle/ 发布;src/dist 形态经相对 URL 统一解析)。 */
485
- const sqliteMigrationsDir = new URL("../drizzle/sqlite/", import.meta.url).pathname;
486
+ const sqliteMigrationsDir = fileURLToPath(new URL("../drizzle/sqlite/", import.meta.url));
486
487
  const sqliteTxQueues = /* @__PURE__ */ new Map();
487
488
  function enqueueSqliteTx(path, fn) {
488
489
  const run = (sqliteTxQueues.get(path) ?? Promise.resolve()).then(fn);
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-IH5I48aL.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 };