@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/dist/artifact.d.mts +22 -3
- package/dist/artifact.mjs +3 -3
- package/dist/{import-mZZgDBLd.mjs → import-BPEHfHNk.mjs} +40 -5
- package/dist/import.d.mts +1 -1
- package/dist/import.mjs +1 -1
- package/dist/{index-GdKkSSb-.d.mts → index-CgAqCQAb.d.mts} +27 -5
- package/dist/index.d.mts +1 -1
- package/dist/index.mjs +31 -19
- package/dist/{log-DBFUBPhv.mjs → log-BIuXv09P.mjs} +143 -20
- package/dist/{sqlite-CnWwGToZ.mjs → sqlite-IH5I48aL.mjs} +3 -2
- package/dist/storage.mjs +1 -1
- package/dist/testing.mjs +4 -4
- package/package.json +17 -12
- package/src/branch.ts +99 -3
- package/src/index.ts +19 -13
- package/src/legacy.ts +32 -14
- package/src/log.ts +160 -17
- package/src/postgres.ts +2 -1
- package/src/sqlite.ts +2 -1
- package/src/testing/contract.ts +1 -1
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 =
|
|
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([
|
|
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
|
|
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 {
|
|
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 >=
|
|
148
|
-
candidate.seq <=
|
|
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?.["
|
|
246
|
-
const end = replace?.["
|
|
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 {
|
|
324
|
-
raw.surfaceOp = {
|
|
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 {
|
|
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 ===
|
|
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 >=
|
|
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
|
-
*
|
|
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)
|
|
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)
|
|
34
|
+
const sqliteMigrationsDir = fileURLToPath(new URL("../drizzle/sqlite/", import.meta.url));
|
|
34
35
|
|
|
35
36
|
const sqliteTxQueues = new Map<string, Promise<void>>();
|
|
36
37
|
|
package/src/testing/contract.ts
CHANGED
|
@@ -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
|
-
//
|
|
538
|
+
// 写路径当前格式校验:未知类型(非 ignorable)拒绝入库,批次不落库。
|
|
539
539
|
await expect(
|
|
540
540
|
handle.append([
|
|
541
541
|
{ type: "mystery/event", seq: SessionSeq(6), time: 7, data: { payload: true } },
|