@morlay/session-rdb 0.0.20 → 0.0.21-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 (69) hide show
  1. package/README.md +59 -31
  2. package/dist/artifact.d.mts +25 -65
  3. package/dist/artifact.mjs +2 -2
  4. package/dist/{schema-BkQN5GyT.d.mts → backend-DpdtxYpz.d.mts} +346 -93
  5. package/dist/{branch-Co6xlbi0.mjs → branch-5JzX9rUq.mjs} +163 -52
  6. package/dist/deletion.d.mts +7 -0
  7. package/dist/deletion.mjs +2 -0
  8. package/dist/dist-vIVO6bA-.mjs +1524 -0
  9. package/dist/import.d.mts +4 -4
  10. package/dist/import.mjs +28 -11
  11. package/dist/index.d.mts +2 -3
  12. package/dist/index.mjs +4 -1631
  13. package/dist/{log-DO69NQnn.mjs → log-CnYct2Dv.mjs} +37 -82
  14. package/dist/{sqlite-DYExtbLo.mjs → sqlite-fpvm5Dzs.mjs} +205 -75
  15. package/dist/src-CWTWV7vx.mjs +1904 -0
  16. package/dist/storage.d.mts +14 -5
  17. package/dist/storage.mjs +2 -2
  18. package/dist/testing.d.mts +1 -26
  19. package/dist/testing.mjs +10503 -9614
  20. package/drizzle/postgres/20260918120000_v3_event_usage/migration.sql +14 -0
  21. package/drizzle/postgres/20260918120000_v3_event_usage/snapshot.json +1309 -0
  22. package/drizzle/sqlite/20260918120000_v3_event_usage/migration.sql +14 -0
  23. package/drizzle/sqlite/20260918120000_v3_event_usage/snapshot.json +1031 -0
  24. package/package.json +28 -29
  25. package/src/adapters/to-postgres.ts +1 -3
  26. package/src/adapters/to-sqlite.ts +0 -2
  27. package/src/adapters/types.ts +0 -3
  28. package/src/artifact.ts +0 -2
  29. package/src/backend.ts +31 -2
  30. package/src/branch.ts +223 -135
  31. package/src/deletion.ts +87 -0
  32. package/src/drizzle/postgres-v2.ts +0 -1
  33. package/src/drizzle/postgres-v3.ts +0 -1
  34. package/src/drizzle/sqlite-v2.ts +0 -1
  35. package/src/drizzle/sqlite-v3.ts +0 -1
  36. package/src/entities/v2/session-events.ts +0 -1
  37. package/src/entities/v3/event-usage.ts +25 -0
  38. package/src/entities/v3/events.ts +1 -3
  39. package/src/entities/v3/index.ts +4 -0
  40. package/src/entities/v3/session-events.ts +1 -2
  41. package/src/entities/v3/session-projcache-rows.ts +0 -8
  42. package/src/entities/v3/sessions.ts +3 -3
  43. package/src/entities/v3/storage-units.ts +0 -1
  44. package/src/entities/v3/workspace-sessions.ts +0 -5
  45. package/src/entities/v3/workspace-state.ts +0 -6
  46. package/src/entities/v3/workspaces.ts +0 -5
  47. package/src/export.ts +103 -0
  48. package/src/gc.ts +76 -0
  49. package/src/import-storages.ts +4 -37
  50. package/src/import.ts +54 -31
  51. package/src/index.ts +150 -114
  52. package/src/legacy.ts +4 -42
  53. package/src/log.ts +63 -106
  54. package/src/postgres.ts +249 -22
  55. package/src/schema.ts +6 -6
  56. package/src/session-query.ts +0 -4
  57. package/src/sqlite.ts +231 -55
  58. package/src/storage-takeover/index.ts +2 -28
  59. package/src/storage-takeover/projection-cache.ts +30 -134
  60. package/src/storage-takeover/repository.ts +20 -59
  61. package/src/storage-takeover/storage-backend.ts +3 -33
  62. package/src/storage-takeover/types.ts +14 -56
  63. package/src/storage.ts +0 -2
  64. package/src/testing/contract.ts +19 -50
  65. package/src/testing/coordinator-contract.ts +10 -25
  66. package/src/testing.ts +1 -4
  67. package/src/usage.ts +191 -0
  68. package/dist/index-D55G9n4k.d.mts +0 -271
  69. package/dist/magic-string.es-BgJoa-3K.mjs +0 -1017
