@morlay/session-rdb 0.0.14 → 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.
Files changed (65) hide show
  1. package/README.md +11 -13
  2. package/dist/artifact.d.mts +6 -16
  3. package/dist/artifact.mjs +3 -3
  4. package/dist/{import-B-tf5vQn.mjs → import-CKrzjXVB.mjs} +73 -83
  5. package/dist/import.d.mts +2 -3
  6. package/dist/import.mjs +2 -2
  7. package/dist/index-CgGDHQSK.d.mts +225 -0
  8. package/dist/index.d.mts +3 -3
  9. package/dist/index.mjs +694 -169
  10. package/dist/log-DTJsxMud.mjs +314 -0
  11. package/dist/{schema-CFXZURX7.d.mts → schema-COK7-wRV.d.mts} +3 -15
  12. package/dist/sqlite-DCy4VTD8.mjs +698 -0
  13. package/dist/storage.d.mts +2 -7
  14. package/dist/storage.mjs +2 -3
  15. package/dist/testing.d.mts +30 -1
  16. package/dist/testing.mjs +676 -1991
  17. package/drizzle/postgres/20260908072805_v2_initial/migration.sql +58 -0
  18. package/drizzle/postgres/20260908072805_v2_initial/snapshot.json +686 -0
  19. package/drizzle/postgres/20260908072806_v3_drop_original_seq/migration.sql +1 -0
  20. package/drizzle/postgres/20260908072806_v3_drop_original_seq/snapshot.json +673 -0
  21. package/drizzle/sqlite/20260908072751_v2_initial/migration.sql +53 -0
  22. package/drizzle/sqlite/20260908072751_v2_initial/snapshot.json +492 -0
  23. package/drizzle/sqlite/20260908072752_v3_drop_original_seq/migration.sql +6 -0
  24. package/drizzle/sqlite/20260908072752_v3_drop_original_seq/snapshot.json +513 -0
  25. package/package.json +17 -12
  26. package/src/adapters/index.ts +10 -2
  27. package/src/adapters/to-postgres.ts +19 -15
  28. package/src/adapters/to-sqlite.ts +20 -14
  29. package/src/{entities → adapters}/types.ts +7 -8
  30. package/src/backend.ts +0 -7
  31. package/src/branch.ts +29 -110
  32. package/src/drizzle/postgres-v2.ts +11 -0
  33. package/src/drizzle/postgres-v3.ts +11 -0
  34. package/src/drizzle/sqlite-v2.ts +10 -0
  35. package/src/drizzle/sqlite-v3.ts +11 -0
  36. package/src/entities/index.ts +2 -30
  37. package/src/entities/v2/index.ts +20 -0
  38. package/src/entities/v2/session-events.ts +11 -0
  39. package/src/entities/v3/events.ts +27 -0
  40. package/src/entities/v3/index.ts +27 -0
  41. package/src/entities/v3/persistence-state.ts +10 -0
  42. package/src/entities/v3/schema-meta.ts +9 -0
  43. package/src/entities/v3/session-events.ts +26 -0
  44. package/src/entities/v3/sessions.ts +20 -0
  45. package/src/import.ts +65 -57
  46. package/src/index.ts +868 -223
  47. package/src/invariant.ts +0 -2
  48. package/src/legacy.ts +67 -0
  49. package/src/log.ts +41 -81
  50. package/src/postgres.ts +75 -93
  51. package/src/schema.ts +3 -14
  52. package/src/sqlite.ts +59 -46
  53. package/src/testing/contract.ts +460 -375
  54. package/src/testing/coordinator-contract.ts +108 -1374
  55. package/src/testing.ts +1 -6
  56. package/dist/index-uUvn6gYp.d.mts +0 -111
  57. package/dist/schema-BfPVmr1X.mjs +0 -875
  58. package/dist/sqlite-BUWnS0so.mjs +0 -356
  59. package/src/adapters/ddl.ts +0 -77
  60. package/src/entities/events.ts +0 -27
  61. package/src/entities/persistence-state.ts +0 -10
  62. package/src/entities/schema-meta.ts +0 -9
  63. package/src/entities/session-events.ts +0 -29
  64. package/src/entities/sessions.ts +0 -20
  65. 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 { SessionSeq, encodeSeqRanges, packChunkRuns } from "@deepseek-ai/dsh-session";
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,71 +75,38 @@ export function sessionConflictRow(storage: SessionStorageMetadata): {
74
75
  };
