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

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 (48) hide show
  1. package/README.md +44 -10
  2. package/dist/artifact.d.mts +9 -2
  3. package/dist/artifact.mjs +3 -3
  4. package/dist/{import-BPEHfHNk.mjs → import-DZIAjOUr.mjs} +2 -1
  5. package/dist/import.d.mts +1 -1
  6. package/dist/import.mjs +1 -1
  7. package/dist/{index-CgAqCQAb.d.mts → index-BEKoV1Uy.d.mts} +11 -2
  8. package/dist/index.d.mts +3 -3
  9. package/dist/index.mjs +509 -6
  10. package/dist/{log-BIuXv09P.mjs → log-DO69NQnn.mjs} +16 -1
  11. package/dist/schema-Bo6Hy5gW.d.mts +3476 -0
  12. package/dist/{sqlite-IH5I48aL.mjs → sqlite-BIPdMNQb.mjs} +504 -6
  13. package/dist/storage.d.mts +10 -2
  14. package/dist/storage.mjs +2 -2
  15. package/dist/testing.d.mts +1 -1
  16. package/dist/testing.mjs +1 -1
  17. package/drizzle/postgres/20260910120001_v3_storage_tables/migration.sql +65 -0
  18. package/drizzle/postgres/20260910120001_v3_storage_tables/snapshot.json +1236 -0
  19. package/drizzle/postgres/20260910130001_v3_session_title_backfill/migration.sql +10 -0
  20. package/drizzle/postgres/20260910130001_v3_session_title_backfill/snapshot.json +1236 -0
  21. package/drizzle/sqlite/20260910120000_v3_storage_tables/migration.sql +66 -0
  22. package/drizzle/sqlite/20260910120000_v3_storage_tables/snapshot.json +958 -0
  23. package/drizzle/sqlite/20260910130000_v3_session_title_backfill/migration.sql +13 -0
  24. package/drizzle/sqlite/20260910130000_v3_session_title_backfill/snapshot.json +958 -0
  25. package/package.json +20 -18
  26. package/src/backend.ts +7 -0
  27. package/src/branch.ts +2 -0
  28. package/src/drizzle/postgres-v3.ts +5 -0
  29. package/src/drizzle/sqlite-v3.ts +5 -0
  30. package/src/entities/v3/index.ts +20 -0
  31. package/src/entities/v3/session-projcache-rows.ts +27 -0
  32. package/src/entities/v3/sessions.ts +6 -0
  33. package/src/entities/v3/storage-units.ts +10 -0
  34. package/src/entities/v3/workspace-sessions.ts +24 -0
  35. package/src/entities/v3/workspace-state.ts +18 -0
  36. package/src/entities/v3/workspaces.ts +19 -0
  37. package/src/import-storages.ts +173 -0
  38. package/src/index.ts +53 -0
  39. package/src/log.ts +19 -0
  40. package/src/postgres.ts +70 -2
  41. package/src/schema.ts +16 -2
  42. package/src/sqlite.ts +87 -4
  43. package/src/storage-takeover/index.ts +60 -0
  44. package/src/storage-takeover/projection-cache.ts +445 -0
  45. package/src/storage-takeover/repository.ts +420 -0
  46. package/src/storage-takeover/storage-backend.ts +177 -0
  47. package/src/storage-takeover/types.ts +82 -0
  48. package/dist/schema-COK7-wRV.d.mts +0 -104
package/src/postgres.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { randomUUID } from "node:crypto";
2
2
  import { readdirSync } from "node:fs";
3
3
  import { fileURLToPath } from "node:url";
4
- import { and, eq, gte, sql } from "drizzle-orm";
4
+ import { and, desc, eq, gte, sql } from "drizzle-orm";
5
5
  import type { PgAsyncDatabase, PgAsyncTransaction } from "drizzle-orm/pg-core";
6
6
  import type { NodePgDatabase, NodePgQueryResultHKT } from "drizzle-orm/node-postgres";
7
7
  import { migrate } from "drizzle-orm/node-postgres/migrator";
@@ -16,7 +16,9 @@ import {
16
16
  } from "./backend.ts";
17
17
  import { toPostgresSchema } from "./adapters/index.ts";
18
18
  import { postgresTableDefs } from "./entities/index.ts";
