@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.
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);
package/src/postgres.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { randomUUID } from "node:crypto";
2
2
  import { readdirSync } from "node:fs";
3
+ import { fileURLToPath } from "node:url";
3
4
  import { and, eq, gte, sql } from "drizzle-orm";
4
5
  import type { PgAsyncDatabase, PgAsyncTransaction } from "drizzle-orm/pg-core";
5
6
  import type { NodePgDatabase, NodePgQueryResultHKT } from "drizzle-orm/node-postgres";
@@ -18,7 +19,7 @@ import { postgresTableDefs } from "./entities/index.ts";
18
19
  import { sessionConflictRow, sessionInsertRow } from "./log.ts";
19
20
 
20
21
  /** drizzle-kit 生成的迁移目录(随包根 drizzle/ 发布;src/dist 形态经相对 URL 统一解析)。 */
21
- const postgresMigrationsDir = new URL("../drizzle/postgres/", import.meta.url).pathname;
22
+ const postgresMigrationsDir = fileURLToPath(new URL("../drizzle/postgres/", import.meta.url));
22
23
 
23
24
  export interface PostgresBackendOptions {
24
25
  identityBase: string;
package/src/sqlite.ts CHANGED
@@ -2,6 +2,7 @@ import { randomUUID } from "node:crypto";
2
2
  import { readdirSync, statSync } from "node:fs";
3
3
  import { mkdir, open } from "node:fs/promises";
4
4
  import { dirname, resolve } from "node:path";
5
+ import { fileURLToPath } from "node:url";
5
6
  import { DatabaseSync } from "node:sqlite";
6
7
  import { and, eq, gte, sql } from "drizzle-orm";
7
8
  import { drizzle, type NodeSQLiteDatabase } from "drizzle-orm/node-sqlite";
@@ -30,7 +31,7 @@ import {
30
31
  type SqliteDb = NodeSQLiteDatabase & { $client: DatabaseSync };
31
32
 
32
33
  /** drizzle-kit 生成的迁移目录(随包根 drizzle/ 发布;src/dist 形态经相对 URL 统一解析)。 */
33
- const sqliteMigrationsDir = new URL("../drizzle/sqlite/", import.meta.url).pathname;
34
+ const sqliteMigrationsDir = fileURLToPath(new URL("../drizzle/sqlite/", import.meta.url));
34
35
 
35
36
  const sqliteTxQueues = new Map<string, Promise<void>>();
36
37
 
@@ -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 } },