@morlay/session-rdb 0.0.15 → 0.0.16-alpha.0
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/README.md +11 -13
- package/dist/artifact.d.mts +6 -16
- package/dist/artifact.mjs +3 -3
- package/dist/{import-DFed8DBt.mjs → import-CKrzjXVB.mjs} +73 -83
- package/dist/import.d.mts +2 -3
- package/dist/import.mjs +2 -2
- package/dist/index-CgGDHQSK.d.mts +225 -0
- package/dist/index.d.mts +3 -3
- package/dist/index.mjs +694 -169
- package/dist/log-DTJsxMud.mjs +314 -0
- package/dist/{schema-CFXZURX7.d.mts → schema-COK7-wRV.d.mts} +3 -15
- package/dist/sqlite-DCy4VTD8.mjs +698 -0
- package/dist/storage.d.mts +2 -7
- package/dist/storage.mjs +2 -3
- package/dist/testing.d.mts +30 -1
- package/dist/testing.mjs +676 -1991
- package/drizzle/postgres/20260908072805_v2_initial/migration.sql +58 -0
- package/drizzle/postgres/20260908072805_v2_initial/snapshot.json +686 -0
- package/drizzle/postgres/20260908072806_v3_drop_original_seq/migration.sql +1 -0
- package/drizzle/postgres/20260908072806_v3_drop_original_seq/snapshot.json +673 -0
- package/drizzle/sqlite/20260908072751_v2_initial/migration.sql +53 -0
- package/drizzle/sqlite/20260908072751_v2_initial/snapshot.json +492 -0
- package/drizzle/sqlite/20260908072752_v3_drop_original_seq/migration.sql +6 -0
- package/drizzle/sqlite/20260908072752_v3_drop_original_seq/snapshot.json +513 -0
- package/package.json +17 -12
- package/src/adapters/index.ts +10 -2
- package/src/adapters/to-postgres.ts +19 -15
- package/src/adapters/to-sqlite.ts +20 -14
- package/src/{entities → adapters}/types.ts +7 -8
- package/src/backend.ts +0 -7
- package/src/branch.ts +29 -110
- package/src/drizzle/postgres-v2.ts +11 -0
- package/src/drizzle/postgres-v3.ts +11 -0
- package/src/drizzle/sqlite-v2.ts +10 -0
- package/src/drizzle/sqlite-v3.ts +11 -0
- package/src/entities/index.ts +2 -30
- package/src/entities/v2/index.ts +20 -0
- package/src/entities/v2/session-events.ts +11 -0
- package/src/entities/v3/events.ts +27 -0
- package/src/entities/v3/index.ts +27 -0
- package/src/entities/v3/persistence-state.ts +10 -0
- package/src/entities/v3/schema-meta.ts +9 -0
- package/src/entities/v3/session-events.ts +26 -0
- package/src/entities/v3/sessions.ts +20 -0
- package/src/import.ts +65 -57
- package/src/index.ts +868 -223
- package/src/invariant.ts +0 -2
- package/src/legacy.ts +67 -0
- package/src/log.ts +41 -87
- package/src/postgres.ts +75 -93
- package/src/schema.ts +3 -14
- package/src/sqlite.ts +59 -46
- package/src/testing/contract.ts +460 -375
- package/src/testing/coordinator-contract.ts +108 -1374
- package/src/testing.ts +1 -6
- package/dist/index-uUvn6gYp.d.mts +0 -111
- package/dist/schema-vwzwEXNE.mjs +0 -878
- package/dist/sqlite-CG_Qcx5C.mjs +0 -356
- package/src/adapters/ddl.ts +0 -77
- package/src/entities/events.ts +0 -27
- package/src/entities/persistence-state.ts +0 -10
- package/src/entities/schema-meta.ts +0 -9
- package/src/entities/session-events.ts +0 -29
- package/src/entities/sessions.ts +0 -20
- package/src/migrate.ts +0 -137
package/src/invariant.ts
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
/* jscpd:ignore-start */
|
|
2
1
|
import type { Context } from "@deepseek-ai/cordis";
|
|
3
2
|
import type { InvariantInstaller } from "@deepseek-ai/dsh-invariants";
|
|
4
3
|
|
|
@@ -12,4 +11,3 @@ const install: InvariantInstaller = () => {};
|
|
|
12
11
|
|
|
13
12
|
export const apply = (ctx: Context): Promise<() => void> =>
|
|
14
13
|
Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install));
|
|
15
|
-
/* jscpd:ignore-end */
|
package/src/legacy.ts
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
// 旧格式(v0/v1)历史数据读取转换:RDB 行 → v0/v1 物理记录 → 上游迁移链 →
|
|
2
|
+
// v2 逻辑事件。RDB 写路径从不持久化 sourceEventSeqs(读取时重计算),且
|
|
3
|
+
// delta(assistant/chunk)落盘被过滤——v1→v2 迁移对无 chunk 引用的
|
|
4
|
+
// assistant/message 走「无 pending 直接 emit」路径,生成 stream: [] 的 v2 事件。
|
|
5
|
+
|
|
6
|
+
import type { SessionEvent, SessionHeader, SessionId } from "@deepseek-ai/dsh-session";
|
|
7
|
+
import { sessionFormatCatalog } from "@deepseek-ai/dsh-session-format-catalog";
|
|
8
|
+
import type { EventRow, SessionRow } from "./backend.ts";
|
|
9
|
+
|
|
10
|
+
/** 重建 v0/v1 物理 header 记录(RDB 行 → 物理 JSON 对象)。 */
|
|
11
|
+
function physicalHeader(row: SessionRow): Record<string, unknown> {
|
|
12
|
+
return {
|
|
13
|
+
type: "session",
|
|
14
|
+
version: row.fVersion,
|
|
15
|
+
id: row.fSessionId,
|
|
16
|
+
createdAt: row.fCreatedAt,
|
|
17
|
+
...(row.fCwd !== null ? { cwd: row.fCwd } : {}),
|
|
18
|
+
...(row.fParentSession !== null ? { parentSession: row.fParentSession } : {}),
|
|
19
|
+
...(row.fSeedLength !== null ? { seedLength: row.fSeedLength } : {}),
|
|
20
|
+
...(row.fOrigin !== null ? { origin: row.fOrigin } : {}),
|
|
21
|
+
delegationDepth: row.fDelegationDepth ?? 0,
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** 重建一条 v0/v1 物理事件记录(桥接行 + 事件行 → 物理 JSON 对象)。 */
|
|
26
|
+
function physicalEvent(row: EventRow): Record<string, unknown> {
|
|
27
|
+
return {
|
|
28
|
+
type: row.fType,
|
|
29
|
+
seq: row.fSequence,
|
|
30
|
+
time: row.fCreatedAt,
|
|
31
|
+
data: JSON.parse(row.fData) as unknown,
|
|
32
|
+
...(row.fSurfaceOp !== null ? { surfaceOp: JSON.parse(row.fSurfaceOp) as unknown } : {}),
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* 把旧格式(v0/v1)行转换为 v2 逻辑事件与 header。
|
|
38
|
+
* @param row - 会话行(f_version < 2)。
|
|
39
|
+
* @param eventRows - 已扫描保留的桥接行(稠密 seq,torn tail 已剔除)。
|
|
40
|
+
* @returns v2 逻辑 header、继承前缀长度与事件。
|
|
41
|
+
* @throws 迁移链拒绝(格式损坏 / 无法迁移)时原样抛出。
|
|
42
|
+
*/
|
|
43
|
+
export function convertLegacyRows(
|
|
44
|
+
row: SessionRow,
|
|
45
|
+
eventRows: readonly EventRow[],
|
|
46
|
+
): { meta: SessionHeader; inheritedEventCount: number; events: SessionEvent[] } {
|
|
47
|
+
const restore = sessionFormatCatalog.createRestore(physicalHeader(row), {
|
|
48
|
+
recovery: "strict",
|
|
49
|
+
validation: "transformed",
|
|
50
|
+
});
|
|
51
|
+
for (const eventRow of eventRows) {
|
|
52
|
+
restore.decodeRow(physicalEvent(eventRow));
|
|
53
|
+
}
|
|
54
|
+
const artifact = restore.finish();
|
|
55
|
+
return {
|
|
56
|
+
meta: artifact.header as unknown as SessionHeader,
|
|
57
|
+
inheritedEventCount: artifact.inheritedEventCount,
|
|
58
|
+
events: artifact.events as unknown as SessionEvent[],
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** 会话行是否携带旧格式(v0/v1)数据。 */
|
|
63
|
+
export function isLegacyVersion(version: number): boolean {
|
|
64
|
+
return version < 2;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export type { SessionId };
|
package/src/log.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { SessionEvent, SessionHeader, SessionId, SurfaceOp } from "@deepseek-ai/dsh-session";
|
|
2
|
-
import {
|
|
2
|
+
import type { SessionFormatEvent, SessionFormatHeader } from "@deepseek-ai/dsh-session-format";
|
|
3
|
+
import { sessionFormatCatalog } from "@deepseek-ai/dsh-session-format-catalog";
|
|
3
4
|
import type { SessionStorageMetadata } from "@deepseek-ai/dsh-session-persistence";
|
|
4
5
|
import type { EventRow, SessionRow } from "./backend.ts";
|
|
5
6
|
|
|
@@ -8,7 +9,7 @@ export function rowToMeta(row: SessionRow): SessionHeader {
|
|
|
8
9
|
throw new Error("stored session createdAt must be a non-negative safe integer");
|
|
9
10
|
}
|
|
10
11
|
return {
|
|
11
|
-
version: row.fVersion,
|
|
12
|
+
version: row.fVersion as SessionHeader["version"],
|
|
12
13
|
id: row.fSessionId as SessionId,
|
|
13
14
|
createdAt: row.fCreatedAt,
|
|
14
15
|
...(row.fCwd !== null ? { cwd: row.fCwd } : {}),
|
|
@@ -74,77 +75,38 @@ export function sessionConflictRow(storage: SessionStorageMetadata): {
|
|
|
74
75
|
};
|
|
75
76
|
}
|
|
76
77
|
|
|
77
|
-
export function
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
// assertProvenance 会以 "must reference earlier events" 拒绝整个会话。
|
|
100
|
-
return seqs.flatMap((seq) => {
|
|
101
|
-
const dense = seqMap.get(seq);
|
|
102
|
-
return dense === undefined ? [] : [dense];
|
|
103
|
-
});
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
export function rowToEvent(row: EventRow, seqMap: ReadonlyMap<number, number>): SessionEvent {
|
|
107
|
-
const remap = (seq: number): number => seqMap.get(seq) ?? seq;
|
|
108
|
-
const surfaceOp =
|
|
109
|
-
row.fSurfaceOp !== null
|
|
110
|
-
? remapSurfaceOp(JSON.parse(row.fSurfaceOp) as SurfaceOp, remap)
|
|
111
|
-
: undefined;
|
|
112
|
-
const data = JSON.parse(row.fData) as SessionEvent["data"];
|
|
113
|
-
// compaction 事件的 shadowedRange / shadowedSeqs 是插件合并字段,经结构化
|
|
114
|
-
// 视图重映射到稠密坐标。shadowedSeqs 是权威的被遮蔽节点列表(range 只是
|
|
115
|
-
// 首尾边界对,压缩竞态下可能漏掉并发落地的节点),replace 的 provenance
|
|
116
|
-
// 重计算依赖它,必须与 range 同空间。
|
|
117
|
-
if (row.fType === "compaction/summary" || row.fType === "compaction/prune") {
|
|
118
|
-
const metering = data as unknown as {
|
|
119
|
-
shadowedRange?: { start: number; end: number };
|
|
120
|
-
shadowedSeqs?: number[];
|
|
121
|
-
};
|
|
122
|
-
if (metering.shadowedRange !== undefined) {
|
|
123
|
-
metering.shadowedRange = remapShadowedRange(metering.shadowedRange, remap);
|
|
124
|
-
}
|
|
125
|
-
if (metering.shadowedSeqs !== undefined) {
|
|
126
|
-
metering.shadowedSeqs = remapShadowedSeqs(metering.shadowedSeqs, seqMap);
|
|
127
|
-
}
|
|
78
|
+
export function rowToEvent(row: EventRow): SessionEvent {
|
|
79
|
+
const surfaceOp = row.fSurfaceOp !== null ? (JSON.parse(row.fSurfaceOp) as SurfaceOp) : undefined;
|
|
80
|
+
const record = JSON.parse(row.fData) as unknown;
|
|
81
|
+
// fData 形状判别:新写入的完整事件(含 ignorable 信封,与 JSONL 每行
|
|
82
|
+
// 同构)vs 旧 v2 库的纯 data 部分。完整事件必有 type/seq/time/data 四键。
|
|
83
|
+
const full =
|
|
84
|
+
typeof record === "object" &&
|
|
85
|
+
record !== null &&
|
|
86
|
+
!Array.isArray(record) &&
|
|
87
|
+
typeof (record as Record<string, unknown>)["type"] === "string" &&
|
|
88
|
+
typeof (record as Record<string, unknown>)["seq"] === "number" &&
|
|
89
|
+
typeof (record as Record<string, unknown>)["time"] === "number" &&
|
|
90
|
+
"data" in (record as Record<string, unknown>);
|
|
91
|
+
if (full) {
|
|
92
|
+
// seq/time 以桥接行/事件列为权威(rewind 截断后仍稠密连续);
|
|
93
|
+
// surfaceOp 走桥接行列,sourceEventSeqs 不落库(读取时重计算)。
|
|
94
|
+
return {
|
|
95
|
+
...(record as SessionEvent),
|
|
96
|
+
seq: row.fSequence,
|
|
97
|
+
time: row.fCreatedAt,
|
|
98
|
+
...(surfaceOp === undefined ? {} : { surfaceOp }),
|
|
99
|
+
} as SessionEvent;
|
|
128
100
|
}
|
|
129
101
|
return {
|
|
130
102
|
type: row.fType as SessionEvent["type"],
|
|
131
103
|
seq: row.fSequence,
|
|
132
104
|
time: row.fCreatedAt,
|
|
133
|
-
data,
|
|
105
|
+
data: record as SessionEvent["data"],
|
|
134
106
|
...(surfaceOp === undefined ? {} : { surfaceOp }),
|
|
135
107
|
} as SessionEvent;
|
|
136
108
|
}
|
|
137
109
|
|
|
138
|
-
export function buildSeqMap(
|
|
139
|
-
rows: readonly Pick<EventRow, "fSequence" | "fOriginalSeq">[],
|
|
140
|
-
): Map<number, number> {
|
|
141
|
-
const map = new Map<number, number>();
|
|
142
|
-
for (const row of rows) {
|
|
143
|
-
if (!map.has(row.fOriginalSeq)) map.set(row.fOriginalSeq, row.fSequence);
|
|
144
|
-
}
|
|
145
|
-
return map;
|
|
146
|
-
}
|
|
147
|
-
|
|
148
110
|
const SURFACE_EVENT_TYPES = new Set(["user/message", "assistant/message", "tool/result"]);
|
|
149
111
|
|
|
150
112
|
const METERING_EVENT_TYPES = new Set(["compaction/summary", "compaction/prune"]);
|
|
@@ -418,7 +380,6 @@ export function repairOrphanInboxSplices(events: SessionEvent[]): void {
|
|
|
418
380
|
export function scanRows(
|
|
419
381
|
rows: readonly EventRow[],
|
|
420
382
|
base = 0,
|
|
421
|
-
seqMap: ReadonlyMap<number, number> = new Map(),
|
|
422
383
|
): { preserved: SessionEvent[]; tornFrom?: number } {
|
|
423
384
|
// Pass 1:解析每行 data;JSON 非法的行是洞(seq/type 列即使在 data 损坏
|
|
424
385
|
// 时也存在)。
|
|
@@ -428,7 +389,7 @@ export function scanRows(
|
|
|
428
389
|
}
|
|
429
390
|
const parsed: Parsed[] = rows.map((row) => {
|
|
430
391
|
try {
|
|
431
|
-
return { ok: true, event: rowToEvent(row
|
|
392
|
+
return { ok: true, event: rowToEvent(row) };
|
|
432
393
|
} catch {
|
|
433
394
|
return { ok: false };
|
|
434
395
|
}
|
|
@@ -471,32 +432,25 @@ export function scanRows(
|
|
|
471
432
|
: { preserved };
|
|
472
433
|
}
|
|
473
434
|
|
|
474
|
-
function toStorageRecord(record: import("@deepseek-ai/dsh-session").StorageRecord): unknown {
|
|
475
|
-
const withSeqs = record as import("@deepseek-ai/dsh-session").StorageRecord & {
|
|
476
|
-
sourceEventSeqs?: unknown;
|
|
477
|
-
};
|
|
478
|
-
if (withSeqs.sourceEventSeqs === undefined) return record;
|
|
479
|
-
return { ...record, sourceEventSeqs: encodeSeqRanges(withSeqs.sourceEventSeqs as never) };
|
|
480
|
-
}
|
|
481
|
-
|
|
482
435
|
export function toJsonlArtifact(
|
|
483
436
|
meta: SessionHeader,
|
|
484
437
|
inheritedEventCount: number,
|
|
485
438
|
events: readonly SessionEvent[],
|
|
486
439
|
): string {
|
|
487
|
-
const header =
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
...(meta.isSeeded ? { seedLength: inheritedEventCount } : {}),
|
|
495
|
-
...(meta.origin === undefined ? {} : { origin: meta.origin }),
|
|
496
|
-
delegationDepth: meta.delegationDepth ?? 0,
|
|
497
|
-
...(meta.agentPreset === undefined ? {} : { agentPreset: meta.agentPreset }),
|
|
498
|
-
};
|
|
440
|
+
const header = sessionFormatCatalog.encodeCurrentHeader(
|
|
441
|
+
{
|
|
442
|
+
...meta,
|
|
443
|
+
delegationDepth: meta.delegationDepth ?? 0,
|
|
444
|
+
} as unknown as SessionFormatHeader,
|
|
445
|
+
inheritedEventCount,
|
|
446
|
+
);
|
|
499
447
|
const lines = [JSON.stringify(header)];
|
|
500
|
-
for (const
|
|
448
|
+
for (const event of events) {
|
|
449
|
+
lines.push(
|
|
450
|
+
JSON.stringify(
|
|
451
|
+
sessionFormatCatalog.encodeCurrentEvent(event as unknown as SessionFormatEvent),
|
|
452
|
+
),
|
|
453
|
+
);
|
|
454
|
+
}
|
|
501
455
|
return lines.join("\n");
|
|
502
456
|
}
|
package/src/postgres.ts
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
|
-
import {
|
|
3
|
-
import
|
|
2
|
+
import { readdirSync } from "node:fs";
|
|
3
|
+
import { and, eq, gte, sql } from "drizzle-orm";
|
|
4
|
+
import type { PgAsyncDatabase, PgAsyncTransaction } from "drizzle-orm/pg-core";
|
|
5
|
+
import type { NodePgDatabase, NodePgQueryResultHKT } from "drizzle-orm/node-postgres";
|
|
6
|
+
import { migrate } from "drizzle-orm/node-postgres/migrator";
|
|
4
7
|
import type { SessionId } from "@deepseek-ai/dsh-session";
|
|
5
8
|
import type { SessionStorageMetadata } from "@deepseek-ai/dsh-session-persistence";
|
|
6
9
|
import {
|
|
@@ -10,11 +13,13 @@ import {
|
|
|
10
13
|
type EventRow,
|
|
11
14
|
type SessionRow,
|
|
12
15
|
} from "./backend.ts";
|
|
13
|
-
import {
|
|
14
|
-
import { createTablesSql, toPostgresSchema } from "./adapters/index.ts";
|
|
16
|
+
import { toPostgresSchema } from "./adapters/index.ts";
|
|
15
17
|
import { postgresTableDefs } from "./entities/index.ts";
|
|
16
18
|
import { sessionConflictRow, sessionInsertRow } from "./log.ts";
|
|
17
19
|
|
|
20
|
+
/** drizzle-kit 生成的迁移目录(随包根 drizzle/ 发布;src/dist 形态经相对 URL 统一解析)。 */
|
|
21
|
+
const postgresMigrationsDir = new URL("../drizzle/postgres/", import.meta.url).pathname;
|
|
22
|
+
|
|
18
23
|
export interface PostgresBackendOptions {
|
|
19
24
|
identityBase: string;
|
|
20
25
|
|
|
@@ -23,60 +28,36 @@ export interface PostgresBackendOptions {
|
|
|
23
28
|
close: () => Promise<void>;
|
|
24
29
|
}
|
|
25
30
|
|
|
26
|
-
export class PostgresBackend
|
|
31
|
+
export class PostgresBackend implements Backend {
|
|
27
32
|
readonly kind = "postgres" as const;
|
|
28
33
|
storeIdentity!: string;
|
|
29
34
|
|
|
30
35
|
private readonly tables: Record<string, any>;
|
|
31
36
|
|
|
32
37
|
constructor(
|
|
33
|
-
private readonly db:
|
|
38
|
+
private readonly db: NodePgDatabase,
|
|
34
39
|
private readonly options: PostgresBackendOptions,
|
|
35
40
|
) {
|
|
36
41
|
this.tables = toPostgresSchema(postgresTableDefs, this.options.schema ?? "public");
|
|
37
42
|
}
|
|
38
43
|
|
|
39
44
|
async open(): Promise<void> {
|
|
40
|
-
const
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
const
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
for (const statement of createTablesSql("postgres", postgresTableDefs, schema)) {
|
|
53
|
-
await tx.execute(sql.raw(statement));
|
|
54
|
-
}
|
|
55
|
-
if (!metaExists) {
|
|
56
|
-
await tx
|
|
57
|
-
.insert(this.tables["t_schema_meta"])
|
|
58
|
-
.values([
|
|
59
|
-
{ fKey: "schema_version", fValue: String(SCHEMA_VERSION) },
|
|
60
|
-
{ fKey: "application_id", fValue: String(SESSION_PERSISTENCE_SQLITE_APPLICATION_ID) },
|
|
61
|
-
])
|
|
62
|
-
.execute();
|
|
63
|
-
}
|
|
64
|
-
// 校验:缺版本行 = 有对象但未版本化 → 拒绝,不迁移。
|
|
65
|
-
const version = await this.readMeta(tx, "schema_version");
|
|
66
|
-
const applicationId = await this.readMeta(tx, "application_id");
|
|
67
|
-
if (version === undefined || applicationId === undefined) {
|
|
68
|
-
throw new Error("session database has an unversioned schema or application identity");
|
|
69
|
-
}
|
|
70
|
-
if (Number(version) !== SCHEMA_VERSION) {
|
|
71
|
-
throw new Error(
|
|
72
|
-
`session database has schema version ${version}, incompatible with this build (${SCHEMA_VERSION})`,
|
|
73
|
-
);
|
|
74
|
-
}
|
|
75
|
-
if (Number(applicationId) !== SESSION_PERSISTENCE_SQLITE_APPLICATION_ID) {
|
|
76
|
-
throw new Error(
|
|
77
|
-
`session database has application id ${applicationId}, expected ${SESSION_PERSISTENCE_SQLITE_APPLICATION_ID}`,
|
|
78
|
-
);
|
|
45
|
+
const schema = this.options.schema ?? "public";
|
|
46
|
+
// drizzle-kit 迁移:v2 baseline 由旧版本建表(首次打开时标记为已应用),
|
|
47
|
+
// 本版本只执行 v3 diff(删 f_original_seq)。
|
|
48
|
+
const qualifiedMeta = schema === "public" ? "t_schema_meta" : `"${schema}".t_schema_meta`;
|
|
49
|
+
const probe = (await this.db.execute(
|
|
50
|
+
sql`SELECT to_regclass(${qualifiedMeta}) IS NOT NULL AS exists`,
|
|
51
|
+
)) as unknown as { rows: { exists: boolean }[] };
|
|
52
|
+
const metaExists = probe.rows[0]?.exists === true;
|
|
53
|
+
if (metaExists) {
|
|
54
|
+
const version = await this.readMeta(this.db, "schema_version");
|
|
55
|
+
if (version === "2") {
|
|
56
|
+
await this.baselineV2();
|
|
79
57
|
}
|
|
58
|
+
}
|
|
59
|
+
await migrate(this.db, { migrationsFolder: postgresMigrationsDir });
|
|
60
|
+
const storeId = await this.db.transaction(async (tx) => {
|
|
80
61
|
await tx
|
|
81
62
|
.insert(this.tables["t_persistence_state"])
|
|
82
63
|
.values({ fSingleton: 1, fStoreId: randomUUID() })
|
|
@@ -87,15 +68,36 @@ export class PostgresBackend<THKT extends PgQueryResultHKT = PgQueryResultHKT> i
|
|
|
87
68
|
.from(this.tables["t_persistence_state"])
|
|
88
69
|
.where(eq(this.tables["t_persistence_state"].fSingleton, 1))
|
|
89
70
|
.execute();
|
|
90
|
-
const
|
|
91
|
-
if (
|
|
71
|
+
const id = store[0]?.fStoreId;
|
|
72
|
+
if (id === undefined || id.length === 0) {
|
|
92
73
|
throw new Error("session database has no valid store identity");
|
|
93
74
|
}
|
|
94
|
-
return
|
|
75
|
+
return id;
|
|
95
76
|
});
|
|
96
77
|
this.storeIdentity = `${this.options.identityBase}:store:${storeId}`;
|
|
97
78
|
}
|
|
98
79
|
|
|
80
|
+
/** 标记 v2 baseline 已应用(迁移表 v1 结构:id/hash/created_at/name/applied_at)。 */
|
|
81
|
+
private async baselineV2(): Promise<void> {
|
|
82
|
+
const dir = readdirSync(postgresMigrationsDir).find((name) => name.endsWith("_v2_initial"));
|
|
83
|
+
if (dir === undefined) throw new Error("missing v2 baseline migration in drizzle/postgres");
|
|
84
|
+
await this.db.execute(sql`
|
|
85
|
+
CREATE SCHEMA IF NOT EXISTS drizzle
|
|
86
|
+
`);
|
|
87
|
+
await this.db.execute(sql`
|
|
88
|
+
CREATE TABLE IF NOT EXISTS drizzle.__drizzle_migrations (
|
|
89
|
+
id SERIAL PRIMARY KEY,
|
|
90
|
+
hash text NOT NULL,
|
|
91
|
+
created_at bigint,
|
|
92
|
+
name text,
|
|
93
|
+
applied_at timestamp with time zone DEFAULT now()
|
|
94
|
+
)
|
|
95
|
+
`);
|
|
96
|
+
await this.db.execute(
|
|
97
|
+
sql`INSERT INTO drizzle.__drizzle_migrations (hash, created_at, name) VALUES ('baseline', 0, ${dir})`,
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
|
|
99
101
|
async close(): Promise<void> {
|
|
100
102
|
await this.options.close();
|
|
101
103
|
}
|
|
@@ -110,17 +112,6 @@ export class PostgresBackend<THKT extends PgQueryResultHKT = PgQueryResultHKT> i
|
|
|
110
112
|
)[0] as SessionRow | undefined;
|
|
111
113
|
}
|
|
112
114
|
|
|
113
|
-
async getSeqMapRows(id: SessionId): Promise<Array<{ fSequence: number; fOriginalSeq: number }>> {
|
|
114
|
-
return this.db
|
|
115
|
-
.select({
|
|
116
|
-
fSequence: this.tables["t_session_events"].fSequence,
|
|
117
|
-
fOriginalSeq: this.tables["t_session_events"].fOriginalSeq,
|
|
118
|
-
})
|
|
119
|
-
.from(this.tables["t_session_events"])
|
|
120
|
-
.where(eq(this.tables["t_session_events"].fSessionId, id))
|
|
121
|
-
.execute();
|
|
122
|
-
}
|
|
123
|
-
|
|
124
115
|
async getEventRows(id: SessionId, fromSequence?: number): Promise<EventRow[]> {
|
|
125
116
|
const scoped =
|
|
126
117
|
fromSequence === undefined
|
|
@@ -146,7 +137,7 @@ export class PostgresBackend<THKT extends PgQueryResultHKT = PgQueryResultHKT> i
|
|
|
146
137
|
return this.db.transaction(async (tx) => fn(this.txFor(tx)));
|
|
147
138
|
}
|
|
148
139
|
|
|
149
|
-
private txFor(tx: PgAsyncTransaction<
|
|
140
|
+
private txFor(tx: PgAsyncTransaction<NodePgQueryResultHKT>): BackendTx {
|
|
150
141
|
return {
|
|
151
142
|
upsertSession: (storage, incarnation) => this.upsertSession(tx, storage, incarnation),
|
|
152
143
|
getHead: (id) => this.getHead(tx, id),
|
|
@@ -159,13 +150,15 @@ export class PostgresBackend<THKT extends PgQueryResultHKT = PgQueryResultHKT> i
|
|
|
159
150
|
bumpRevision: (id) => this.bumpRevision(tx, id),
|
|
160
151
|
deleteBridgeTail: (id, fromSequence) => this.deleteBridgeTail(tx, id, fromSequence),
|
|
161
152
|
getPrevBridge: (id, sequence) => this.getPrevBridge(tx, id, sequence),
|
|
162
|
-
getLastBridge: (id) => this.getLastBridge(tx, id),
|
|
163
153
|
};
|
|
164
154
|
}
|
|
165
155
|
|
|
166
156
|
// --- meta helpers ---
|
|
167
157
|
|
|
168
|
-
private async readMeta(
|
|
158
|
+
private async readMeta(
|
|
159
|
+
exec: PgAsyncDatabase<NodePgQueryResultHKT>,
|
|
160
|
+
key: string,
|
|
161
|
+
): Promise<string | undefined> {
|
|
169
162
|
const rows = await exec
|
|
170
163
|
.select({ fValue: this.tables["t_schema_meta"].fValue })
|
|
171
164
|
.from(this.tables["t_schema_meta"])
|
|
@@ -177,7 +170,7 @@ export class PostgresBackend<THKT extends PgQueryResultHKT = PgQueryResultHKT> i
|
|
|
177
170
|
// --- row primitives (transaction-internal) ---
|
|
178
171
|
|
|
179
172
|
private async upsertSession(
|
|
180
|
-
exec: PgAsyncDatabase<
|
|
173
|
+
exec: PgAsyncDatabase<NodePgQueryResultHKT>,
|
|
181
174
|
storage: SessionStorageMetadata,
|
|
182
175
|
incarnation: string,
|
|
183
176
|
): Promise<void> {
|
|
@@ -192,7 +185,7 @@ export class PostgresBackend<THKT extends PgQueryResultHKT = PgQueryResultHKT> i
|
|
|
192
185
|
}
|
|
193
186
|
|
|
194
187
|
private async getHead(
|
|
195
|
-
exec: PgAsyncDatabase<
|
|
188
|
+
exec: PgAsyncDatabase<NodePgQueryResultHKT>,
|
|
196
189
|
id: SessionId,
|
|
197
190
|
): Promise<Pick<SessionRow, "fHeadEventId" | "fHeadSequence">> {
|
|
198
191
|
const head = (
|
|
@@ -210,7 +203,10 @@ export class PostgresBackend<THKT extends PgQueryResultHKT = PgQueryResultHKT> i
|
|
|
210
203
|
return head;
|
|
211
204
|
}
|
|
212
205
|
|
|
213
|
-
private async getSeedLength(
|
|
206
|
+
private async getSeedLength(
|
|
207
|
+
exec: PgAsyncDatabase<NodePgQueryResultHKT>,
|
|
208
|
+
id: SessionId,
|
|
209
|
+
): Promise<number | null> {
|
|
214
210
|
const row = (
|
|
215
211
|
await exec
|
|
216
212
|
.select({ fSeedLength: this.tables["t_sessions"].fSeedLength })
|
|
@@ -224,7 +220,7 @@ export class PostgresBackend<THKT extends PgQueryResultHKT = PgQueryResultHKT> i
|
|
|
224
220
|
}
|
|
225
221
|
|
|
226
222
|
private async updateSeedLength(
|
|
227
|
-
exec: PgAsyncDatabase<
|
|
223
|
+
exec: PgAsyncDatabase<NodePgQueryResultHKT>,
|
|
228
224
|
id: SessionId,
|
|
229
225
|
seedLength: number,
|
|
230
226
|
): Promise<void> {
|
|
@@ -237,7 +233,10 @@ export class PostgresBackend<THKT extends PgQueryResultHKT = PgQueryResultHKT> i
|
|
|
237
233
|
|
|
238
234
|
private static readonly INSERT_BATCH_ROWS = 1000;
|
|
239
235
|
|
|
240
|
-
private async insertEvents(
|
|
236
|
+
private async insertEvents(
|
|
237
|
+
exec: PgAsyncDatabase<NodePgQueryResultHKT>,
|
|
238
|
+
events: EventInsert[],
|
|
239
|
+
): Promise<void> {
|
|
241
240
|
if (events.length === 0) return;
|
|
242
241
|
for (let i = 0; i < events.length; i += PostgresBackend.INSERT_BATCH_ROWS) {
|
|
243
242
|
await exec
|
|
@@ -250,12 +249,11 @@ export class PostgresBackend<THKT extends PgQueryResultHKT = PgQueryResultHKT> i
|
|
|
250
249
|
}
|
|
251
250
|
|
|
252
251
|
private async insertBridges(
|
|
253
|
-
exec: PgAsyncDatabase<
|
|
252
|
+
exec: PgAsyncDatabase<NodePgQueryResultHKT>,
|
|
254
253
|
rows: Array<{
|
|
255
254
|
fSessionId: SessionId;
|
|
256
255
|
fEventId: string;
|
|
257
256
|
fSequence: number;
|
|
258
|
-
fOriginalSeq: number;
|
|
259
257
|
fSurfaceOp: string | null;
|
|
260
258
|
}>,
|
|
261
259
|
): Promise<void> {
|
|
@@ -269,7 +267,7 @@ export class PostgresBackend<THKT extends PgQueryResultHKT = PgQueryResultHKT> i
|
|
|
269
267
|
}
|
|
270
268
|
|
|
271
269
|
private async updateHead(
|
|
272
|
-
exec: PgAsyncDatabase<
|
|
270
|
+
exec: PgAsyncDatabase<NodePgQueryResultHKT>,
|
|
273
271
|
id: SessionId,
|
|
274
272
|
headEventId: string,
|
|
275
273
|
headSequence: number,
|
|
@@ -281,7 +279,10 @@ export class PostgresBackend<THKT extends PgQueryResultHKT = PgQueryResultHKT> i
|
|
|
281
279
|
.execute();
|
|
282
280
|
}
|
|
283
281
|
|
|
284
|
-
private async bumpRevision(
|
|
282
|
+
private async bumpRevision(
|
|
283
|
+
exec: PgAsyncDatabase<NodePgQueryResultHKT>,
|
|
284
|
+
id: SessionId,
|
|
285
|
+
): Promise<void> {
|
|
285
286
|
await exec
|
|
286
287
|
.update(this.tables["t_sessions"])
|
|
287
288
|
.set({ fRevision: sql`${this.tables["t_sessions"].fRevision} + 1` })
|
|
@@ -290,7 +291,7 @@ export class PostgresBackend<THKT extends PgQueryResultHKT = PgQueryResultHKT> i
|
|
|
290
291
|
}
|
|
291
292
|
|
|
292
293
|
private async deleteBridgeTail(
|
|
293
|
-
exec: PgAsyncDatabase<
|
|
294
|
+
exec: PgAsyncDatabase<NodePgQueryResultHKT>,
|
|
294
295
|
id: SessionId,
|
|
295
296
|
fromSequence: number,
|
|
296
297
|
): Promise<void> {
|
|
@@ -306,7 +307,7 @@ export class PostgresBackend<THKT extends PgQueryResultHKT = PgQueryResultHKT> i
|
|
|
306
307
|
}
|
|
307
308
|
|
|
308
309
|
private async getPrevBridge(
|
|
309
|
-
exec: PgAsyncDatabase<
|
|
310
|
+
exec: PgAsyncDatabase<NodePgQueryResultHKT>,
|
|
310
311
|
id: SessionId,
|
|
311
312
|
sequence: number,
|
|
312
313
|
): Promise<{ fEventId: string; fSequence: number } | undefined> {
|
|
@@ -327,30 +328,11 @@ export class PostgresBackend<THKT extends PgQueryResultHKT = PgQueryResultHKT> i
|
|
|
327
328
|
)[0] as { fEventId: string; fSequence: number } | undefined;
|
|
328
329
|
}
|
|
329
330
|
|
|
330
|
-
private
|
|
331
|
-
exec: PgAsyncDatabase<THKT>,
|
|
332
|
-
id: SessionId,
|
|
333
|
-
): Promise<{ fEventId: string; fSequence: number } | undefined> {
|
|
334
|
-
return (
|
|
335
|
-
await exec
|
|
336
|
-
.select({
|
|
337
|
-
fEventId: this.tables["t_session_events"].fEventId,
|
|
338
|
-
fSequence: this.tables["t_session_events"].fSequence,
|
|
339
|
-
})
|
|
340
|
-
.from(this.tables["t_session_events"])
|
|
341
|
-
.where(eq(this.tables["t_session_events"].fSessionId, id))
|
|
342
|
-
.orderBy(desc(this.tables["t_session_events"].fSequence))
|
|
343
|
-
.limit(1)
|
|
344
|
-
.execute()
|
|
345
|
-
)[0] as { fEventId: string; fSequence: number } | undefined;
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
private eventRows(exec: PgAsyncDatabase<THKT>) {
|
|
331
|
+
private eventRows(exec: PgAsyncDatabase<NodePgQueryResultHKT>) {
|
|
349
332
|
return exec
|
|
350
333
|
.select({
|
|
351
334
|
fEventId: this.tables["t_session_events"].fEventId,
|
|
352
335
|
fSequence: this.tables["t_session_events"].fSequence,
|
|
353
|
-
fOriginalSeq: this.tables["t_session_events"].fOriginalSeq,
|
|
354
336
|
fType: this.tables["t_events"].fType,
|
|
355
337
|
fKind: this.tables["t_events"].fKind,
|
|
356
338
|
fRole: this.tables["t_events"].fRole,
|
package/src/schema.ts
CHANGED
|
@@ -2,12 +2,10 @@ import type { SessionEvent } from "@deepseek-ai/dsh-session";
|
|
|
2
2
|
import { toSqliteSchema } from "./adapters/index.ts";
|
|
3
3
|
import { sqliteTableDefs } from "./entities/index.ts";
|
|
4
4
|
|
|
5
|
-
export const SCHEMA_VERSION =
|
|
5
|
+
export const SCHEMA_VERSION = 3;
|
|
6
6
|
|
|
7
7
|
export const SESSION_PERSISTENCE_SQLITE_APPLICATION_ID = 0x44534850;
|
|
8
8
|
|
|
9
|
-
export const EPHEMERAL_EVENT_TYPES = ["assistant/chunk"] as const;
|
|
10
|
-
|
|
11
9
|
export const EVENT_ENCODING = "json";
|
|
12
10
|
|
|
13
11
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
@@ -15,6 +13,8 @@ const sqliteTables: Record<string, any> = toSqliteSchema(sqliteTableDefs);
|
|
|
15
13
|
|
|
16
14
|
export const tPersistenceState = sqliteTables["t_persistence_state"]!;
|
|
17
15
|
|
|
16
|
+
export const tSchemaMeta = sqliteTables["t_schema_meta"]!;
|
|
17
|
+
|
|
18
18
|
export const tSessions = sqliteTables["t_sessions"]!;
|
|
19
19
|
|
|
20
20
|
export const tEvents = sqliteTables["t_events"]!;
|
|
@@ -29,17 +29,6 @@ export type JournalMode = "wal" | "delete" | "truncate" | "persist";
|
|
|
29
29
|
|
|
30
30
|
export const DEFAULT_BUSY_TIMEOUT_MS = 5000;
|
|
31
31
|
|
|
32
|
-
export function isEphemeralType(type: string): boolean {
|
|
33
|
-
return (EPHEMERAL_EVENT_TYPES as readonly string[]).includes(type);
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
export function isPersistedEvent(event: SessionEvent): boolean {
|
|
37
|
-
return (
|
|
38
|
-
!isEphemeralType(event.type) &&
|
|
39
|
-
(event as SessionEvent & { ignorable?: unknown }).ignorable !== true
|
|
40
|
-
);
|
|
41
|
-
}
|
|
42
|
-
|
|
43
32
|
export type EventKind =
|
|
44
33
|
| "message"
|
|
45
34
|
| "thinking"
|