75
76
  }
76
77
 
77
- export function remapSurfaceOp(op: SurfaceOp, remap: (seq: number) => number): SurfaceOp {
78
- if (op === "append") return op;
79
- return {
80
- op: "replace",
81
- start: SessionSeq(remap(op.start)),
82
- end: SessionSeq(remap(op.end)),
83
- };
84
- }
85
-
86
- export function remapShadowedRange(
87
- range: { start: number; end: number },
88
- remap: (seq: number) => number,
89
- ): { start: number; end: number } {
90
- return { start: remap(range.start), end: remap(range.end) };
91
- }
92
-
93
- export function remapShadowedSeqs(
94
- seqs: readonly number[],
95
- remap: (seq: number) => number,
96
- ): number[] {
97
- return seqs.map(remap);
98
- }
99
-
100
- export function rowToEvent(row: EventRow, seqMap: ReadonlyMap<number, number>): SessionEvent {
101
- const remap = (seq: number): number => seqMap.get(seq) ?? seq;
102
- const surfaceOp =
103
- row.fSurfaceOp !== null
104
- ? remapSurfaceOp(JSON.parse(row.fSurfaceOp) as SurfaceOp, remap)
105
- : undefined;
106
- const data = JSON.parse(row.fData) as SessionEvent["data"];
107
- // compaction 事件的 shadowedRange / shadowedSeqs 是插件合并字段,经结构化
108
- // 视图重映射到稠密坐标。shadowedSeqs 是权威的被遮蔽节点列表(range 只是
109
- // 首尾边界对,压缩竞态下可能漏掉并发落地的节点),replace 的 provenance
110
- // 重计算依赖它,必须与 range 同空间。
111
- if (row.fType === "compaction/summary" || row.fType === "compaction/prune") {
112
- const metering = data as unknown as {
113
- shadowedRange?: { start: number; end: number };
114
- shadowedSeqs?: number[];
115
- };
116
- if (metering.shadowedRange !== undefined) {
117
- metering.shadowedRange = remapShadowedRange(metering.shadowedRange, remap);
118
- }
119
- if (metering.shadowedSeqs !== undefined) {
120
- metering.shadowedSeqs = remapShadowedSeqs(metering.shadowedSeqs, remap);
121
- }
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;
122
100
  }
123
101
  return {
124
102
  type: row.fType as SessionEvent["type"],
125
103
  seq: row.fSequence,
126
104
  time: row.fCreatedAt,
127
- data,
105
+ data: record as SessionEvent["data"],
128
106
  ...(surfaceOp === undefined ? {} : { surfaceOp }),
129
107
  } as SessionEvent;
130
108
  }
131
109
 
132
- export function buildSeqMap(
133
- rows: readonly Pick<EventRow, "fSequence" | "fOriginalSeq">[],
134
- ): Map<number, number> {
135
- const map = new Map<number, number>();
136
- for (const row of rows) {
137
- if (!map.has(row.fOriginalSeq)) map.set(row.fOriginalSeq, row.fSequence);
138
- }
139
- return map;
140
- }
141
-
142
110
  const SURFACE_EVENT_TYPES = new Set(["user/message", "assistant/message", "tool/result"]);
143
111
 
144
112
  const METERING_EVENT_TYPES = new Set(["compaction/summary", "compaction/prune"]);
