@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
@@ -1,6 +1,7 @@
1
- import { m as sessionInsertRow, p as sessionConflictRow } from "./log-BIuXv09P.mjs";
1
+ import { g as titleOfEventData, m as sessionInsertRow, p as sessionConflictRow } from "./log-DO69NQnn.mjs";
2
2
  import { randomUUID } from "node:crypto";
3
- import { and, eq, gte, sql } from "drizzle-orm";
3
+ import { SessionId, SessionLogOffset, SessionSeq } from "@deepseek-ai/dsh-session";
4
+ import { and, desc, eq, gte, isNotNull, notInArray, sql } from "drizzle-orm";
4
5
  import { check, index, integer, sqliteTable, text, unique } from "drizzle-orm/sqlite-core";
5
6
  import { bigint, check as check$1, index as index$1, integer as integer$1, pgSchema, pgTable, serial, text as text$1, unique as unique$1 } from "drizzle-orm/pg-core";
6
7
  import { readdirSync, statSync } from "node:fs";
@@ -196,7 +197,13 @@ const sessions = {
196
197
  f_revision: {
197
198
  type: "integer",
198
199
  notNull: true
199
- }
200
+ },
201
+ /** 归档标记:非空即该会话已归档(毫秒时间戳),NULL 表示未归档。 */
202
+ f_archived_at: { type: "bigint" },
203
+ /** 最新会话标题(最后一条 `session/title` 事件的 title),NULL 表示尚无标题。 */
204
+ f_title: { type: "text" },
205
+ /** `f_title` 来源事件的稠密 seq(列表 hint 的水位)。 */
206
+ f_title_seq: { type: "integer" }
200
207
  }
201
208
  };
202
209
  //#endregion
@@ -302,20 +309,193 @@ const sessionEvents$1 = {
302
309
  indexes: { idx_session_events_event_id: { columns: ["f_event_id"] } }
303
310
  };
304
311
  //#endregion