19
- import { sessionConflictRow, sessionInsertRow } from "./log.ts";
19
+ import { sessionConflictRow, sessionInsertRow, titleOfEventData } from "./log.ts";
20
+ import { createStorageRepository } from "./storage-takeover/repository.ts";
21
+ import type { StorageRepository } from "./storage-takeover/types.ts";
20
22
 
21
23
  /** drizzle-kit 生成的迁移目录(随包根 drizzle/ 发布;src/dist 形态经相对 URL 统一解析)。 */
22
24
  const postgresMigrationsDir = fileURLToPath(new URL("../drizzle/postgres/", import.meta.url));
@@ -35,14 +37,54 @@ export class PostgresBackend implements Backend {
35
37
 
36
38
  private readonly tables: Record<string, any>;
37
39
 
40
+ /** storages 接管表访问层;句柄在 open()(含迁移)完成后解析。 */
41
+ readonly storage: StorageRepository;
42
+
43
+ private readonly opened: Promise<void>;
44
+ private resolveOpened!: () => void;
45
+ private rejectOpened!: (error: unknown) => void;
46
+ /** 事务期间的连接覆盖:storage 写方法经它加入当前事务。 */
47
+ private txOverride: unknown;
48
+
38
49
  constructor(
39
50
  private readonly db: NodePgDatabase,
40
51
  private readonly options: PostgresBackendOptions,
41
52
  ) {
42
53
  this.tables = toPostgresSchema(postgresTableDefs, this.options.schema ?? "public");
54
+ this.opened = new Promise<void>((resolve, reject) => {
55
+ this.resolveOpened = resolve;
56
+ this.rejectOpened = reject;
57
+ });
58
+ // 每个 storage 原语都会重新 await 同一 promise,失败仍逐一可见;这个
59
+ // 守卫只防止 open 失败早于首次使用时的 unhandled rejection。
60
+ this.opened.catch(() => {});
61
+ this.storage = createStorageRepository({
62
+ db: () => this.opened.then(() => this.txOverride ?? this.db),
63
+ writeAtomically: (fn) =>
64
+ this.db.transaction(async (tx) => {
65
+ const previous = this.txOverride;
66
+ this.txOverride = tx;
67
+ try {
68
+ return await fn();
69
+ } finally {
70
+ this.txOverride = previous;
71
+ }
72
+ }),
73
+ tables: this.tables,
74
+ });
43
75
  }
44
76
 
45
77
  async open(): Promise<void> {
78
+ try {
79
+ await this.doOpen();
80
+ this.resolveOpened();
81
+ } catch (error: unknown) {
82
+ this.rejectOpened(error);
83
+ throw error;
84
+ }
85
+ }
86
+
87
+ private async doOpen(): Promise<void> {
46
88
  const schema = this.options.schema ?? "public";
47
89
  // drizzle-kit 迁移:v2 baseline 由旧版本建表(首次打开时标记为已应用),
48
90
  // 本版本只执行 v3 diff(删 f_original_seq)。
@@ -149,6 +191,7 @@ export class PostgresBackend implements Backend {
149
191
  updateHead: (id, headEventId, headSequence) =>
150
192
  this.updateHead(tx, id, headEventId, headSequence),
151
193
  bumpRevision: (id) => this.bumpRevision(tx, id),
194
+ refreshTitle: (id) => this.refreshTitle(tx, id),
152
195
  deleteBridgeTail: (id, fromSequence) => this.deleteBridgeTail(tx, id, fromSequence),
153
196
  getPrevBridge: (id, sequence) => this.getPrevBridge(tx, id, sequence),
154
197
  };
@@ -291,6 +334,31 @@ export class PostgresBackend implements Backend {
291
334
  .execute();
292
335
  }
293
336
 
337
+ /** 从事件表重算标题(最后一条 `session/title`),写回会话行的 f_title 列。 */
338
+ private async refreshTitle(
339
+ exec: PgAsyncDatabase<NodePgQueryResultHKT>,
340
+ id: SessionId,
341
+ ): Promise<void> {
342
+ const bridges = this.tables["t_session_events"];
343
+ const entities = this.tables["t_events"];
344
+ const row = (
345
+ await exec
346
+ .select({ fSequence: bridges.fSequence, fData: entities.fData })
347
+ .from(bridges)
348
+ .innerJoin(entities, eq(entities.fEventId, bridges.fEventId))
349
+ .where(and(eq(bridges.fSessionId, id), eq(entities.fType, "session/title")))
350
+ .orderBy(desc(bridges.fSequence))
351
+ .limit(1)
352
+ .execute()
353
+ )[0] as { fSequence: number; fData: string } | undefined;
354
+ const title = row === undefined ? undefined : titleOfEventData(row.fData);
355
+ await exec
356
+ .update(this.tables["t_sessions"])
357
+ .set({ fTitle: title ?? null, fTitleSeq: title === undefined ? null : row!.fSequence })
358
+ .where(eq(this.tables["t_sessions"].fSessionId, id))
359
+ .execute();
360
+ }
361
+
294
362
  private async deleteBridgeTail(
295
363
  exec: PgAsyncDatabase<NodePgQueryResultHKT>,
296
364
  id: SessionId,
package/src/schema.ts CHANGED
@@ -21,6 +21,16 @@ export const tEvents = sqliteTables["t_events"]!;
21
21
 
22
22
  export const tSessionEvents = sqliteTables["t_session_events"]!;
23
23
 
24
+ export const tStorageUnits = sqliteTables["t_storage_units"]!;
25
+
26
+ export const tWorkspaces = sqliteTables["t_workspaces"]!;
27
+
28
+ export const tWorkspaceSessions = sqliteTables["t_workspace_sessions"]!;
29
+
30
+ export const tWorkspaceState = sqliteTables["t_workspace_state"]!;
31
+
32
+ export const tSessionProjcacheRows = sqliteTables["t_session_projcache_row"]!;
33
+
24
34
  export type { SessionRow } from "./backend.ts";
25
35
 
26
36
  export type { EventRow } from "./backend.ts";
@@ -70,7 +80,9 @@ export function eventKind(event: { type: string; data?: unknown }): EventKind |
70
80
  return "turn";
71
81
  case "tool/call":
72
82
  case "tool/result":
73
- case "tool/code-dispatch-start":
83
+ case "tool/ptc-dispatch-start":
84
+ case "tool/ptc-dispatch":
85
+ case "tool/code-dispatch-start": // v2 旧名(读路径归一为 ptc)
74
86
  case "tool/code-dispatch":
75
87
  return "tool";
76
88
  case "request/header":
@@ -158,7 +170,9 @@ export function eventDimensions(event: SessionEvent): {
158
170
  name: typeof data["name"] === "string" ? data["name"] : "",
159
171
  actionId: typeof data["callId"] === "string" ? data["callId"] : "",
160
172
  };
161
- case "tool/code-dispatch-start":
173
+ case "tool/ptc-dispatch-start":
174
+ case "tool/ptc-dispatch":
175
+ case "tool/code-dispatch-start": // v2 旧名(读路径归一为 ptc)
162
176
  case "tool/code-dispatch":
163
177
  return {
164
178
  kind,
package/src/sqlite.ts CHANGED
@@ -4,7 +4,7 @@ import { mkdir, open } from "node:fs/promises";
4
4
  import { dirname, resolve } from "node:path";
5
5
  import { fileURLToPath } from "node:url";
6
6
  import { DatabaseSync } from "node:sqlite";
7
- import { and, eq, gte, sql } from "drizzle-orm";
7
+ import { and, desc, eq, gte, sql } from "drizzle-orm";
8
8
  import { drizzle, type NodeSQLiteDatabase } from "drizzle-orm/node-sqlite";
9
9
  import { migrate } from "drizzle-orm/node-sqlite/migrator";
10
10
  import type { SessionId } from "@deepseek-ai/dsh-session";
@@ -16,7 +16,7 @@ import {
16
16
  type EventRow,
17
17
  type SessionRow,
18
18
  } from "./backend.ts";
19
- import { sessionConflictRow, sessionInsertRow } from "./log.ts";
19
+ import { sessionConflictRow, sessionInsertRow, titleOfEventData } from "./log.ts";
20
20
  import {
21
21
  DEFAULT_BUSY_TIMEOUT_MS,
22
22
  SCHEMA_VERSION,
@@ -24,9 +24,16 @@ import {
24
24
  tEvents,
25
25
  tPersistenceState,
26
26
  tSessionEvents,
27
+ tSessionProjcacheRows,
27
28
  tSessions,
29
+ tStorageUnits,
30
+ tWorkspaceSessions,
31
+ tWorkspaceState,
32
+ tWorkspaces,
28
33
  type JournalMode,
29
34
  } from "./schema.ts";
35
+ import { createStorageRepository } from "./storage-takeover/repository.ts";
36
+ import type { StorageRepository } from "./storage-takeover/types.ts";
30
37
 
31
38
  type SqliteDb = NodeSQLiteDatabase & { $client: DatabaseSync };
32
39
 
@@ -178,12 +185,69 @@ export class SqliteBackend implements Backend {
178
185
  readonly kind = "sqlite" as const;
179
186
  storeIdentity!: string;
180
187
 
188
+ /** storages 接管表访问层;句柄在 open() 完成后解析。 */
189
+ readonly storage: StorageRepository;
190
+
181
191
  private dbPath = "";
182
192
  private db!: SqliteDb;
183
-
184
- constructor(private readonly options: SqliteBackendOptions) {}
193
+ private readonly dbReady: Promise<SqliteDb>;
194
+ private resolveDb!: (db: SqliteDb) => void;
195
+ private rejectDb!: (error: unknown) => void;
196
+
197
+ constructor(private readonly options: SqliteBackendOptions) {
198
+ this.dbReady = new Promise<SqliteDb>((resolve, reject) => {
199
+ this.resolveDb = resolve;
200
+ this.rejectDb = reject;
201
+ });
202
+ // 每个 storage 原语都会重新 await 同一 promise,失败仍逐一可见;这个
203
+ // 守卫只防止 open 失败早于首次使用时的 unhandled rejection。
204
+ this.dbReady.catch(() => {});
205
+ this.storage = createStorageRepository({
206
+ db: () => this.dbReady,
207
+ // 同步驱动:读路径直读介质(open 之前的调用是装配错误,fail loud)。
208
+ dbSync: () => {
209
+ if (this.db === undefined) throw new Error("sqlite session database is not open");
210
+ return this.db;
211
+ },
212
+ writeAtomically: (fn) =>
213
+ enqueueSqliteTx(this.dbPath, async () => {
214
+ const db = await this.dbReady;
215
+ db.$client.exec("BEGIN IMMEDIATE");
216
+ try {
217
+ const result = await fn();
218
+ db.$client.exec("COMMIT");
219
+ return result;
220
+ } catch (error: unknown) {
221
+ try {
222
+ db.$client.exec("ROLLBACK");
223
+ } catch {
224
+ // 原始 SQLite 失败仍是可操作的根因。
225
+ }
226
+ throw error;
227
+ }
228
+ }),
229
+ tables: {
230
+ t_sessions: tSessions,
231
+ t_storage_units: tStorageUnits,
232
+ t_workspaces: tWorkspaces,
233
+ t_workspace_sessions: tWorkspaceSessions,
234
+ t_workspace_state: tWorkspaceState,
235
+ t_session_projcache_row: tSessionProjcacheRows,
236
+ },
237
+ });
238
+ }
185
239
 
186
240
  async open(): Promise<void> {
241
+ try {
242
+ await this.doOpen();
243
+ this.resolveDb(this.db);
244
+ } catch (error: unknown) {
245
+ this.rejectDb(error);
246
+ throw error;
247
+ }
248
+ }
249
+
250
+ private async doOpen(): Promise<void> {
187
251
  const actual =
188
252
  this.options.path === ":memory:" ? this.options.path : resolve(this.options.path);
189
253
  this.dbPath = actual;
@@ -290,6 +354,7 @@ export class SqliteBackend implements Backend {
290
354
  insertBridges: (rows) => this.insertBridges(rows),
291
355
  updateHead: (id, headEventId, headSequence) => this.updateHead(id, headEventId, headSequence),
292
356
  bumpRevision: (id) => this.bumpRevision(id),
357
+ refreshTitle: (id) => this.refreshTitle(id),
293
358
  deleteBridgeTail: (id, fromSequence) => this.deleteBridgeTail(id, fromSequence),
294
359
  getPrevBridge: (id, sequence) => this.getPrevBridge(id, sequence),
295
360
  };
@@ -388,6 +453,24 @@ export class SqliteBackend implements Backend {
388
453
  .run();
389
454
  }
390
455
 
456
+ /** 从事件表重算标题(最后一条 `session/title`),写回会话行的 f_title 列。 */
457
+ private async refreshTitle(id: SessionId): Promise<void> {
458
+ const row = this.db
459
+ .select({ fSequence: tSessionEvents.fSequence, fData: tEvents.fData })
460
+ .from(tSessionEvents)
461
+ .innerJoin(tEvents, eq(tEvents.fEventId, tSessionEvents.fEventId))
462
+ .where(and(eq(tSessionEvents.fSessionId, id), eq(tEvents.fType, "session/title")))
463
+ .orderBy(desc(tSessionEvents.fSequence))
464
+ .limit(1)
465
+ .get() as { fSequence: number; fData: string } | undefined;
466
+ const title = row === undefined ? undefined : titleOfEventData(row.fData);
467
+ this.db
468
+ .update(tSessions)
469
+ .set({ fTitle: title ?? null, fTitleSeq: title === undefined ? null : row!.fSequence })
470
+ .where(eq(tSessions.fSessionId, id))
471
+ .run();
472
+ }
473
+
391
474
  private async deleteBridgeTail(id: SessionId, fromSequence: number): Promise<void> {
392
475
  this.db
393
476
  .delete(tSessionEvents)
@@ -0,0 +1,60 @@
1
+ /**
2
+ * storages 接管的装配入口:在 session-rdb 插件内注册
3
+ *
4
+ * 1. storage hub 的 `rdb` KV 后端——上游 `storage-domain` 的 `StorageBackend.kv`
5
+ * 契约实现(名字来自上游),把 workspace 域映射到 rdb 语义专用表;
6
+ * 2. `ctx.sessionProjectionCache` 服务——替换上游 `session-projection-cache`
7
+ * 插件,直接读写 rdb 的投影 checkpoint 表。
8
+ *
9
+ * 两者都经 `ctx.inject` 挂在可选依赖上:装配里缺 `storage` 或
10
+ * `sessionProjections`(纯 cordis 单测)时静默跳过,消费者按上游的缺服务
11
+ * 路径降级。
12
+ */
13
+
14
+ import type { Context } from "@deepseek-ai/cordis";
15
+ import { storageBackendServiceKey } from "@deepseek-ai/dsh-storage";
16
+ import type { ProjectionCacheConfig } from "./projection-cache.ts";
17
+ import { SessionProjectionCacheRdb } from "./projection-cache.ts";
18
+ import { RDB_STORAGE_BACKEND, RdbStorageBackend } from "./storage-backend.ts";
19
+ import type { StorageRepository } from "./types.ts";
20
+
21
+ /** 装配参数。 */
22
+ export interface StorageTakeoverOptions {
23
+ /** storages 接管表访问层(与事件日志同介质)。 */
24
+ repository: StorageRepository;
25
+ /** 投影缓存的写节流参数(上游 base 装配的部署值)。 */
26
+ projectionCache: ProjectionCacheConfig;
27
+ /** 介质就绪信号:投影缓存直读介质前必须等到。 */
28
+ ready: Promise<unknown>;
29
+ }
30
+
31
+ /**
32
+ * Install the storages takeover on the session-rdb plugin context.
33
+ * @param ctx - session-rdb plugin context.
34
+ * @param options - repository and projection-cache throttle parameters.
35
+ */
36
+ export function installStorageTakeover(ctx: Context, options: StorageTakeoverOptions): void {
37
+ // workspace 域:上游 workspaceRegistry 仍持有服务与内存态,这里只提供它
38
+ // 依赖的 storage-domain 介质(backend 名 `rdb`,由 profile patch 路由)。
39
+ ctx.inject(["storage"], (storageCtx) => {
40
+ const backend = new RdbStorageBackend(options.repository);
41
+ storageCtx.effect(() => {
42
+ const unregister = storageCtx.storage.backend.register(RDB_STORAGE_BACKEND, backend);
43
+ return async () => {
44
+ unregister();
45
+ await backend.close();
46
+ };
47
+ }, "session-rdb.storageBackend");
48
+ storageCtx.provide(storageBackendServiceKey(RDB_STORAGE_BACKEND), backend);
49
+ });
50
+
51
+ // 投影 checkpoint:上游插件被禁用后由本服务提供同名 API。
52
+ ctx.inject(["sessionProjections"], (projectionCtx) => {
53
+ new SessionProjectionCacheRdb(
54
+ projectionCtx,
55
+ options.projectionCache,
56
+ options.repository,
57
+ options.ready,
58
+ );
59
+ });
60
+ }