@@ -412,7 +380,6 @@ export function repairOrphanInboxSplices(events: SessionEvent[]): void {
412
380
  export function scanRows(
413
381
  rows: readonly EventRow[],
414
382
  base = 0,
415
- seqMap: ReadonlyMap<number, number> = new Map(),
416
383
  ): { preserved: SessionEvent[]; tornFrom?: number } {
417
384
  // Pass 1:解析每行 data;JSON 非法的行是洞(seq/type 列即使在 data 损坏
418
385
  // 时也存在)。
@@ -422,7 +389,7 @@ export function scanRows(
422
389
  }
423
390
  const parsed: Parsed[] = rows.map((row) => {
424
391
  try {
425
- return { ok: true, event: rowToEvent(row, seqMap) };
392
+ return { ok: true, event: rowToEvent(row) };
426
393
  } catch {
427
394
  return { ok: false };
428
395
  }
@@ -465,32 +432,25 @@ export function scanRows(
465
432
  : { preserved };
466
433
  }
467
434
 
468
- function toStorageRecord(record: import("@deepseek-ai/dsh-session").StorageRecord): unknown {
469
- const withSeqs = record as import("@deepseek-ai/dsh-session").StorageRecord & {
470
- sourceEventSeqs?: unknown;
471
- };
472
- if (withSeqs.sourceEventSeqs === undefined) return record;
473
- return { ...record, sourceEventSeqs: encodeSeqRanges(withSeqs.sourceEventSeqs as never) };
474
- }
475
-
476
435
  export function toJsonlArtifact(
477
436
  meta: SessionHeader,
478
437
  inheritedEventCount: number,
479
438
  events: readonly SessionEvent[],
480
439
  ): string {
481
- const header = {
482
- type: "session",
483
- version: meta.version,
484
- id: meta.id,
485
- createdAt: meta.createdAt,
486
- ...(meta.cwd === undefined ? {} : { cwd: meta.cwd }),
487
- ...(meta.parentSession === undefined ? {} : { parentSession: meta.parentSession }),
488
- ...(meta.isSeeded ? { seedLength: inheritedEventCount } : {}),
489
- ...(meta.origin === undefined ? {} : { origin: meta.origin }),
490
- delegationDepth: meta.delegationDepth ?? 0,
491
- ...(meta.agentPreset === undefined ? {} : { agentPreset: meta.agentPreset }),
492
- };
440
+ const header = sessionFormatCatalog.encodeCurrentHeader(
441
+ {
442
+ ...meta,
443
+ delegationDepth: meta.delegationDepth ?? 0,
444
+ } as unknown as SessionFormatHeader,
445
+ inheritedEventCount,
446
+ );
493
447
  const lines = [JSON.stringify(header)];
494
- for (const record of packChunkRuns(events)) lines.push(JSON.stringify(toStorageRecord(record)));
448
+ for (const event of events) {
449
+ lines.push(
450
+ JSON.stringify(
451
+ sessionFormatCatalog.encodeCurrentEvent(event as unknown as SessionFormatEvent),
452
+ ),
453
+ );
454
+ }
495
455
  return lines.join("\n");
496
456
  }
package/src/postgres.ts CHANGED
@@ -1,6 +1,9 @@
1
1
  import { randomUUID } from "node:crypto";
2
- import { and, desc, eq, gte, sql } from "drizzle-orm";
3
- import type { PgAsyncDatabase, PgAsyncTransaction, PgQueryResultHKT } from "drizzle-orm/pg-core";
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 { SCHEMA_VERSION, SESSION_PERSISTENCE_SQLITE_APPLICATION_ID } from "./schema.ts";
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<THKT extends PgQueryResultHKT = PgQueryResultHKT> implements Backend {
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: PgAsyncDatabase<THKT>,
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 storeId = await this.db.transaction(async (tx) => {
41
- const schema = this.options.schema ?? "public";
42
- // 探测在建表之前:t_schema_meta 存在与否区分全新库与已有库。
43
- // `to_regclass` 是数据库元数据查询,drizzle 无对应 API;schema 限定
44
- // 显式引用(不依赖 search_path)。
45
- const qualifiedMeta = schema === "public" ? "t_schema_meta" : `"${schema}".t_schema_meta`;
46
- const probe = (await tx.execute(
47
- sql`SELECT to_regclass(${qualifiedMeta}) IS NOT NULL AS exists`,
48
- )) as unknown as { rows: { exists: boolean }[] };
49
- const metaExists = probe.rows[0]?.exists === true;
50
- // DDL 一次一条(PG extended query protocol 拒绝多语句字符串),
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 storeId = store[0]?.fStoreId;
91
- if (storeId === undefined || storeId.length === 0) {
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 storeId;
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<THKT>): BackendTx {
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(exec: PgAsyncDatabase<THKT>, key: string): Promise<string | undefined> {
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<THKT>,
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<THKT>,
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(exec: PgAsyncDatabase<THKT>, id: SessionId): Promise<number | null> {
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<THKT>,
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(exec: PgAsyncDatabase<THKT>, events: EventInsert[]): Promise<void> {
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<THKT>,
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<THKT>,
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(exec: PgAsyncDatabase<THKT>, id: SessionId): Promise<void> {
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<THKT>,
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<THKT>,
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 async getLastBridge(
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 = 2;
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"