312
+ //#region src/entities/v3/storage-units.ts
313
+ /** storage 域版本账本:域首次打开时写入 descriptor version,其后按 accepted 集合校验。 */
314
+ const storageUnits = {
315
+ name: "t_storage_units",
316
+ columns: {
317
+ f_name: {
318
+ type: "text",
319
+ primaryKey: true
320
+ },
321
+ f_version: {
322
+ type: "integer",
323
+ notNull: true
324
+ }
325
+ }
326
+ };
327
+ //#endregion
328
+ //#region src/entities/v3/workspaces.ts
329
+ /**
330
+ * workspace 域记录表(上游 `workspace` 域 v2 的 `workspaces` 表):
331
+ * 记录本身拆成语义列;会话归属在 {@link workspaceSessions}(带顺序),
332
+ * 显示顺序由 `f_position` 承载(未进入 `workspaceIds` 的记录为 -1)。
333
+ */
334
+ const workspaces = {
335
+ name: "t_workspaces",
336
+ columns: {
337
+ f_id: {
338
+ type: "serial",
339
+ primaryKey: true
340
+ },
341
+ f_workspace_id: {
342
+ type: "text",
343
+ notNull: true,
344
+ unique: true
345
+ },
346
+ f_path: {
347
+ type: "text",
348
+ notNull: true
349
+ },
350
+ f_title: {
351
+ type: "text",
352
+ notNull: true
353
+ },
354
+ f_created_at: {
355
+ type: "text",
356
+ notNull: true
357
+ },
358
+ f_updated_at: {
359
+ type: "text",
360
+ notNull: true
361
+ },
362
+ f_position: {
363
+ type: "integer",
364
+ notNull: true,
365
+ default: -1
366
+ }
367
+ }
368
+ };
369
+ //#endregion
370
+ //#region src/entities/v3/workspace-sessions.ts
371
+ /**
372
+ * workspace 的会话归属(上游 `workspaceRecord.sessionIds` 的数组顺序):
373
+ * 一个会话一行,`f_position` 是归属显示顺序。会话 id 有独立索引,可按
374
+ * session 反查归属 workspace。
375
+ */
376
+ const workspaceSessions = {
377
+ name: "t_workspace_sessions",
378
+ columns: {
379
+ f_id: {
380
+ type: "serial",
381
+ primaryKey: true
382
+ },
383
+ f_workspace_id: {
384
+ type: "text",
385
+ notNull: true,
386
+ references: {
387
+ table: "t_workspaces",
388
+ column: "f_workspace_id",
389
+ onDelete: "cascade"
390
+ }
391
+ },
392
+ f_session_id: {
393
+ type: "text",
394
+ notNull: true
395
+ },
396
+ f_position: {
397
+ type: "integer",
398
+ notNull: true
399
+ }
400
+ },
401
+ uniques: { uq_workspace_sessions_workspace_session: { columns: ["f_workspace_id", "f_session_id"] } },
402
+ indexes: { idx_workspace_sessions_session_id: { columns: ["f_session_id"] } }
403
+ };
404
+ //#endregion
405
+ //#region src/entities/v3/workspace-state.ts
406
+ /**
407
+ * workspace 域 global 单例(上游 `workspaceDomainState`):`initialized` 是
408
+ * 引导标记,`pendingMutation` 拆成操作与目标两列(create / delete 的可恢复
409
+ * 两写标记)。显示顺序(`workspaceIds`)在 `t_workspaces.f_position`,
410
+ * 归档集在 `t_session_archives`。
411
+ */
412
+ const workspaceState = {
413
+ name: "t_workspace_state",
414
+ columns: {
415
+ f_singleton: {
416
+ type: "integer",
417
+ primaryKey: true
418
+ },
419
+ f_initialized: {
420
+ type: "integer",
421
+ notNull: true
422
+ },
423
+ f_pending_operation: { type: "text" },
424
+ f_pending_workspace_id: { type: "text" }
425
+ },
426
+ checks: { ck_workspace_state_singleton: { expression: "f_singleton = 1" } }
427
+ };
428
+ //#endregion
429
+ //#region src/entities/v3/session-projcache-rows.ts
430
+ /**
431
+ * 投影 checkpoint 行(上游 checkpoint 的 `(sessionId, key, ver, seq, val)`
432
+ * 逐行落库):`f_seq` 是该行的日志水位、`f_ver` 是投影单元的 stateVersion。
433
+ * 行是折出捷径、非权威,版本不匹配时整行丢弃。
434
+ *
435
+ * checkpoint 的日志 identity 不另存一份:它与 `t_sessions` 的行是 1:1,直接
436
+ * 复用该行的 `f_version` / `f_created_at` / `f_cwd` / `f_seed_length` 做校验。
437
+ */
438
+ const sessionProjcacheRows = {
439
+ name: "t_session_projcache_row",
440
+ columns: {
441
+ f_id: {
442
+ type: "serial",
443
+ primaryKey: true
444
+ },
445
+ f_session_id: {
446
+ type: "text",
447
+ notNull: true,
448
+ references: {
449
+ table: "t_sessions",
450
+ column: "f_session_id",
451
+ onDelete: "cascade"
452
+ }
453
+ },
454
+ f_key: {
455
+ type: "text",
456
+ notNull: true
457
+ },
458
+ f_ver: {
459
+ type: "integer",
460
+ notNull: true
461
+ },
462
+ f_seq: {
463
+ type: "integer",
464
+ notNull: true
465
+ },
466
+ f_val: {
467
+ type: "text",
468
+ notNull: true
469
+ }
470
+ },
471
+ uniques: { uq_session_projcache_row_session_key: { columns: ["f_session_id", "f_key"] } },
472
+ indexes: { idx_session_projcache_row_key: { columns: ["f_key"] } }
473
+ };
474
+ //#endregion
305
475
  //#region src/entities/v3/index.ts
306
476
  const sqliteTableDefs = [
307
477
  persistenceState,
308
478
  schemaMeta,
309
479
  sessions,
310
480
  events,
311
- sessionEvents$1
481
+ sessionEvents$1,
482
+ storageUnits,
483
+ workspaces,
484
+ workspaceSessions,
485
+ workspaceState,
486
+ sessionProjcacheRows
312
487
  ];
313
488
  const postgresTableDefs = [
314
489
  persistenceState,
315
490
  schemaMeta,
316
491
  sessions,
317
492
  events,
318
- sessionEvents$1
493
+ sessionEvents$1,
494
+ storageUnits,
495
+ workspaces,
496
+ workspaceSessions,
497
+ workspaceState,
498
+ sessionProjcacheRows
319
499
  ];
320
500
  ({ ...sessionEvents$1 }), { ...sessionEvents$1.columns };
321
501
  //#endregion
@@ -329,6 +509,11 @@ const tSchemaMeta = sqliteTables["t_schema_meta"];
329
509
  const tSessions = sqliteTables["t_sessions"];
330
510
  const tEvents = sqliteTables["t_events"];
331
511
  const tSessionEvents = sqliteTables["t_session_events"];
512
+ const tStorageUnits = sqliteTables["t_storage_units"];
513
+ const tWorkspaces = sqliteTables["t_workspaces"];
514
+ const tWorkspaceSessions = sqliteTables["t_workspace_sessions"];
515
+ const tWorkspaceState = sqliteTables["t_workspace_state"];
516
+ const tSessionProjcacheRows = sqliteTables["t_session_projcache_row"];
332
517
  const DEFAULT_BUSY_TIMEOUT_MS = 5e3;