@@ -1,11 +1,10 @@
1
- import { g as titleOfEventData, m as sessionInsertRow, p as sessionConflictRow } from "./log-DO69NQnn.mjs";
1
+ import { g as titleOfEventData, m as sessionInsertRow, p as sessionConflictRow, v as usageRowOf } from "./log-CnYct2Dv.mjs";
2
2
  import { randomUUID } from "node:crypto";
3
3
  import { SESSION_FORMAT_VERSION, SessionId, SessionLogOffset, SessionSeq } from "@deepseek-ai/dsh-session";
4
4
  import { and, desc, eq, gte, inArray, isNotNull, lt, notInArray, sql } from "drizzle-orm";
5
5
  import { check, index, integer, sqliteTable, text, unique } from "drizzle-orm/sqlite-core";
6
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";
7
- import { readdirSync, statSync } from "node:fs";
8
- import { mkdir, open } from "node:fs/promises";
7
+ import { mkdir, open, readdir, stat } from "node:fs/promises";
9
8
  import { dirname, resolve } from "node:path";
10
9
  import { fileURLToPath } from "node:url";
11
10
  import { DatabaseSync } from "node:sqlite";
@@ -198,11 +197,8 @@ const sessions = {
198
197
  type: "integer",
199
198
  notNull: true
200
199
  },
201
- /** 归档标记:非空即该会话已归档(毫秒时间戳),NULL 表示未归档。 */
202
200
  f_archived_at: { type: "bigint" },
203
- /** 最新会话标题(最后一条 `session/title` 事件的 title),NULL 表示尚无标题。 */
204
201
  f_title: { type: "text" },
205
- /** `f_title` 来源事件的稠密 seq(列表 hint 的水位)。 */
206
202
  f_title_seq: { type: "integer" }
207
203
  }
208
204
  };
@@ -310,7 +306,6 @@ const sessionEvents$1 = {
310
306
  };
311
307
  //#endregion
312
308
  //#region src/entities/v3/storage-units.ts