333
518
  function eventKind(event) {
334
519
  switch (event.type) {
@@ -343,6 +528,8 @@ function eventKind(event) {
343
528
  case "session/end-seed": return "turn";
344
529
  case "tool/call":
345
530
  case "tool/result":
531
+ case "tool/ptc-dispatch-start":
532
+ case "tool/ptc-dispatch":
346
533
  case "tool/code-dispatch-start":
347
534
  case "tool/code-dispatch": return "tool";
348
535
  case "request/header":
@@ -416,6 +603,8 @@ function eventDimensions(event) {
416
603
  name: typeof data["name"] === "string" ? data["name"] : "",
417
604
  actionId: typeof data["callId"] === "string" ? data["callId"] : ""
418
605
  };
606
+ case "tool/ptc-dispatch-start":
607
+ case "tool/ptc-dispatch":
419
608
  case "tool/code-dispatch-start":
420
609
  case "tool/code-dispatch": return {
421
610
  kind,
@@ -481,6 +670,254 @@ function eventDimensions(event) {
481
670
  }
482
671
  }
483
672
  //#endregion
673
+ //#region src/storage-takeover/repository.ts
674
+ /**
675
+ * storages 接管表的方言无关实现:SQLite 与 PostgreSQL 的 drizzle 查询构建器
676
+ * 共享同一套调用形状(SQLite 走同步 `.all()`/`.get()`/`.run()`,PostgreSQL
677
+ * 走 thenable),差别由本模块的执行 helper 吸收。
678
+ *
679
+ * workspace 域是权威数据:记录、归属、显示顺序与归档集都拆成语义表,每次写入
680
+ * 在介质事务内完成(`host.writeAtomically`),不留部分应用的中间态。
681
+ */
682
+ /** 多行查询:SQLite 同步 `.all()`,PostgreSQL await。 */
683
+ async function allRows(query) {
684
+ const q = query;
685
+ if (typeof q.all === "function") return q.all();
686
+ return await query;
687
+ }
688
+ /** 单行查询:SQLite 同步 `.get()`,PostgreSQL await 后取首行。 */
689
+ async function oneRow(query) {
690
+ const q = query;
691
+ if (typeof q.get === "function") return q.get();
692
+ return (await query)[0];
693
+ }
694
+ /** 写入语句:SQLite 同步 `.run()`,PostgreSQL await。 */
695
+ async function runQuery(query) {
696
+ const q = query;
697
+ if (typeof q.run === "function") {
698
+ q.run();
699
+ return;
700
+ }
701
+ await query;
702
+ }
703
+ /** 一行会话元数据 → 上游 `CheckpointIdentity`。 */
704
+ function identityOfSession(row) {
705
+ return {
706
+ formatVersion: row.fVersion,
707
+ createdAt: row.fCreatedAt,
708
+ ...row.fCwd === null ? {} : { cwd: row.fCwd },
709
+ isSeeded: row.fSeedLength !== null,
710
+ ...row.fSeedLength === null ? {} : { inheritedEventCount: SessionLogOffset(row.fSeedLength) }
711
+ };
712
+ }
713
+ /** 存储的整数水位 → 上游 seq cursor(-1 是空日志哨兵)。 */
714
+ function seqCursor(seq) {
715
+ return seq === -1 ? -1 : SessionSeq(seq);
716
+ }
717
+ /** 两写标记的两列 → 上游 `pendingMutation`(未知操作按缺失处理)。 */
718
+ function pendingMutationOf(row) {
719
+ const operation = row.fPendingOperation;
720
+ if (operation !== "create" && operation !== "delete") return void 0;
721
+ return {
722
+ operation,
723
+ workspaceId: row.fPendingWorkspaceId ?? ""
724
+ };
725
+ }
726
+ /**
727
+ * 构建一个介质的 storages 接管访问层。
728
+ * @param host - 介质句柄、方言表对象与事务执行器。
729
+ * @returns 方言无关的访问层。
730
+ */
731
+ function createStorageRepository(host) {
732
+ const tables = host.tables;
733
+ const tUnits = tables["t_storage_units"];
734
+ const tSessions = tables["t_sessions"];
735
+ const tWorkspaces = tables["t_workspaces"];
736
+ const tWorkspaceSessions = tables["t_workspace_sessions"];
737
+ const tWorkspaceState = tables["t_workspace_state"];
738
+ const tProjcacheRows = tables["t_session_projcache_row"];
739
+ /** checkpoint identity 的列投影:直接复用会话行,不再单存记录头。 */
740
+ const sessionIdentity = {
741
+ fSessionId: tSessions.fSessionId,
742
+ fVersion: tSessions.fVersion,
743
+ fCreatedAt: tSessions.fCreatedAt,
744
+ fCwd: tSessions.fCwd,
745
+ fSeedLength: tSessions.fSeedLength
746
+ };
747
+ const dbx = async () => await host.db();
748
+ return {
749
+ async readUnitVersion(name) {
750
+ return (await oneRow((await dbx()).select().from(tUnits).where(eq(tUnits.fName, name))))?.fVersion;
751
+ },
752
+ async insertUnitVersion(name, version) {
753
+ await runQuery((await dbx()).insert(tUnits).values({
754
+ fName: name,
755
+ fVersion: version
756
+ }).onConflictDoNothing());
757
+ },
758
+ async listWorkspaces() {
759
+ const db = await dbx();
760
+ const rows = await allRows(db.select().from(tWorkspaces));
761
+ const links = await allRows(db.select().from(tWorkspaceSessions));
762
+ const sessions = /* @__PURE__ */ new Map();
763
+ for (const link of links) {
764
+ const owned = sessions.get(link.fWorkspaceId) ?? [];
765
+ owned.push({
766
+ id: link.fSessionId,
767
+ position: link.fPosition
768
+ });
769
+ sessions.set(link.fWorkspaceId, owned);
770
+ }
771
+ return rows.map((row) => ({
772
+ id: row.fWorkspaceId,
773
+ record: {
774
+ path: row.fPath,
775
+ title: row.fTitle,
776
+ sessionIds: (sessions.get(row.fWorkspaceId) ?? []).sort((left, right) => left.position - right.position).map((entry) => SessionId(entry.id)),
777
+ createdAt: row.fCreatedAt,
778
+ updatedAt: row.fUpdatedAt
779
+ }
780
+ }));
781
+ },
782
+ async putWorkspace(id, record) {
783
+ await host.writeAtomically(async () => {
784
+ const db = await dbx();
785
+ const values = {
786
+ fWorkspaceId: id,
787
+ fPath: record.path,
788
+ fTitle: record.title,
789
+ fCreatedAt: record.createdAt,
790
+ fUpdatedAt: record.updatedAt
791
+ };
792
+ await runQuery(db.insert(tWorkspaces).values(values).onConflictDoUpdate({
793
+ target: tWorkspaces.fWorkspaceId,
794
+ set: values
795
+ }));
796
+ await runQuery(db.delete(tWorkspaceSessions).where(eq(tWorkspaceSessions.fWorkspaceId, id)));
797
+ const links = record.sessionIds.map((sessionId, position) => ({
798
+ fWorkspaceId: id,
799
+ fSessionId: sessionId,
800
+ fPosition: position
801
+ }));
802
+ if (links.length > 0) await runQuery(db.insert(tWorkspaceSessions).values(links));
803
+ });
804
+ },
805
+ async deleteWorkspace(id) {
806
+ await host.writeAtomically(async () => {
807
+ const db = await dbx();
808
+ await runQuery(db.delete(tWorkspaceSessions).where(eq(tWorkspaceSessions.fWorkspaceId, id)));
809
+ await runQuery(db.delete(tWorkspaces).where(eq(tWorkspaces.fWorkspaceId, id)));
810
+ });
811
+ },
812
+ async readWorkspaceState() {
813
+ const db = await dbx();
814
+ const row = await oneRow(db.select().from(tWorkspaceState).where(eq(tWorkspaceState.fSingleton, 1)));
815
+ if (row === void 0) return null;
816
+ const ordered = await allRows(db.select().from(tWorkspaces).where(gte(tWorkspaces.fPosition, 0)).orderBy(tWorkspaces.fPosition));
817
+ const archives = await allRows(db.select({ fSessionId: tSessions.fSessionId }).from(tSessions).where(isNotNull(tSessions.fArchivedAt)).orderBy(tSessions.fArchivedAt));
818
+ const pendingMutation = pendingMutationOf(row);
819
+ return {
820
+ initialized: row.fInitialized !== 0,
821
+ workspaceIds: ordered.map((entry) => entry.fWorkspaceId),
822
+ archivedSessionIds: archives.map((entry) => entry.fSessionId),
823
+ ...pendingMutation === void 0 ? {} : { pendingMutation }
824
+ };
825
+ },
826
+ async writeWorkspaceState(state) {
827
+ await host.writeAtomically(async () => {
828
+ const db = await dbx();
829
+ const pending = state.pendingMutation;
830
+ const values = {
831
+ fSingleton: 1,
832
+ fInitialized: state.initialized ? 1 : 0,
833
+ fPendingOperation: pending === void 0 || typeof pending.operation !== "string" ? null : pending.operation,
834
+ fPendingWorkspaceId: pending === void 0 || typeof pending.workspaceId !== "string" ? null : pending.workspaceId
835
+ };
836
+ await runQuery(db.insert(tWorkspaceState).values(values).onConflictDoUpdate({
837
+ target: tWorkspaceState.fSingleton,
838
+ set: values
839
+ }));
840
+ await runQuery(db.update(tWorkspaces).set({ fPosition: -1 }));
841
+ for (const [position, workspaceId] of state.workspaceIds.entries()) await runQuery(db.update(tWorkspaces).set({ fPosition: position }).where(eq(tWorkspaces.fWorkspaceId, workspaceId)));
842
+ const archived = state.archivedSessionIds;
843
+ await runQuery(archived.length === 0 ? db.update(tSessions).set({ fArchivedAt: null }).where(isNotNull(tSessions.fArchivedAt)) : db.update(tSessions).set({ fArchivedAt: null }).where(and(isNotNull(tSessions.fArchivedAt), notInArray(tSessions.fSessionId, archived))));
844
+ const stamp = Date.now();
845
+ for (const sessionId of archived) await runQuery(db.update(tSessions).set({ fArchivedAt: sql`COALESCE(${tSessions.fArchivedAt}, ${stamp})` }).where(eq(tSessions.fSessionId, sessionId)));
846
+ });
847
+ },
848
+ async loadProjcache() {
849
+ const db = await dbx();
850
+ const rows = await allRows(db.select().from(tProjcacheRows));
851
+ if (rows.length === 0) return [];
852
+ const sessions = await allRows(db.select(sessionIdentity).from(tSessions));
853
+ const entries = /* @__PURE__ */ new Map();
854
+ for (const session of sessions) entries.set(session.fSessionId, {
855
+ sessionId: SessionId(session.fSessionId),
856
+ identity: identityOfSession(session),
857
+ rows: {}
858
+ });
859
+ for (const row of rows) {
860
+ const entry = entries.get(row.fSessionId);
861
+ if (entry === void 0) continue;
862
+ entry.rows[row.fKey] = {
863
+ ver: row.fVer,
864
+ seq: seqCursor(row.fSeq),
865
+ val: JSON.parse(row.fVal)
866
+ };
867
+ }
868
+ return [...entries.values()].filter((entry) => Object.keys(entry.rows).length > 0);
869
+ },
870
+ async putProjcache(sessionId, rows) {
871
+ await host.writeAtomically(async () => {
872
+ const db = await dbx();
873
+ await runQuery(db.delete(tProjcacheRows).where(eq(tProjcacheRows.fSessionId, sessionId)));
874
+ const values = Object.entries(rows).map(([key, row]) => ({
875
+ fSessionId: sessionId,
876
+ fKey: key,
877
+ fVer: row.ver,
878
+ fSeq: row.seq,
879
+ fVal: JSON.stringify(row.val)
880
+ }));
881
+ if (values.length > 0) await runQuery(db.insert(tProjcacheRows).values(values));
882
+ });
883
+ },
884
+ async deleteProjcache(sessionId) {
885
+ await runQuery((await dbx()).delete(tProjcacheRows).where(eq(tProjcacheRows.fSessionId, sessionId)));
886
+ },
887
+ ...host.dbSync === void 0 ? {} : {
888
+ readProjcacheSync: (sessionId) => {
889
+ const db = host.dbSync();
890
+ const stored = db.select().from(tProjcacheRows).where(eq(tProjcacheRows.fSessionId, sessionId)).all();
891
+ if (stored.length === 0) return void 0;
892
+ const session = db.select(sessionIdentity).from(tSessions).where(eq(tSessions.fSessionId, sessionId)).get();
893
+ if (session === void 0) return void 0;
894
+ const rows = {};
895
+ for (const row of stored) rows[row.fKey] = {
896
+ ver: row.fVer,
897
+ seq: seqCursor(row.fSeq),
898
+ val: JSON.parse(row.fVal)
899
+ };
900
+ return {
901
+ sessionId: SessionId(sessionId),
902
+ identity: identityOfSession(session),
903
+ rows
904
+ };
905
+ },
906
+ readSessionTitleSync: (sessionId) => {
907
+ const row = host.dbSync().select({
908
+ fTitle: tSessions.fTitle,
909
+ fTitleSeq: tSessions.fTitleSeq
910
+ }).from(tSessions).where(eq(tSessions.fSessionId, sessionId)).get();
911
+ if (row === void 0 || row.fTitle === null || row.fTitleSeq === null) return void 0;
912
+ return {
913
+ title: row.fTitle,
914
+ seq: row.fTitleSeq
915
+ };
916
+ }
917
+ }
918
+ };
919
+ }
920
+ //#endregion
484
921
  //#region src/sqlite.ts
485
922
  /** drizzle-kit 生成的迁移目录(随包根 drizzle/ 发布;src/dist 形态经相对 URL 统一解析)。 */
486
923
  const sqliteMigrationsDir = fileURLToPath(new URL("../drizzle/sqlite/", import.meta.url));
@@ -555,12 +992,60 @@ var SqliteBackend = class SqliteBackend {
555
992
  options;
556
993
  kind = "sqlite";
557
994
  storeIdentity;
995
+ /** storages 接管表访问层;句柄在 open() 完成后解析。 */
996
+ storage;
558
997
  dbPath = "";
559
998
  db;
999
+ dbReady;
1000
+ resolveDb;
1001
+ rejectDb;
560
1002
  constructor(options) {
561
1003
  this.options = options;
1004
+ this.dbReady = new Promise((resolve, reject) => {
1005
+ this.resolveDb = resolve;
1006
+ this.rejectDb = reject;
1007
+ });
1008
+ this.dbReady.catch(() => {});
1009
+ this.storage = createStorageRepository({
1010
+ db: () => this.dbReady,
1011
+ dbSync: () => {
1012
+ if (this.db === void 0) throw new Error("sqlite session database is not open");
1013
+ return this.db;
1014
+ },
1015
+ writeAtomically: (fn) => enqueueSqliteTx(this.dbPath, async () => {
1016
+ const db = await this.dbReady;
1017
+ db.$client.exec("BEGIN IMMEDIATE");
1018
+ try {
1019
+ const result = await fn();
1020
+ db.$client.exec("COMMIT");
1021
+ return result;
1022
+ } catch (error) {
1023
+ try {
1024
+ db.$client.exec("ROLLBACK");
1025
+ } catch {}
1026
+ throw error;
1027
+ }
1028
+ }),
1029
+ tables: {
1030
+ t_sessions: tSessions,
1031
+ t_storage_units: tStorageUnits,
1032
+ t_workspaces: tWorkspaces,
1033
+ t_workspace_sessions: tWorkspaceSessions,
1034
+ t_workspace_state: tWorkspaceState,
1035
+ t_session_projcache_row: tSessionProjcacheRows
1036
+ }
1037
+ });
562
1038
  }
563
1039
  async open() {
1040
+ try {
1041
+ await this.doOpen();
1042
+ this.resolveDb(this.db);
1043
+ } catch (error) {
1044
+ this.rejectDb(error);
1045
+ throw error;
1046
+ }
1047
+ }
1048
+ async doOpen() {
564
1049
  const actual = this.options.path === ":memory:" ? this.options.path : resolve(this.options.path);
565
1050
  this.dbPath = actual;
566
1051
  if (actual !== ":memory:") {
@@ -626,6 +1111,7 @@ var SqliteBackend = class SqliteBackend {
626
1111
  insertBridges: (rows) => this.insertBridges(rows),
627
1112
  updateHead: (id, headEventId, headSequence) => this.updateHead(id, headEventId, headSequence),
628
1113
  bumpRevision: (id) => this.bumpRevision(id),
1114
+ refreshTitle: (id) => this.refreshTitle(id),
629
1115
  deleteBridgeTail: (id, fromSequence) => this.deleteBridgeTail(id, fromSequence),
630
1116
  getPrevBridge: (id, sequence) => this.getPrevBridge(id, sequence)
631
1117
  };
@@ -671,6 +1157,18 @@ var SqliteBackend = class SqliteBackend {
671
1157
  async bumpRevision(id) {
672
1158
  this.db.update(tSessions).set({ fRevision: sql`${tSessions.fRevision} + 1` }).where(eq(tSessions.fSessionId, id)).run();
673
1159
  }
1160
+ /** 从事件表重算标题(最后一条 `session/title`),写回会话行的 f_title 列。 */
1161
+ async refreshTitle(id) {
1162
+ const row = this.db.select({
1163
+ fSequence: tSessionEvents.fSequence,
1164
+ fData: tEvents.fData
1165
+ }).from(tSessionEvents).innerJoin(tEvents, eq(tEvents.fEventId, tSessionEvents.fEventId)).where(and(eq(tSessionEvents.fSessionId, id), eq(tEvents.fType, "session/title"))).orderBy(desc(tSessionEvents.fSequence)).limit(1).get();
1166
+ const title = row === void 0 ? void 0 : titleOfEventData(row.fData);
1167
+ this.db.update(tSessions).set({
1168
+ fTitle: title ?? null,
1169
+ fTitleSeq: title === void 0 ? null : row.fSequence
1170
+ }).where(eq(tSessions.fSessionId, id)).run();
1171
+ }
674
1172
  async deleteBridgeTail(id, fromSequence) {
675
1173
  this.db.delete(tSessionEvents).where(and(eq(tSessionEvents.fSessionId, id), gte(tSessionEvents.fSequence, fromSequence))).run();
676
1174
  }
@@ -696,4 +1194,4 @@ var SqliteBackend = class SqliteBackend {
696
1194
  }
697
1195
  };
698
1196
  //#endregion
699
- export { SCHEMA_VERSION as a, eventKind as c, tSchemaMeta as d, tSessionEvents as f, WriteGuard as g, toPostgresSchema as h, EVENT_ENCODING as i, tEvents as l, postgresTableDefs as m, openDatabase as n, SESSION_PERSISTENCE_SQLITE_APPLICATION_ID as o, tSessions as p, DEFAULT_BUSY_TIMEOUT_MS as r, eventDimensions as s, SqliteBackend as t, tPersistenceState as u };
1197
+ export { WriteGuard as S, tWorkspaceSessions as _, EVENT_ENCODING as a, postgresTableDefs as b, eventDimensions as c, tPersistenceState as d, tSchemaMeta as f, tStorageUnits as g, tSessions as h, DEFAULT_BUSY_TIMEOUT_MS as i, eventKind as l, tSessionProjcacheRows as m, openDatabase as n, SCHEMA_VERSION as o, tSessionEvents as p, createStorageRepository as r, SESSION_PERSISTENCE_SQLITE_APPLICATION_ID as s, SqliteBackend as t, tEvents as u, tWorkspaceState as v, toPostgresSchema as x, tWorkspaces as y };
@@ -1,4 +1,4 @@
1
- import { _ as BackendTx, a as JournalMode, c as eventDimensions, d as tPersistenceState, f as tSchemaMeta, g as Backend, h as WriteGuard, i as EventRole, l as eventKind, m as tSessions, n as EVENT_ENCODING, o as SCHEMA_VERSION, p as tSessionEvents, r as EventKind, s as SESSION_PERSISTENCE_SQLITE_APPLICATION_ID, t as DEFAULT_BUSY_TIMEOUT_MS, u as tEvents, v as EventRow, y as SessionRow } from "./schema-COK7-wRV.mjs";
1
+ import { C as EventRow, S as BackendTx, T as StorageRepository, _ as tWorkspaceSessions, a as JournalMode, b as WriteGuard, c as eventDimensions, d as tPersistenceState, f as tSchemaMeta, g as tStorageUnits, h as tSessions, i as EventRole, l as eventKind, m as tSessionProjcacheRows, n as EVENT_ENCODING, o as SCHEMA_VERSION, p as tSessionEvents, r as EventKind, s as SESSION_PERSISTENCE_SQLITE_APPLICATION_ID, t as DEFAULT_BUSY_TIMEOUT_MS, u as tEvents, v as tWorkspaceState, w as SessionRow, x as Backend, y as tWorkspaces } from "./schema-Bo6Hy5gW.mjs";
2
2
  import { SessionId } from "@deepseek-ai/dsh-session";
3
3
  import { DatabaseSync } from "node:sqlite";
4
4
  //#region src/sqlite.d.ts
@@ -12,10 +12,16 @@ declare class SqliteBackend implements Backend {
12
12
  private readonly options;
13
13
  readonly kind: "sqlite";
14
14
  storeIdentity: string;
15
+ /** storages 接管表访问层;句柄在 open() 完成后解析。 */
16
+ readonly storage: StorageRepository;
15
17
  private dbPath;
16
18
  private db;
19
+ private readonly dbReady;
20
+ private resolveDb;
21
+ private rejectDb;
17
22
  constructor(options: SqliteBackendOptions);
18
23
  open(): Promise<void>;
24
+ private doOpen;
19
25
  close(): Promise<void>;
20
26
  getSession(id: SessionId): Promise<SessionRow | undefined>;
21
27
  getEventRows(id: SessionId, fromSequence?: number): Promise<EventRow[]>;
@@ -31,9 +37,11 @@ declare class SqliteBackend implements Backend {
31
37
  private insertBridges;
32
38
  private updateHead;
33
39
  private bumpRevision;
40
+ /** 从事件表重算标题(最后一条 `session/title`),写回会话行的 f_title 列。 */
41
+ private refreshTitle;
34
42
  private deleteBridgeTail;
35
43
  private getPrevBridge;
36
44
  private eventRows;
37
45
  }
38
46
  //#endregion
39
- export { DEFAULT_BUSY_TIMEOUT_MS, EVENT_ENCODING, EventKind, EventRole, type EventRow, JournalMode, SCHEMA_VERSION, SESSION_PERSISTENCE_SQLITE_APPLICATION_ID, type SessionRow, SqliteBackend, SqliteBackendOptions, WriteGuard, eventDimensions, eventKind, openDatabase, tEvents, tPersistenceState, tSchemaMeta, tSessionEvents, tSessions };
47
+ export { DEFAULT_BUSY_TIMEOUT_MS, EVENT_ENCODING, EventKind, EventRole, type EventRow, JournalMode, SCHEMA_VERSION, SESSION_PERSISTENCE_SQLITE_APPLICATION_ID, type SessionRow, SqliteBackend, SqliteBackendOptions, WriteGuard, eventDimensions, eventKind, openDatabase, tEvents, tPersistenceState, tSchemaMeta, tSessionEvents, tSessionProjcacheRows, tSessions, tStorageUnits, tWorkspaceSessions, tWorkspaceState, tWorkspaces };
package/dist/storage.mjs CHANGED
@@ -1,2 +1,2 @@
1
- import { a as SCHEMA_VERSION, c as eventKind, d as tSchemaMeta, f as tSessionEvents, g as WriteGuard, i as EVENT_ENCODING, l as tEvents, n as openDatabase, o as SESSION_PERSISTENCE_SQLITE_APPLICATION_ID, p as tSessions, r as DEFAULT_BUSY_TIMEOUT_MS, s as eventDimensions, t as SqliteBackend, u as tPersistenceState } from "./sqlite-IH5I48aL.mjs";
2
- export { DEFAULT_BUSY_TIMEOUT_MS, EVENT_ENCODING, SCHEMA_VERSION, SESSION_PERSISTENCE_SQLITE_APPLICATION_ID, SqliteBackend, WriteGuard, eventDimensions, eventKind, openDatabase, tEvents, tPersistenceState, tSchemaMeta, tSessionEvents, tSessions };
1
+ import { S as WriteGuard, _ as tWorkspaceSessions, a as EVENT_ENCODING, c as eventDimensions, d as tPersistenceState, f as tSchemaMeta, g as tStorageUnits, h as tSessions, i as DEFAULT_BUSY_TIMEOUT_MS, l as eventKind, m as tSessionProjcacheRows, n as openDatabase, o as SCHEMA_VERSION, p as tSessionEvents, s as SESSION_PERSISTENCE_SQLITE_APPLICATION_ID, t as SqliteBackend, u as tEvents, v as tWorkspaceState, y as tWorkspaces } from "./sqlite-BIPdMNQb.mjs";
2
+ export { DEFAULT_BUSY_TIMEOUT_MS, EVENT_ENCODING, SCHEMA_VERSION, SESSION_PERSISTENCE_SQLITE_APPLICATION_ID, SqliteBackend, WriteGuard, eventDimensions, eventKind, openDatabase, tEvents, tPersistenceState, tSchemaMeta, tSessionEvents, tSessionProjcacheRows, tSessions, tStorageUnits, tWorkspaceSessions, tWorkspaceState, tWorkspaces };
@@ -1,7 +1,7 @@
1
1
  import { SessionPersistence } from "@deepseek-ai/dsh-session-persistence";
2
2
  import { Session, SessionEvent, SessionHeader, SessionId } from "@deepseek-ai/dsh-session";
3
- import { SettingsNamespace, SettingsProvider } from "@deepseek-ai/dsh-settings";
4
3
  import { Context, Fiber } from "@deepseek-ai/cordis";
4
+ import { SettingsNamespace, SettingsProvider } from "@deepseek-ai/dsh-settings";
5
5
  //#region src/testing/helpers.d.ts
6
6
  declare class EmptySettings extends SettingsProvider {
7
7
  constructor(ctx: Context);
package/dist/testing.mjs CHANGED
@@ -1,8 +1,8 @@
1
1
  import { SessionAlreadyExistsError, SessionAlreadyOwnedError, SessionFormatUnsupportedError, SessionHandleClosedError, SessionPersistenceNotFoundError, SessionReadOnlyError } from "@deepseek-ai/dsh-session-persistence";
2
2
  import SessionStore, { SESSION_FORMAT_VERSION, SessionId, SessionLogOffset, SessionSeq } from "@deepseek-ai/dsh-session";
3
+ import { Context } from "@deepseek-ai/cordis";
3
4
  import { SettingsProvider } from "@deepseek-ai/dsh-settings";
4
5
  import { MessageId, createUserMessage, freezeMessage } from "@deepseek-ai/dsh-llm";
5
- import { Context } from "@deepseek-ai/cordis";
6
6
  //#region src/testing/helpers.ts
7
7
  var EmptySettings = class extends SettingsProvider {
8
8
  constructor(ctx) {