313
- /** storage 域版本账本:域首次打开时写入 descriptor version,其后按 accepted 集合校验。 */
314
309
  const storageUnits = {
315
310
  name: "t_storage_units",
316
311
  columns: {
@@ -326,11 +321,6 @@ const storageUnits = {
326
321
  };
327
322
  //#endregion
328
323
  //#region src/entities/v3/workspaces.ts
329
- /**
330
- * workspace 域记录表(上游 `workspace` 域 v2 的 `workspaces` 表):
331
- * 记录本身拆成语义列;会话归属在 {@link workspaceSessions}(带顺序),
332
- * 显示顺序由 `f_position` 承载(未进入 `workspaceIds` 的记录为 -1)。
333
- */
334
324
  const workspaces = {
335
325
  name: "t_workspaces",
336
326
  columns: {
@@ -368,11 +358,6 @@ const workspaces = {
368
358
  };
369
359
  //#endregion
370
360
  //#region src/entities/v3/workspace-sessions.ts
371
- /**
372
- * workspace 的会话归属(上游 `workspaceRecord.sessionIds` 的数组顺序):
373
- * 一个会话一行,`f_position` 是归属显示顺序。会话 id 有独立索引,可按
374
- * session 反查归属 workspace。
375
- */
376
361
  const workspaceSessions = {
377
362
  name: "t_workspace_sessions",
378
363
  columns: {
@@ -403,12 +388,6 @@ const workspaceSessions = {
403
388
  };
404
389
  //#endregion
405
390
  //#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_sessions.f_archived_at` 承载(归档不触碰归属槽位)。
411
- */
412
391
  const workspaceState = {
413
392
  name: "t_workspace_state",
414
393
  columns: {
@@ -427,14 +406,6 @@ const workspaceState = {
427
406
  };
428
407
  //#endregion
429
408
  //#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
409
  const sessionProjcacheRows = {
439
410
  name: "t_session_projcache_row",
440
411
  columns: {
@@ -472,6 +443,53 @@ const sessionProjcacheRows = {
472
443
  indexes: { idx_session_projcache_row_key: { columns: ["f_key"] } }
473
444
  };
474
445
  //#endregion
446
+ //#region src/entities/v3/event-usage.ts
447
+ /**
448
+ * 用量日志:一条 `assistant/message` 事件一行(按事件行唯一,fork 共享行不重复)。
449
+ * 统计只读这张表,不再逐行解析事件的 JSON;事件行本身仍留在 `t_events`。
450
+ */
451
+ const eventUsage = {
452
+ name: "t_event_usage",
453
+ columns: {
454
+ f_event_id: {
455
+ type: "text",
456
+ primaryKey: true,
457
+ references: {
458
+ table: "t_events",
459
+ column: "f_event_id",
460
+ onDelete: "cascade"
461
+ }
462
+ },
463
+ f_created_at: {
464
+ type: "integer",
465
+ notNull: true
466
+ },
467
+ f_provider: { type: "text" },
468
+ f_model: { type: "text" },
469
+ f_input_tokens: {
470
+ type: "integer",
471
+ notNull: true
472
+ },
473
+ f_output_tokens: {
474
+ type: "integer",
475
+ notNull: true
476
+ },
477
+ f_cache_read_tokens: {
478
+ type: "integer",
479
+ notNull: true
480
+ },
481
+ f_reasoning_tokens: {
482
+ type: "integer",
483
+ notNull: true
484
+ },
485
+ f_total_tokens: {
486
+ type: "integer",
487
+ notNull: true
488
+ }
489
+ },
490
+ indexes: { idx_event_usage_created_at: { columns: ["f_created_at"] } }
491
+ };
492
+ //#endregion
475
493
  //#region src/entities/v3/index.ts
476
494
  const sqliteTableDefs = [
477
495
  persistenceState,
@@ -483,7 +501,8 @@ const sqliteTableDefs = [
483
501
  workspaces,
484
502
  workspaceSessions,
485
503
  workspaceState,
486
- sessionProjcacheRows
504
+ sessionProjcacheRows,
505
+ eventUsage
487
506
  ];
488
507
  const postgresTableDefs = [
489
508
  persistenceState,
@@ -495,7 +514,8 @@ const postgresTableDefs = [
495
514
  workspaces,
496
515
  workspaceSessions,
497
516
  workspaceState,
498
- sessionProjcacheRows
517
+ sessionProjcacheRows,
518
+ eventUsage
499
519
  ];
500
520
  ({ ...sessionEvents$1 }), { ...sessionEvents$1.columns };
501
521
  //#endregion
@@ -514,6 +534,7 @@ const tWorkspaces = sqliteTables["t_workspaces"];
514
534
  const tWorkspaceSessions = sqliteTables["t_workspace_sessions"];
515
535
  const tWorkspaceState = sqliteTables["t_workspace_state"];
516
536
  const tSessionProjcacheRows = sqliteTables["t_session_projcache_row"];
537
+ const tEventUsage = sqliteTables["t_event_usage"];
517
538
  const DEFAULT_BUSY_TIMEOUT_MS = 5e3;
518
539
  function eventKind(event) {
519
540
  switch (event.type) {
@@ -671,27 +692,16 @@ function eventDimensions(event) {
671
692
  }
672
693
  //#endregion
673
694
  //#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
695
  async function allRows(query) {
684
696
  const q = query;
685
697
  if (typeof q.all === "function") return q.all();
686
698
  return await query;
687
699
  }
688
- /** 单行查询:SQLite 同步 `.get()`,PostgreSQL await 后取首行。 */
689
700
  async function oneRow(query) {
690
701
  const q = query;
691
702
  if (typeof q.get === "function") return q.get();
692
703
  return (await query)[0];
693
704
  }
694
- /** 写入语句:SQLite 同步 `.run()`,PostgreSQL await。 */
695
705
  async function runQuery(query) {
696
706
  const q = query;
697
707
  if (typeof q.run === "function") {
@@ -700,7 +710,6 @@ async function runQuery(query) {
700
710
  }
701
711
  await query;
702
712
  }
703
- /** 一行会话元数据 → 上游 `CheckpointIdentity`。 */
704
713
  function identityOfSession(row) {
705
714
  return {
706
715
  formatVersion: row.fVersion,
@@ -710,11 +719,9 @@ function identityOfSession(row) {
710
719
  ...row.fSeedLength === null ? {} : { inheritedEventCount: SessionLogOffset(row.fSeedLength) }
711
720
  };
712
721
  }
713
- /** 存储的整数水位 → 上游 seq cursor(-1 是空日志哨兵)。 */
714
722
  function seqCursor(seq) {
715
723
  return seq === -1 ? -1 : SessionSeq(seq);
716
724
  }
717
- /** 两写标记的两列 → 上游 `pendingMutation`(未知操作按缺失处理)。 */
718
725
  function pendingMutationOf(row) {
719
726
  const operation = row.fPendingOperation;
720
727
  if (operation !== "create" && operation !== "delete") return void 0;
@@ -723,11 +730,6 @@ function pendingMutationOf(row) {
723
730
  workspaceId: row.fPendingWorkspaceId ?? ""
724
731
  };
725
732
  }
726
- /**
727
- * 构建一个介质的 storages 接管访问层。
728
- * @param host - 介质句柄、方言表对象与事务执行器。
729
- * @returns 方言无关的访问层。
730
- */
731
733
  function createStorageRepository(host) {
732
734
  const tables = host.tables;
733
735
  const tUnits = tables["t_storage_units"];
@@ -736,7 +738,6 @@ function createStorageRepository(host) {
736
738
  const tWorkspaceSessions = tables["t_workspace_sessions"];
737
739
  const tWorkspaceState = tables["t_workspace_state"];
738
740
  const tProjcacheRows = tables["t_session_projcache_row"];
739
- /** checkpoint identity 的列投影:直接复用会话行,不再单存记录头。 */
740
741
  const sessionIdentity = {
741
742
  fSessionId: tSessions.fSessionId,
742
743
  fVersion: tSessions.fVersion,
@@ -905,9 +906,9 @@ function createStorageRepository(host) {
905
906
  await runQuery(db.delete(tProjcacheRows).where(inArray(tProjcacheRows.fSessionId, stale.map((row) => row.fSessionId))));
906
907
  return stale.length;
907
908
  },
908
- ...host.dbSync === void 0 ? {} : {
909
- readProjcacheSync: (sessionId) => {
910
- const db = host.dbSync();
909
+ ...host.dbDirect === void 0 ? {} : {
910
+ readProjcacheDirect: (sessionId) => {
911
+ const db = host.dbDirect();
911
912
  const stored = db.select().from(tProjcacheRows).where(eq(tProjcacheRows.fSessionId, sessionId)).all();
912
913
  if (stored.length === 0) return void 0;
913
914
  const session = db.select(sessionIdentity).from(tSessions).where(eq(tSessions.fSessionId, sessionId)).get();
@@ -924,8 +925,8 @@ function createStorageRepository(host) {
924
925
  rows
925
926
  };
926
927
  },
927
- readSessionTitleSync: (sessionId) => {
928
- const row = host.dbSync().select({
928
+ readSessionTitleDirect: (sessionId) => {
929
+ const row = host.dbDirect().select({
929
930
  fTitle: tSessions.fTitle,
930
931
  fTitleSeq: tSessions.fTitleSeq
931
932
  }).from(tSessions).where(eq(tSessions.fSessionId, sessionId)).get();
@@ -940,7 +941,45 @@ function createStorageRepository(host) {
940
941
  }
941
942
  //#endregion
942
943
  //#region src/sqlite.ts
943
- /** drizzle-kit 生成的迁移目录(随包根 drizzle/ 发布;src/dist 形态经相对 URL 统一解析)。 */
944
+ /**
945
+ * 一次性回填:旧库的用量从未写过 `t_event_usage`,建表后按事件行补一次(幂等:表非空即跳过)。
946
+ */
947
+ function backfillEventUsage(db) {
948
+ if (db.prepare("SELECT count(*) AS n FROM t_event_usage").get().n > 0) return;
949
+ db.exec(`INSERT OR IGNORE INTO t_event_usage (
950
+ f_event_id, f_created_at, f_provider, f_model,
951
+ f_input_tokens, f_output_tokens, f_cache_read_tokens, f_reasoning_tokens, f_total_tokens)
952
+ SELECT e.f_event_id, e.f_created_at,
953
+ coalesce(json_extract(e.f_data, '$.data.message.source.provider'),
954
+ json_extract(e.f_data, '$.message.source.provider')),
955
+ coalesce(json_extract(e.f_data, '$.data.message.source.model'),
956
+ json_extract(e.f_data, '$.message.source.model')),
957
+ coalesce(json_extract(e.f_data, '$.data.usage.inputTokens'),
958
+ json_extract(e.f_data, '$.usage.inputTokens'), 0),
959
+ coalesce(json_extract(e.f_data, '$.data.usage.outputTokens'),
960
+ json_extract(e.f_data, '$.usage.outputTokens'), 0),
961
+ coalesce(json_extract(e.f_data, '$.data.usage.cacheReadTokens'),
962
+ json_extract(e.f_data, '$.usage.cacheReadTokens'), 0),
963
+ coalesce(json_extract(e.f_data, '$.data.usage.reasoningTokens'),
964
+ json_extract(e.f_data, '$.usage.reasoningTokens'), 0),
965
+ coalesce(json_extract(e.f_data, '$.data.usage.totalTokens'),
966
+ json_extract(e.f_data, '$.usage.totalTokens'), 0)
967
+ FROM t_events e
968
+ WHERE e.f_type = 'assistant/message'
969
+ AND (json_extract(e.f_data, '$.data.usage') IS NOT NULL
970
+ OR json_extract(e.f_data, '$.usage') IS NOT NULL)`);
971
+ }
972
+ /** SQL 的缺失求和是 NULL:没有 usage 字段的行使该字段计 0。 */
973
+ function totalsOf(row) {
974
+ return {
975
+ events: row.events,
976
+ inputTokens: row.input_tokens ?? 0,
977
+ outputTokens: row.output_tokens ?? 0,
978
+ cacheReadTokens: row.cache_read_tokens ?? 0,
979
+ reasoningTokens: row.reasoning_tokens ?? 0,
980
+ totalTokens: row.total_tokens ?? 0
981
+ };
982
+ }
944
983
  const sqliteMigrationsDir = fileURLToPath(new URL("../drizzle/sqlite/", import.meta.url));
945
984
  const sqliteTxQueues = /* @__PURE__ */ new Map();
946
985
  function enqueueSqliteTx(path, fn) {
@@ -955,14 +994,15 @@ async function createDatabaseFile(path) {
955
994
  if (error.code !== "EEXIST") throw error;
956
995
  }
957
996
  }
958
- function openDatabase(path, journalMode, busyTimeout = DEFAULT_BUSY_TIMEOUT_MS) {
997
+ async function openDatabase(path, journalMode, busyTimeout = DEFAULT_BUSY_TIMEOUT_MS) {
959
998
  const db = new DatabaseSync(path);
960
999
  try {
961
1000
  configureDatabase(db, path, journalMode, busyTimeout);
962
1001
  const dbx = drizzle({ client: db });
963
1002
  const { user_version: onDisk } = db.prepare("PRAGMA user_version").get();
964
- if (onDisk === 2 && !hasMigrationsTable(db)) baselineV2(db);
1003
+ if (onDisk === 2 && !hasMigrationsTable(db)) await baselineV2(db);
965
1004
  migrate(dbx, { migrationsFolder: sqliteMigrationsDir });
1005
+ backfillEventUsage(db);
966
1006
  dbx.insert(tPersistenceState).values({
967
1007
  fSingleton: 1,
968
1008
  fStoreId: randomUUID()
@@ -978,9 +1018,8 @@ function openDatabase(path, journalMode, busyTimeout = DEFAULT_BUSY_TIMEOUT_MS)
978
1018
  function hasMigrationsTable(db) {
979
1019
  return db.prepare("SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = '__drizzle_migrations'").get() !== void 0;
980
1020
  }
981
- /** 标记 v2 baseline 已应用(迁移表 v1 结构:id/hash/created_at/name/applied_at)。 */
982
- function baselineV2(db) {
983
- const dir = readdirSync(sqliteMigrationsDir).find((name) => name.endsWith("_v2_initial"));
1021
+ async function baselineV2(db) {
1022
+ const dir = (await readdir(sqliteMigrationsDir)).find((name) => name.endsWith("_v2_initial"));
984
1023
  if (dir === void 0) throw new Error("missing v2 baseline migration in drizzle/sqlite");
985
1024
  db.exec(`
986
1025
  CREATE TABLE __drizzle_migrations (
@@ -1013,7 +1052,6 @@ var SqliteBackend = class SqliteBackend {
1013
1052
  options;
1014
1053
  kind = "sqlite";
1015
1054
  storeIdentity;
1016
- /** storages 接管表访问层;句柄在 open() 完成后解析。 */
1017
1055
  storage;
1018
1056
  dbPath = "";
1019
1057
  db;
@@ -1029,7 +1067,7 @@ var SqliteBackend = class SqliteBackend {
1029
1067
  this.dbReady.catch(() => {});
1030
1068
  this.storage = createStorageRepository({
1031
1069
  db: () => this.dbReady,
1032
- dbSync: () => {
1070
+ dbDirect: () => {
1033
1071
  if (this.db === void 0) throw new Error("sqlite session database is not open");
1034
1072
  return this.db;
1035
1073
  },
@@ -1077,16 +1115,15 @@ var SqliteBackend = class SqliteBackend {
1077
1115
  await createDatabaseFile(actual);
1078
1116
  }
1079
1117
  await enqueueSqliteTx(actual, async () => {
1080
- this.db = drizzle({ client: openDatabase(actual, this.options.journalMode, this.options.busyTimeout) });
1118
+ this.db = drizzle({ client: await openDatabase(actual, this.options.journalMode, this.options.busyTimeout) });
1081
1119
  migrate(this.db, { migrationsFolder: sqliteMigrationsDir });
1082
1120
  });
1083
1121
  try {
1084
1122
  const row = this.db.select({ fStoreId: tPersistenceState.fStoreId }).from(tPersistenceState).where(eq(tPersistenceState.fSingleton, 1)).get();
1085
- /* v8 ignore next -- openDatabase inserts the singleton before returning. */
1086
1123
  if (row === void 0) throw new Error(`session database at "${actual}" has no store identity`);
1087
1124
  if (row.fStoreId.length === 0) throw new Error(`session database at "${actual}" has no valid store identity`);
1088
1125
  if (actual !== ":memory:") {
1089
- const identity = statSync(actual, { bigint: true });
1126
+ const identity = await stat(actual, { bigint: true });
1090
1127
  this.storeIdentity = `file:${identity.dev}:${identity.ino}:${identity.birthtimeNs}:store:${row.fStoreId}`;
1091
1128
  } else this.storeIdentity = `memory:store:${row.fStoreId}`;
1092
1129
  } catch (error) {
@@ -1104,6 +1141,12 @@ var SqliteBackend = class SqliteBackend {
1104
1141
  async getEventRows(id, fromSequence) {
1105
1142
  return (fromSequence === void 0 ? this.eventRows().where(eq(tSessionEvents.fSessionId, id)) : this.eventRows().where(and(eq(tSessionEvents.fSessionId, id), gte(tSessionEvents.fSequence, fromSequence)))).orderBy(tSessionEvents.fSequence).all();
1106
1143
  }
1144
+ async getEventTypeAt(id, sequence) {
1145
+ return this.eventRows().where(and(eq(tSessionEvents.fSessionId, id), eq(tSessionEvents.fSequence, sequence))).get()?.fType;
1146
+ }
1147
+ async getEventTypesBefore(id, beforeSequence, limit) {
1148
+ return this.eventRows().where(and(eq(tSessionEvents.fSessionId, id), lt(tSessionEvents.fSequence, beforeSequence))).orderBy(desc(tSessionEvents.fSequence)).limit(limit).all();
1149
+ }
1107
1150
  async listSessions() {
1108
1151
  return this.db.select().from(tSessions).all();
1109
1152
  }
@@ -1115,7 +1158,6 @@ var SqliteBackend = class SqliteBackend {
1115
1158
  this.db.$client.exec("COMMIT");
1116
1159
  return result;
1117
1160
  } catch (error) {
1118
- /* v8 ignore start */
1119
1161
  try {
1120
1162
  this.db.$client.exec("ROLLBACK");
1121
1163
  } catch {}
@@ -1134,7 +1176,9 @@ var SqliteBackend = class SqliteBackend {
1134
1176
  bumpRevision: (id) => this.bumpRevision(id),
1135
1177
  refreshTitle: (id) => this.refreshTitle(id),
1136
1178
  deleteBridgeTail: (id, fromSequence) => this.deleteBridgeTail(id, fromSequence),
1137
- getPrevBridge: (id, sequence) => this.getPrevBridge(id, sequence)
1179
+ getPrevBridge: (id, sequence) => this.getPrevBridge(id, sequence),
1180
+ deleteSession: (id) => this.deleteSession(id),
1181
+ deleteSessions: (ids) => this.deleteSessions(ids)
1138
1182
  };
1139
1183
  async upsertSession(storage, incarnation) {
1140
1184
  this.db.insert(tSessions).values(sessionInsertRow(storage, incarnation)).onConflictDoUpdate({
@@ -1147,13 +1191,11 @@ var SqliteBackend = class SqliteBackend {
1147
1191
  fHeadEventId: tSessions.fHeadEventId,
1148
1192
  fHeadSequence: tSessions.fHeadSequence
1149
1193
  }).from(tSessions).where(eq(tSessions.fSessionId, id)).get();
1150
- /* v8 ignore next -- appendBatch/commitRepair always materialize the row before reading the head */
1151
1194
  if (head === void 0) throw new Error(`session "${id}" has no materialized row`);
1152
1195
  return head;
1153
1196
  }
1154
1197
  async getSeedLength(id) {
1155
1198
  const row = this.db.select({ fSeedLength: tSessions.fSeedLength }).from(tSessions).where(eq(tSessions.fSessionId, id)).get();
1156
- /* v8 ignore next -- rewind always materializes the row before reading the seed length */
1157
1199
  if (row === void 0) throw new Error(`session "${id}" has no materialized row`);
1158
1200
  return row.fSeedLength;
1159
1201
  }
@@ -1164,6 +1206,11 @@ var SqliteBackend = class SqliteBackend {
1164
1206
  async insertEvents(events) {
1165
1207
  if (events.length === 0) return;
1166
1208
  for (let i = 0; i < events.length; i += SqliteBackend.INSERT_BATCH_ROWS) this.db.insert(tEvents).values(events.slice(i, i + SqliteBackend.INSERT_BATCH_ROWS).map((event) => ({ ...event }))).run();
1209
+ const usage = events.flatMap((event) => {
1210
+ const row = usageRowOf(event);
1211
+ return row === void 0 ? [] : [row];
1212
+ });
1213
+ if (usage.length > 0) this.db.insert(tEventUsage).values(usage).onConflictDoNothing().run();
1167
1214
  }
1168
1215
  async insertBridges(rows) {
1169
1216
  if (rows.length === 0) return;
@@ -1178,7 +1225,6 @@ var SqliteBackend = class SqliteBackend {
1178
1225
  async bumpRevision(id) {
1179
1226
  this.db.update(tSessions).set({ fRevision: sql`${tSessions.fRevision} + 1` }).where(eq(tSessions.fSessionId, id)).run();
1180
1227
  }
1181
- /** 从事件表重算标题(最后一条 `session/title`),写回会话行的 f_title 列。 */
1182
1228
  async refreshTitle(id) {
1183
1229
  const row = this.db.select({
1184
1230
  fSequence: tSessionEvents.fSequence,
@@ -1199,6 +1245,90 @@ var SqliteBackend = class SqliteBackend {
1199
1245
  fSequence: tSessionEvents.fSequence
1200
1246
  }).from(tSessionEvents).where(and(eq(tSessionEvents.fSessionId, id), eq(tSessionEvents.fSequence, sequence))).get();
1201
1247
  }
1248
+ async deleteSession(id) {
1249
+ this.db.delete(tSessionEvents).where(eq(tSessionEvents.fSessionId, id)).run();
1250
+ this.db.delete(tWorkspaceSessions).where(eq(tWorkspaceSessions.fSessionId, id)).run();
1251
+ this.db.delete(tSessionProjcacheRows).where(eq(tSessionProjcacheRows.fSessionId, id)).run();
1252
+ this.db.delete(tSessions).where(eq(tSessions.fSessionId, id)).run();
1253
+ }
1254
+ /** 事件行可能被多个会话共享(fork 派生),所以孤儿只能在全库范围内判定。 */
1255
+ async collectOrphans() {
1256
+ const referenced = this.db.select({ fEventId: tSessionEvents.fEventId }).from(tSessionEvents);
1257
+ const info = this.db.delete(tEvents).where(notInArray(tEvents.fEventId, referenced)).run();
1258
+ this.db.delete(tEventUsage).where(notInArray(tEventUsage.fEventId, this.db.select({ fEventId: tEvents.fEventId }).from(tEvents))).run();
1259
+ return Number(info.changes);
1260
+ }
1261
+ /** 父会话被删后留下的 subagent 会话:父已不在表里,或本来就没有父。 */
1262
+ async listOrphanSubagentSessions() {
1263
+ return this.db.$client.prepare(`SELECT f_session_id AS id FROM t_sessions
1264
+ WHERE f_origin = 'subagent'
1265
+ AND (f_parent_session IS NULL
1266
+ OR f_parent_session NOT IN (SELECT f_session_id FROM t_sessions))`).all().map((row) => row.id);
1267
+ }
1268
+ async deleteSessions(ids) {
1269
+ if (ids.length === 0) return 0;
1270
+ this.db.delete(tSessionEvents).where(inArray(tSessionEvents.fSessionId, ids)).run();
1271
+ this.db.delete(tWorkspaceSessions).where(inArray(tWorkspaceSessions.fSessionId, ids)).run();
1272
+ this.db.delete(tSessionProjcacheRows).where(inArray(tSessionProjcacheRows.fSessionId, ids)).run();
1273
+ const info = this.db.delete(tSessions).where(inArray(tSessions.fSessionId, ids)).run();
1274
+ return Number(info.changes);
1275
+ }
1276
+ /** 用量聚合:只读 `t_event_usage`;可选只算 `sinceMs` 之后的事件行。 */
1277
+ async usageReport(sinceMs) {
1278
+ const sinceClause = sinceMs === void 0 ? "" : " AND u.f_created_at >= ?";
1279
+ const sinceParams = sinceMs === void 0 ? [] : [sinceMs];
1280
+ const buckets = this.db.$client.prepare(`SELECT date(u.f_created_at / 1000, 'unixepoch', 'localtime') AS day,
1281
+ u.f_provider AS provider,
1282
+ u.f_model AS model,
1283
+ EXISTS (SELECT 1 FROM t_session_events sb
1284
+ JOIN t_sessions ss ON ss.f_session_id = sb.f_session_id
1285
+ WHERE sb.f_event_id = u.f_event_id AND ss.f_origin = 'subagent') AS subagent,
1286
+ count(*) AS events,
1287
+ sum(u.f_input_tokens) AS input_tokens,
1288
+ sum(u.f_output_tokens) AS output_tokens,
1289
+ sum(u.f_cache_read_tokens) AS cache_read_tokens,
1290
+ sum(u.f_reasoning_tokens) AS reasoning_tokens,
1291
+ sum(u.f_total_tokens) AS total_tokens
1292
+ FROM t_event_usage u
1293
+ WHERE EXISTS (SELECT 1 FROM t_session_events rb WHERE rb.f_event_id = u.f_event_id)${sinceClause}
1294
+ GROUP BY day, provider, model, subagent`).all(...sinceParams);
1295
+ const sessions = this.db.$client.prepare(`SELECT b.f_session_id AS session_id,
1296
+ s.f_title AS title,
1297
+ (s.f_origin = 'subagent') AS subagent,
1298
+ (s.f_archived_at IS NOT NULL) AS archived,
1299
+ count(*) AS events,
1300
+ sum(u.f_input_tokens) AS input_tokens,
1301
+ sum(u.f_output_tokens) AS output_tokens,
1302
+ sum(u.f_cache_read_tokens) AS cache_read_tokens,
1303
+ sum(u.f_reasoning_tokens) AS reasoning_tokens,
1304
+ sum(u.f_total_tokens) AS total_tokens
1305
+ FROM t_session_events b
1306
+ JOIN t_event_usage u ON u.f_event_id = b.f_event_id
1307
+ JOIN t_sessions s ON s.f_session_id = b.f_session_id
1308
+ WHERE 1 = 1${sinceClause}
1309
+ GROUP BY b.f_session_id, s.f_title, s.f_origin, s.f_archived_at`).all(...sinceParams);
1310
+ return {
1311
+ buckets: buckets.map((row) => ({
1312
+ day: row.day,
1313
+ provider: row.provider,
1314
+ model: row.model,
1315
+ subagent: row.subagent === 1,
1316
+ ...totalsOf(row)
1317
+ })),
1318
+ sessions: sessions.map((row) => ({
1319
+ sessionId: row.session_id,
1320
+ title: row.title,
1321
+ subagent: row.subagent === 1,
1322
+ archived: row.archived === 1,
1323
+ ...totalsOf(row)
1324
+ }))
1325
+ };
1326
+ }
1327
+ async vacuum() {
1328
+ await enqueueSqliteTx(this.dbPath, async () => {
1329
+ this.db.$client.exec("VACUUM");
1330
+ });
1331
+ }
1202
1332
  eventRows() {
1203
1333
  return this.db.select({
1204
1334
  fEventId: tSessionEvents.fEventId,
@@ -1215,4 +1345,4 @@ var SqliteBackend = class SqliteBackend {
1215
1345
  }
1216
1346
  };
1217
1347
  //#endregion
1218
- 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 };
1348
+ export { WriteGuard as C, toPostgresSchema as S, tStorageUnits as _, EVENT_ENCODING as a, tWorkspaces as b, eventDimensions as c, tEvents as d, tPersistenceState as f, tSessions as g, tSessionProjcacheRows as h, DEFAULT_BUSY_TIMEOUT_MS as i, eventKind as l, tSessionEvents as m, openDatabase as n, SCHEMA_VERSION as o, tSchemaMeta as p, createStorageRepository as r, SESSION_PERSISTENCE_SQLITE_APPLICATION_ID as s, SqliteBackend as t, tEventUsage as u, tWorkspaceSessions as v, postgresTableDefs as x, tWorkspaceState as y };