@morlay/session-rdb 0.0.21-alpha.7 → 0.0.21-alpha.9

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 (45) hide show
  1. package/README.md +31 -12
  2. package/dist/artifact.d.mts +2 -101
  3. package/dist/artifact.mjs +2 -2
  4. package/dist/{branch-BjUR6wfe.mjs → branch-D4nVzQQM.mjs} +3 -3
  5. package/dist/deletion.d.mts +1 -1
  6. package/dist/deletion.mjs +1 -1
  7. package/dist/{dist-vIVO6bA-.mjs → dist-DHer1cql.mjs} +14 -7
  8. package/dist/export.d.mts +8 -0
  9. package/dist/export.mjs +76 -0
  10. package/dist/gc.d.mts +8 -0
  11. package/dist/gc.mjs +51 -0
  12. package/dist/import.d.mts +1 -1
  13. package/dist/import.mjs +1 -1
  14. package/dist/index.d.mts +2 -2
  15. package/dist/index.mjs +3 -3
  16. package/dist/{log-qCNkDjh5.mjs → log-BWVFYAh5.mjs} +39 -12
  17. package/dist/{backend-E0bmTXpz.d.mts → log-CUjKQbOR.d.mts} +222 -39
  18. package/dist/{sqlite-zcXVyMxr.mjs → sqlite-SCiSnwaz.mjs} +321 -259
  19. package/dist/{src-B9X8BKyh.mjs → src-BZ1tffd5.mjs} +342 -310
  20. package/dist/storage.d.mts +20 -7
  21. package/dist/storage.mjs +2 -2
  22. package/dist/testing.d.mts +1 -1
  23. package/dist/testing.mjs +48 -29
  24. package/dist/usage.d.mts +2 -0
  25. package/dist/usage.mjs +152 -0
  26. package/drizzle/postgres/20260923120000_v3_usage_materialized/migration.sql +48 -0
  27. package/drizzle/sqlite/20260923120000_v3_usage_materialized/migration.sql +48 -0
  28. package/package.json +25 -30
  29. package/src/backend.ts +41 -17
  30. package/src/branch.ts +4 -4
  31. package/src/drizzle/postgres-v3.ts +2 -0
  32. package/src/drizzle/sqlite-v3.ts +2 -0
  33. package/src/entities/v3/event-usage.ts +8 -1
  34. package/src/entities/v3/index.ts +8 -0
  35. package/src/entities/v3/session-counts.ts +23 -0
  36. package/src/entities/v3/session-usage.ts +29 -0
  37. package/src/index.ts +230 -88
  38. package/src/log.ts +58 -17
  39. package/src/postgres.ts +248 -149
  40. package/src/rows.ts +2 -1
  41. package/src/schema.ts +4 -0
  42. package/src/sqlite.ts +263 -124
  43. package/src/storage-takeover/repository.ts +1 -4
  44. package/src/usage.ts +10 -6
  45. package/cordis.patch.yml +0 -13
@@ -1,4 +1,5 @@
1
- import { _ as sessionInsertRow, g as sessionConflictRow, x as usageRowOf, y as titleOfEventData } from "./log-qCNkDjh5.mjs";
1
+ import { COUNTED_EVENT_TYPES, addActivityTotals, addTokenTotals, addTotals, emptyTotals, localDayKey } from "./usage.mjs";
2
+ import { _ as sessionConflictRow, b as titleOfEventData, v as sessionInsertRow } from "./log-BWVFYAh5.mjs";
2
3
  import { randomUUID } from "node:crypto";
3
4
  import { SESSION_FORMAT_VERSION, SessionId, SessionLogOffset, SessionSeq } from "@deepseek-ai/dsh-session";
4
5
  import { and, desc, eq, gte, inArray, isNotNull, lt, notInArray, sql } from "drizzle-orm";
@@ -453,7 +454,9 @@ const sessionProjcacheRows = {
453
454
  //#region src/entities/v3/event-usage.ts
454
455
  /**
455
456
  * 用量日志:一条 `assistant/message` 事件一行(按事件行唯一,fork 共享行不重复)。
456
- * 统计只读这张表,不再逐行解析事件的 JSON;事件行本身仍留在 `t_events`。
457
+ * 读侧维度全部物化在行上——本地日、是否被会话引用、是否被子代理会话引用——统计因此不做
458
+ * `EXISTS` 判定、也不回连事件表;事件行本身仍留在 `t_events`。
459
+ * 取舍见 ADR-统计衍生表物化归属与汇总。
457
460
  */
458
461
  const eventUsage = {
459
462
  name: "t_event_usage",
@@ -471,8 +474,22 @@ const eventUsage = {
471
474
  type: "bigint",
472
475
  notNull: true
473
476
  },
477
+ f_day: {
478
+ type: "text",
479
+ notNull: true
480
+ },
474
481
  f_provider: { type: "text" },
475
482
  f_model: { type: "text" },
483
+ f_referenced: {
484
+ type: "integer",
485
+ notNull: true,
486
+ default: 0
487
+ },
488
+ f_subagent: {
489
+ type: "integer",
490
+ notNull: true,
491
+ default: 0
492
+ },
476
493
  f_input_tokens: {
477
494
  type: "integer",
478
495
  notNull: true,
@@ -502,6 +519,118 @@ const eventUsage = {
502
519
  indexes: { idx_event_usage_created_at: { columns: ["f_created_at"] } }
503
520
  };
504
521
  //#endregion
522
+ //#region src/entities/v3/session-usage.ts
523
+ /**
524
+ * 会话 × 本地日 × 模型的 token 汇总(派生统计表):**按会话的行**读它,因此保住
525
+ * 「fork 子会话含继承前缀」的口径(各会话行之和 ≥ 总量)而不回连事件表。
526
+ * 旁路累加、可按会话重建,读侧只读;取舍见 ADR-统计衍生表物化归属与汇总。
527
+ */
528
+ const sessionUsage = {
529
+ name: "t_session_usage",
530
+ columns: {
531
+ f_session_id: {
532
+ type: "text",
533
+ notNull: true,
534
+ references: {
535
+ table: "t_sessions",
536
+ column: "f_session_id",
537
+ onDelete: "cascade"
538
+ }
539
+ },
540
+ f_day: {
541
+ type: "text",
542
+ notNull: true
543
+ },
544
+ f_provider: {
545
+ type: "text",
546
+ notNull: true,
547
+ default: ""
548
+ },
549
+ f_model: {
550
+ type: "text",
551
+ notNull: true,
552
+ default: ""
553
+ },
554
+ f_input_tokens: {
555
+ type: "integer",
556
+ notNull: true,
557
+ default: 0
558
+ },
559
+ f_output_tokens: {
560
+ type: "integer",
561
+ notNull: true,
562
+ default: 0
563
+ },
564
+ f_cache_read_tokens: {
565
+ type: "integer",
566
+ notNull: true,
567
+ default: 0
568
+ },
569
+ f_reasoning_tokens: {
570
+ type: "integer",
571
+ notNull: true,
572
+ default: 0
573
+ },
574
+ f_total_tokens: {
575
+ type: "integer",
576
+ notNull: true,
577
+ default: 0
578
+ }
579
+ },
580
+ uniques: { uq_session_usage_key: { columns: [
581
+ "f_session_id",
582
+ "f_day",
583
+ "f_provider",
584
+ "f_model"
585
+ ] } }
586
+ };
587
+ //#endregion
588
+ //#region src/entities/v3/session-counts.ts
589
+ /**
590
+ * 会话 × 本地日的活动计数(派生统计表):轮次 / 步骤 / 用户输入 / 工具调用四列直接读,
591
+ * 不再存事件类型、也不在读取时折。旁路累加、可按会话重建,读侧只读;
592
+ * 取舍见 ADR-统计衍生表物化归属与汇总。
593
+ */
594
+ const sessionCounts = {
595
+ name: "t_session_counts",
596
+ columns: {
597
+ f_session_id: {
598
+ type: "text",
599
+ notNull: true,
600
+ references: {
601
+ table: "t_sessions",
602
+ column: "f_session_id",
603
+ onDelete: "cascade"
604
+ }
605
+ },
606
+ f_day: {
607
+ type: "text",
608
+ notNull: true
609
+ },
610
+ f_turns: {
611
+ type: "integer",
612
+ notNull: true,
613
+ default: 0
614
+ },
615
+ f_steps: {
616
+ type: "integer",
617
+ notNull: true,
618
+ default: 0
619
+ },
620
+ f_user_inputs: {
621
+ type: "integer",
622
+ notNull: true,
623
+ default: 0
624
+ },
625
+ f_tool_calls: {
626
+ type: "integer",
627
+ notNull: true,
628
+ default: 0
629
+ }
630
+ },
631
+ uniques: { uq_session_counts_key: { columns: ["f_session_id", "f_day"] } }
632
+ };
633
+ //#endregion
505
634
  //#region src/entities/v3/index.ts
506
635
  const sqliteTableDefs = [
507
636
  persistenceState,
@@ -514,7 +643,9 @@ const sqliteTableDefs = [
514
643
  workspaceSessions,
515
644
  workspaceState,
516
645
  sessionProjcacheRows,
517
- eventUsage
646
+ eventUsage,
647
+ sessionUsage,
648
+ sessionCounts
518
649
  ];
519
650
  const postgresTableDefs = [
520
651
  persistenceState,
@@ -527,7 +658,9 @@ const postgresTableDefs = [
527
658
  workspaceSessions,
528
659
  workspaceState,
529
660
  sessionProjcacheRows,
530
- eventUsage
661
+ eventUsage,
662
+ sessionUsage,
663
+ sessionCounts
531
664
  ];
532
665
  ({ ...sessionEvents$1 }), { ...sessionEvents$1.columns };
533
666
  //#endregion
@@ -547,6 +680,8 @@ const tWorkspaceSessions = sqliteTables["t_workspace_sessions"];
547
680
  const tWorkspaceState = sqliteTables["t_workspace_state"];
548
681
  const tSessionProjcacheRows = sqliteTables["t_session_projcache_row"];
549
682
  const tEventUsage = sqliteTables["t_event_usage"];
683
+ const tSessionUsage = sqliteTables["t_session_usage"];
684
+ const tSessionCounts = sqliteTables["t_session_counts"];
550
685
  const DEFAULT_BUSY_TIMEOUT_MS = 5e3;
551
686
  function eventKind(event) {
552
687
  switch (event.type) {
@@ -703,152 +838,6 @@ function eventDimensions(event) {
703
838
  }
704
839
  }
705
840
  //#endregion
706
- //#region src/usage.ts
707
- const SESSION_USAGE_PATH = "/api/session.usage";
708
- const DAY_MS = 864e5;
709
- const ROLLING_DAYS = {
710
- "7d": 7,
711
- "30d": 30,
712
- "90d": 90
713
- };
714
- function localDayStart(now) {
715
- const start = new Date(now);
716
- start.setHours(0, 0, 0, 0);
717
- return start;
718
- }
719
- /**
720
- * 范围起点(含),不限时为 undefined。自然日 / 自然周按 host 的本地时区算,周边界是周一 00:00。
721
- * @param range - 语义键。
722
- * @param now - 当前时刻。
723
- * @returns 起点毫秒时间戳,或 undefined。
724
- */
725
- function resolveUsageSince(range, now) {
726
- switch (range) {
727
- case "all": return;
728
- case "day": return localDayStart(now).getTime();
729
- case "week": {
730
- const start = localDayStart(now);
731
- start.setDate(start.getDate() - (start.getDay() + 6) % 7);
732
- return start.getTime();
733
- }
734
- default: return now - ROLLING_DAYS[range] * DAY_MS;
735
- }
736
- }
737
- function parseUsageRange(value) {
738
- return value === "day" || value === "week" || value === "7d" || value === "30d" || value === "90d" ? value : "all";
739
- }
740
- /** 计入活动计数的事件类型(轮次 / 步骤 / 用户输入 / 工具调用)。 */
741
- const COUNTED_EVENT_TYPES = [
742
- "turn/start",
743
- "step/start",
744
- "user/message",
745
- "tool/call"
746
- ];
747
- /** 按事件类型把计数累加到活动指标上(表里按类型存,读的时候折成四项)。 */
748
- function addActivityCount(target, type, count) {
749
- switch (type) {
750
- case "turn/start":
751
- target.turns += count;
752
- return target;
753
- case "step/start":
754
- target.steps += count;
755
- return target;
756
- case "user/message":
757
- target.userInputs += count;
758
- return target;
759
- case "tool/call":
760
- target.toolCalls += count;
761
- return target;
762
- default: return target;
763
- }
764
- }
765
- /** 毫秒时间戳 → host 本地日(`YYYY-MM-DD`,与 SQL 的 localtime 口径一致)。 */
766
- function localDayKey(ms) {
767
- const date = new Date(ms);
768
- const pad = (value) => String(value).padStart(2, "0");
769
- return `${String(date.getFullYear())}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`;
770
- }
771
- function emptyTotals() {
772
- return {
773
- turns: 0,
774
- steps: 0,
775
- userInputs: 0,
776
- toolCalls: 0,
777
- inputTokens: 0,
778
- outputTokens: 0,
779
- cacheReadTokens: 0,
780
- reasoningTokens: 0,
781
- totalTokens: 0
782
- };
783
- }
784
- function addTokenTotals(target, row) {
785
- target.inputTokens += row.inputTokens;
786
- target.outputTokens += row.outputTokens;
787
- target.cacheReadTokens += row.cacheReadTokens;
788
- target.reasoningTokens += row.reasoningTokens;
789
- target.totalTokens += row.totalTokens;
790
- return target;
791
- }
792
- function addActivityTotals(target, row) {
793
- target.turns += row.turns;
794
- target.steps += row.steps;
795
- target.userInputs += row.userInputs;
796
- target.toolCalls += row.toolCalls;
797
- return target;
798
- }
799
- /** 把一行的整套指标累加进目标(后端合并「是否子代理」两组时用)。 */
800
- function addTotals(target, row) {
801
- addActivityTotals(target, row);
802
- addTokenTotals(target, row);
803
- return target;
804
- }
805
- /**
806
- * 用量统计通道:一次请求回报总览(含 subagent 拆分)、按天 × 模型的桶与按会话的行。
807
- * 聚合只算被会话引用的事件行——fork 共享行因此只计一次,已删会话留下的孤儿行不计。
808
- */
809
- function registerSessionUsage(ctx, persistence) {
810
- ctx.inject(["webServer", "connection"], (webCtx) => {
811
- const webServer = webCtx.webServer;
812
- const connection = webCtx.get("connection");
813
- return webCtx.effect(() => webServer.register({
814
- kind: "exact",
815
- path: SESSION_USAGE_PATH,
816
- handler: async (req, res) => {
817
- const rejection = connection.requestRejection(req);
818
- if (rejection !== void 0) {
819
- res.writeHead(rejection);
820
- res.end(rejection === 401 ? "unauthorized" : "forbidden");
821
- return;
822
- }
823
- if (req.method !== "POST") {
824
- res.writeHead(405, { "content-type": "application/json" });
825
- res.end(JSON.stringify({ error: "method not allowed" }));
826
- return;
827
- }
828
- const chunks = [];
829
- for await (const chunk of req) chunks.push(chunk);
830
- const raw = Buffer.concat(chunks).toString("utf8");
831
- let envelope = {};
832
- if (raw !== "") try {
833
- envelope = JSON.parse(raw);
834
- } catch {
835
- res.writeHead(400, { "content-type": "application/json" });
836
- res.end(JSON.stringify({ error: "request body is not JSON" }));
837
- return;
838
- }
839
- try {
840
- const report = await persistence.usageReport(resolveUsageSince(parseUsageRange(envelope.range), Date.now()));
841
- res.writeHead(200, { "content-type": "application/json" });
842
- res.end(JSON.stringify(report));
843
- } catch (error) {
844
- res.writeHead(500, { "content-type": "application/json" });
845
- res.end(JSON.stringify({ error: error instanceof Error ? error.message : "usage report failed" }));
846
- }
847
- }
848
- }), `session-rdb: ${SESSION_USAGE_PATH} route`);
849
- });
850
- }
851
- //#endregion
852
841
  //#region src/storage-takeover/repository.ts
853
842
  async function allRows(query) {
854
843
  const q = query;
@@ -1107,45 +1096,44 @@ function createStorageRepository(host) {
1107
1096
  }
1108
1097
  //#endregion
1109
1098
  //#region src/sqlite.ts
1110
- /** 活动计数的派生表:可销毁重建(不在 drizzle schema / 迁移里,建表幂等)。 */
1111
- function ensureEventCountsTable(db) {
1112
- db.exec(`CREATE TABLE IF NOT EXISTS t_event_counts (
1113
- f_id INTEGER PRIMARY KEY AUTOINCREMENT,
1114
- f_session_id TEXT NOT NULL,
1115
- f_day TEXT NOT NULL,
1116
- f_type TEXT NOT NULL,
1117
- f_count INTEGER NOT NULL DEFAULT 0
1118
- );
1119
- CREATE UNIQUE INDEX IF NOT EXISTS uq_event_counts_session_day_type
1120
- ON t_event_counts(f_session_id, f_day, f_type);
1121
- CREATE INDEX IF NOT EXISTS idx_event_counts_day ON t_event_counts(f_day);`);
1122
- }
1123
- /** 一次性回填活动计数(表为空时从事件表重算;幂等)。 */
1124
- function backfillEventCounts(db) {
1125
- if (db.prepare("SELECT count(*) AS n FROM t_event_counts").get().n > 0) return;
1126
- db.exec(`INSERT OR IGNORE INTO t_event_counts (f_session_id, f_day, f_type, f_count)
1127
- SELECT b.f_session_id,
1128
- date(e.f_created_at / 1000, 'unixepoch', 'localtime'),
1129
- e.f_type,
1130
- count(*)
1131
- FROM t_session_events b
1132
- JOIN t_events e ON e.f_event_id = b.f_event_id
1133
- WHERE e.f_type IN (${COUNTED_EVENT_TYPE_SQL})
1134
- GROUP BY 1, 2, 3`);
1099
+ /** 用量行的「被引用」物化表达式(`%EVENT%` 换成事件 id 列)。 */
1100
+ const EVENT_REFERENCED_SQL = `EXISTS (SELECT 1 FROM t_session_events rb
1101
+ WHERE rb.f_event_id = %EVENT%)`;
1102
+ /** 用量行的「被 subagent 会话引用」物化表达式(`%EVENT%` 换成事件 id 列)。 */
1103
+ const EVENT_SUBAGENT_SQL = `EXISTS (SELECT 1 FROM t_session_events sb
1104
+ JOIN t_sessions ss ON ss.f_session_id = sb.f_session_id
1105
+ WHERE sb.f_event_id = %EVENT% AND ss.f_origin = 'subagent')`;
1106
+ /** `IN (...)` 用的类型字面量(与 `COUNTED_EVENT_TYPES` 同源)。 */
1107
+ const COUNTED_EVENT_TYPE_SQL = COUNTED_EVENT_TYPES.map((type) => `'${type}'`).join(", ");
1108
+ function withEvent(expression, column) {
1109
+ return expression.replace("%EVENT%", column);
1135
1110
  }
1136
1111
  /**
1137
- * 一次性回填:旧库的用量从未写过 `t_event_usage`,建表后按事件行补一次(幂等:表非空即跳过)。
1112
+ * 派生统计表的回填:任一表为空即按事件表全量重算(幂等;迁移删表重建后、手动清空后都靠它自愈)。
1113
+ * 顺序有依赖:用量行(带物化列)先落,两张会话汇总表再从它 / 事件表重算。
1138
1114
  */
1115
+ function backfillUsageTables(db) {
1116
+ backfillEventUsage(db);
1117
+ backfillSessionUsage(db);
1118
+ backfillSessionCounts(db);
1119
+ }
1120
+ function tableIsEmpty(db, table) {
1121
+ return db.prepare(`SELECT count(*) AS n FROM ${table}`).get().n === 0;
1122
+ }
1123
+ /** 用量行:一条 `assistant/message` 事件行一行,物化本地日与两个归属标记。 */
1139
1124
  function backfillEventUsage(db) {
1140
- if (db.prepare("SELECT count(*) AS n FROM t_event_usage").get().n > 0) return;
1125
+ if (!tableIsEmpty(db, "t_event_usage")) return;
1141
1126
  db.exec(`INSERT OR IGNORE INTO t_event_usage (
1142
- f_event_id, f_created_at, f_provider, f_model,
1127
+ f_event_id, f_created_at, f_day, f_provider, f_model, f_referenced, f_subagent,
1143
1128
  f_input_tokens, f_output_tokens, f_cache_read_tokens, f_reasoning_tokens, f_total_tokens)
1144
1129
  SELECT e.f_event_id, e.f_created_at,
1130
+ date(e.f_created_at / 1000, 'unixepoch', 'localtime'),
1145
1131
  coalesce(json_extract(e.f_data, '$.data.message.source.provider'),
1146
1132
  json_extract(e.f_data, '$.message.source.provider')),
1147
1133
  coalesce(json_extract(e.f_data, '$.data.message.source.model'),
1148
1134
  json_extract(e.f_data, '$.message.source.model')),
1135
+ ${withEvent(EVENT_REFERENCED_SQL, "e.f_event_id")},
1136
+ ${withEvent(EVENT_SUBAGENT_SQL, "e.f_event_id")},
1149
1137
  coalesce(json_extract(e.f_data, '$.data.usage.inputTokens'),
1150
1138
  json_extract(e.f_data, '$.usage.inputTokens'), 0),
1151
1139
  coalesce(json_extract(e.f_data, '$.data.usage.outputTokens'),
@@ -1161,6 +1149,35 @@ function backfillEventUsage(db) {
1161
1149
  AND (json_extract(e.f_data, '$.data.usage') IS NOT NULL
1162
1150
  OR json_extract(e.f_data, '$.usage') IS NOT NULL)`);
1163
1151
  }
1152
+ /** 会话 × 本地日 × 模型的 token 汇总:从桥接行 × 用量行重算(含 fork 继承前缀)。 */
1153
+ function backfillSessionUsage(db) {
1154
+ if (!tableIsEmpty(db, "t_session_usage")) return;
1155
+ db.exec(`INSERT OR IGNORE INTO t_session_usage (
1156
+ f_session_id, f_day, f_provider, f_model,
1157
+ f_input_tokens, f_output_tokens, f_cache_read_tokens, f_reasoning_tokens, f_total_tokens)
1158
+ SELECT b.f_session_id, u.f_day, coalesce(u.f_provider, ''), coalesce(u.f_model, ''),
1159
+ sum(u.f_input_tokens), sum(u.f_output_tokens), sum(u.f_cache_read_tokens),
1160
+ sum(u.f_reasoning_tokens), sum(u.f_total_tokens)
1161
+ FROM t_session_events b
1162
+ JOIN t_event_usage u ON u.f_event_id = b.f_event_id
1163
+ GROUP BY b.f_session_id, u.f_day, coalesce(u.f_provider, ''), coalesce(u.f_model, '')`);
1164
+ }
1165
+ /** 会话 × 本地日的活动计数:从桥接行 × 事件类型重算。 */
1166
+ function backfillSessionCounts(db) {
1167
+ if (!tableIsEmpty(db, "t_session_counts")) return;
1168
+ db.exec(`INSERT OR IGNORE INTO t_session_counts (
1169
+ f_session_id, f_day, f_turns, f_steps, f_user_inputs, f_tool_calls)
1170
+ SELECT b.f_session_id,
1171
+ date(e.f_created_at / 1000, 'unixepoch', 'localtime'),
1172
+ sum(CASE WHEN e.f_type = 'turn/start' THEN 1 ELSE 0 END),
1173
+ sum(CASE WHEN e.f_type = 'step/start' THEN 1 ELSE 0 END),
1174
+ sum(CASE WHEN e.f_type = 'user/message' THEN 1 ELSE 0 END),
1175
+ sum(CASE WHEN e.f_type = 'tool/call' THEN 1 ELSE 0 END)
1176
+ FROM t_session_events b
1177
+ JOIN t_events e ON e.f_event_id = b.f_event_id
1178
+ WHERE e.f_type IN (${COUNTED_EVENT_TYPE_SQL})
1179
+ GROUP BY b.f_session_id, 2`);
1180
+ }
1164
1181
  function sessionListRowOf(row) {
1165
1182
  return {
1166
1183
  sessionId: typeof row["session_id"] === "string" ? row["session_id"] : String(row["session_id"]),
@@ -1184,6 +1201,15 @@ function tokenTotalsOf(row) {
1184
1201
  totalTokens: row.total_tokens ?? 0
1185
1202
  };
1186
1203
  }
1204
+ /** `t_session_counts` 的原始行(列名是 SQL 别名)折成四项活动计数。 */
1205
+ function activityTotalsOf(row) {
1206
+ return {
1207
+ turns: row.turns ?? 0,
1208
+ steps: row.steps ?? 0,
1209
+ userInputs: row.user_inputs ?? 0,
1210
+ toolCalls: row.tool_calls ?? 0
1211
+ };
1212
+ }
1187
1213
  /** 一行的整套指标:token 用量 + 活动计数(轮次 / 步骤 / 用户输入 / 工具调用)。 */
1188
1214
  function emptyActivity() {
1189
1215
  return {
@@ -1193,17 +1219,10 @@ function emptyActivity() {
1193
1219
  toolCalls: 0
1194
1220
  };
1195
1221
  }
1196
- /** 有 token 用量的会话:活动计数与列表都限定在这一批会话里,口径一致。 */
1222
+ /** 有 token 用量的会话:活动计数与列表都限定在这一批会话里,口径一致(读汇总表,几百行)。 */
1197
1223
  function hasUsageSession(column) {
1198
- return `EXISTS (SELECT 1 FROM t_session_events ub
1199
- JOIN t_event_usage uu ON uu.f_event_id = ub.f_event_id
1200
- WHERE ub.f_session_id = ${column})`;
1224
+ return `EXISTS (SELECT 1 FROM t_session_usage ub WHERE ub.f_session_id = ${column})`;
1201
1225
  }
1202
- const SUBAGENT_OF_EVENT = `EXISTS (SELECT 1 FROM t_session_events sb
1203
- JOIN t_sessions ss ON ss.f_session_id = sb.f_session_id
1204
- WHERE sb.f_event_id = %EVENT% AND ss.f_origin = 'subagent')`;
1205
- /** `IN (...)` 用的类型字面量(与 `COUNTED_EVENT_TYPES` 同源)。 */
1206
- const COUNTED_EVENT_TYPE_SQL = COUNTED_EVENT_TYPES.map((type) => `'${type}'`).join(", ");
1207
1226
  const sqliteMigrationsDir = fileURLToPath(new URL("../drizzle/sqlite/", import.meta.url));
1208
1227
  const sqliteTxQueues = /* @__PURE__ */ new Map();
1209
1228
  function enqueueSqliteTx(path, fn) {
@@ -1226,9 +1245,7 @@ async function openDatabase(path, journalMode, busyTimeout = DEFAULT_BUSY_TIMEOU
1226
1245
  const { user_version: onDisk } = db.prepare("PRAGMA user_version").get();
1227
1246
  if (onDisk === 2 && !hasMigrationsTable(db)) await baselineV2(db);
1228
1247
  migrate(dbx, { migrationsFolder: sqliteMigrationsDir });
1229
- ensureEventCountsTable(db);
1230
- backfillEventCounts(db);
1231
- backfillEventUsage(db);
1248
+ backfillUsageTables(db);
1232
1249
  dbx.insert(tPersistenceState).values({
1233
1250
  fSingleton: 1,
1234
1251
  fStoreId: randomUUID()
@@ -1399,7 +1416,7 @@ var SqliteBackend = class SqliteBackend {
1399
1416
  getHead: (id) => this.getHead(id),
1400
1417
  getSeedLength: (id) => this.getSeedLength(id),
1401
1418
  updateSeedLength: (id, seedLength) => this.updateSeedLength(id, seedLength),
1402
- insertEvents: (events) => this.insertEvents(events),
1419
+ insertEvents: (events, usageRows) => this.insertEvents(events, usageRows),
1403
1420
  insertBridges: (rows) => this.insertBridges(rows),
1404
1421
  updateHead: (id, headEventId, headSequence) => this.updateHead(id, headEventId, headSequence),
1405
1422
  bumpRevision: (id, lastEventAt) => this.bumpRevision(id, lastEventAt),
@@ -1432,14 +1449,10 @@ var SqliteBackend = class SqliteBackend {
1432
1449
  this.db.update(tSessions).set({ fSeedLength: seedLength }).where(eq(tSessions.fSessionId, id)).run();
1433
1450
  }
1434
1451
  static INSERT_BATCH_ROWS = 1e3;
1435
- async insertEvents(events) {
1452
+ async insertEvents(events, usageRows) {
1436
1453
  if (events.length === 0) return;
1437
1454
  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();
1438
- const usage = events.flatMap((event) => {
1439
- const row = usageRowOf(event);
1440
- return row === void 0 ? [] : [row];
1441
- });
1442
- if (usage.length > 0) this.db.insert(tEventUsage).values(usage).onConflictDoNothing().run();
1455
+ if (usageRows.length > 0) this.db.insert(tEventUsage).values(usageRows.map((row) => ({ ...row }))).onConflictDoNothing().run();
1443
1456
  }
1444
1457
  async insertBridges(rows) {
1445
1458
  if (rows.length === 0) return;
@@ -1549,41 +1562,49 @@ var SqliteBackend = class SqliteBackend {
1549
1562
  total: Number(counted.n)
1550
1563
  };
1551
1564
  }
1565
+ /**
1566
+ * 用量聚合:只读三张派生统计表——事件级用量行给去重口径的总量与「天 × 模型」桶,
1567
+ * `t_session_usage` / `t_session_counts` 给按会话的行(含 fork 继承前缀)。
1568
+ * 归属、本地日与「是否被引用」都是物化列,这里不再现算、也不回连事件表。
1569
+ * 事件级按毫秒时间戳过滤、汇总表按本地日过滤——`sinceMs` 对齐到本地零点时两者等价。
1570
+ */
1552
1571
  async usageReport(sinceMs) {
1553
- const usageSince = sinceMs === void 0 ? "" : " AND u.f_created_at >= ?";
1572
+ const usageSince = sinceMs === void 0 ? "" : " AND f_created_at >= ?";
1554
1573
  const params = sinceMs === void 0 ? [] : [sinceMs];
1555
- const daySince = sinceMs === void 0 ? "" : " AND c.f_day >= ?";
1574
+ const daySince = sinceMs === void 0 ? "" : " AND %COL% >= ?";
1556
1575
  const dayParams = sinceMs === void 0 ? [] : [localDayKey(sinceMs)];
1557
- const subagentOfUsage = SUBAGENT_OF_EVENT.replace("%EVENT%", "u.f_event_id");
1558
- const buckets = this.db.$client.prepare(`SELECT date(u.f_created_at / 1000, 'unixepoch', 'localtime') AS day,
1559
- u.f_provider AS provider,
1560
- u.f_model AS model,
1561
- ${subagentOfUsage} AS subagent,
1562
- sum(u.f_input_tokens) AS input_tokens,
1563
- sum(u.f_output_tokens) AS output_tokens,
1564
- sum(u.f_cache_read_tokens) AS cache_read_tokens,
1565
- sum(u.f_reasoning_tokens) AS reasoning_tokens,
1566
- sum(u.f_total_tokens) AS total_tokens
1567
- FROM t_event_usage u
1568
- WHERE EXISTS (SELECT 1 FROM t_session_events rb WHERE rb.f_event_id = u.f_event_id)${usageSince}
1569
- GROUP BY day, provider, model, subagent`).all(...params);
1570
- const tokenScope = this.db.$client.prepare(`SELECT ${subagentOfUsage} AS subagent,
1571
- sum(u.f_input_tokens) AS input_tokens,
1572
- sum(u.f_output_tokens) AS output_tokens,
1573
- sum(u.f_cache_read_tokens) AS cache_read_tokens,
1574
- sum(u.f_reasoning_tokens) AS reasoning_tokens,
1575
- sum(u.f_total_tokens) AS total_tokens
1576
- FROM t_event_usage u
1577
- WHERE EXISTS (SELECT 1 FROM t_session_events rb WHERE rb.f_event_id = u.f_event_id)${usageSince}
1578
- GROUP BY subagent`).all(...params);
1576
+ const dayFilter = (column) => daySince.replace("%COL%", column);
1577
+ const buckets = this.db.$client.prepare(`SELECT f_day AS day,
1578
+ f_provider AS provider,
1579
+ f_model AS model,
1580
+ f_subagent AS subagent,
1581
+ sum(f_input_tokens) AS input_tokens,
1582
+ sum(f_output_tokens) AS output_tokens,
1583
+ sum(f_cache_read_tokens) AS cache_read_tokens,
1584
+ sum(f_reasoning_tokens) AS reasoning_tokens,
1585
+ sum(f_total_tokens) AS total_tokens
1586
+ FROM t_event_usage
1587
+ WHERE f_referenced = 1${usageSince}
1588
+ GROUP BY f_day, f_provider, f_model, f_subagent`).all(...params);
1589
+ const tokenScope = this.db.$client.prepare(`SELECT f_subagent AS subagent,
1590
+ sum(f_input_tokens) AS input_tokens,
1591
+ sum(f_output_tokens) AS output_tokens,
1592
+ sum(f_cache_read_tokens) AS cache_read_tokens,
1593
+ sum(f_reasoning_tokens) AS reasoning_tokens,
1594
+ sum(f_total_tokens) AS total_tokens
1595
+ FROM t_event_usage
1596
+ WHERE f_referenced = 1${usageSince}
1597
+ GROUP BY f_subagent`).all(...params);
1579
1598
  const activityScope = this.db.$client.prepare(`SELECT (s.f_origin = 'subagent') AS subagent,
1580
- c.f_type AS f_type,
1581
- sum(c.f_count) AS n
1582
- FROM t_event_counts c
1599
+ sum(c.f_turns) AS turns,
1600
+ sum(c.f_steps) AS steps,
1601
+ sum(c.f_user_inputs) AS user_inputs,
1602
+ sum(c.f_tool_calls) AS tool_calls
1603
+ FROM t_session_counts c
1583
1604
  JOIN t_sessions s ON s.f_session_id = c.f_session_id
1584
- WHERE ${hasUsageSession("c.f_session_id")}${daySince}
1585
- GROUP BY subagent, c.f_type`).all(...dayParams);
1586
- const tokenSessions = this.db.$client.prepare(`SELECT b.f_session_id AS session_id,
1605
+ WHERE ${hasUsageSession("c.f_session_id")}${dayFilter("c.f_day")}
1606
+ GROUP BY subagent`).all(...dayParams);
1607
+ const tokenSessions = this.db.$client.prepare(`SELECT u.f_session_id AS session_id,
1587
1608
  s.f_title AS title,
1588
1609
  (s.f_origin = 'subagent') AS subagent,
1589
1610
  (s.f_archived_at IS NOT NULL) AS archived,
@@ -1592,28 +1613,25 @@ var SqliteBackend = class SqliteBackend {
1592
1613
  sum(u.f_cache_read_tokens) AS cache_read_tokens,
1593
1614
  sum(u.f_reasoning_tokens) AS reasoning_tokens,
1594
1615
  sum(u.f_total_tokens) AS total_tokens
1595
- FROM t_session_events b
1596
- JOIN t_event_usage u ON u.f_event_id = b.f_event_id
1597
- JOIN t_sessions s ON s.f_session_id = b.f_session_id
1598
- WHERE 1 = 1${usageSince}
1599
- GROUP BY b.f_session_id, s.f_title, s.f_origin, s.f_archived_at`).all(...params);
1600
- const activitySessions = this.db.$client.prepare(`SELECT c.f_session_id AS session_id,
1601
- c.f_type AS f_type,
1602
- sum(c.f_count) AS n
1603
- FROM t_event_counts c
1604
- WHERE ${hasUsageSession("c.f_session_id")}${daySince}
1605
- GROUP BY c.f_session_id, c.f_type`).all(...dayParams);
1616
+ FROM t_session_usage u
1617
+ JOIN t_sessions s ON s.f_session_id = u.f_session_id
1618
+ WHERE 1 = 1${dayFilter("u.f_day")}
1619
+ GROUP BY u.f_session_id, s.f_title, s.f_origin, s.f_archived_at`).all(...dayParams);
1620
+ const activitySessions = this.db.$client.prepare(`SELECT f_session_id AS session_id,
1621
+ sum(f_turns) AS turns,
1622
+ sum(f_steps) AS steps,
1623
+ sum(f_user_inputs) AS user_inputs,
1624
+ sum(f_tool_calls) AS tool_calls
1625
+ FROM t_session_counts
1626
+ WHERE 1 = 1${dayFilter("f_day")}
1627
+ GROUP BY f_session_id`).all(...dayParams);
1606
1628
  const human = emptyTotals();
1607
1629
  const subagent = emptyTotals();
1608
1630
  for (const row of tokenScope) addTokenTotals(row.subagent === 1 ? subagent : human, tokenTotalsOf(row));
1609
- for (const row of activityScope) addActivityCount(row.subagent === 1 ? subagent : human, row.f_type, row.n ?? 0);
1631
+ for (const row of activityScope) addActivityTotals(row.subagent === 1 ? subagent : human, activityTotalsOf(row));
1610
1632
  const totals = addTotals(addTotals(emptyTotals(), human), subagent);
1611
1633
  const activityBySession = /* @__PURE__ */ new Map();
1612
- for (const row of activitySessions) {
1613
- const target = activityBySession.get(row.session_id) ?? emptyActivity();
1614
- addActivityCount(target, row.f_type, row.n ?? 0);
1615
- activityBySession.set(row.session_id, target);
1616
- }
1634
+ for (const row of activitySessions) activityBySession.set(row.session_id, activityTotalsOf(row));
1617
1635
  return {
1618
1636
  totals,
1619
1637
  subagent,
@@ -1635,28 +1653,72 @@ var SqliteBackend = class SqliteBackend {
1635
1653
  }))
1636
1654
  };
1637
1655
  }
1638
- /** 活动计数旁路累加:派生表 `t_event_counts`,不在写事务里(失败可丢,表可销毁重建)。 */
1639
- async incrementEventCounts(id, buckets) {
1656
+ /** token 用量旁路累加:派生表 `t_session_usage`,不在写事务里(失败可丢,表可销毁重建)。 */
1657
+ async incrementSessionUsage(id, buckets) {
1658
+ if (buckets.length === 0) return;
1659
+ const statement = this.db.$client.prepare(`INSERT INTO t_session_usage (f_session_id, f_day, f_provider, f_model,
1660
+ f_input_tokens, f_output_tokens, f_cache_read_tokens, f_reasoning_tokens, f_total_tokens)
1661
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
1662
+ ON CONFLICT(f_session_id, f_day, f_provider, f_model) DO UPDATE SET
1663
+ f_input_tokens = f_input_tokens + excluded.f_input_tokens,
1664
+ f_output_tokens = f_output_tokens + excluded.f_output_tokens,
1665
+ f_cache_read_tokens = f_cache_read_tokens + excluded.f_cache_read_tokens,
1666
+ f_reasoning_tokens = f_reasoning_tokens + excluded.f_reasoning_tokens,
1667
+ f_total_tokens = f_total_tokens + excluded.f_total_tokens`);
1668
+ for (const bucket of buckets) statement.run(id, bucket.day, bucket.provider ?? "", bucket.model ?? "", bucket.inputTokens, bucket.outputTokens, bucket.cacheReadTokens, bucket.reasoningTokens, bucket.totalTokens);
1669
+ }
1670
+ /** 活动计数旁路累加:派生表 `t_session_counts`,同上(四项计数一次落)。 */
1671
+ async incrementSessionCounts(id, buckets) {
1640
1672
  if (buckets.length === 0) return;
1641
- const statement = this.db.$client.prepare(`INSERT INTO t_event_counts (f_session_id, f_day, f_type, f_count) VALUES (?, ?, ?, ?)
1642
- ON CONFLICT(f_session_id, f_day, f_type) DO UPDATE SET f_count = f_count + excluded.f_count`);
1643
- for (const bucket of buckets) statement.run(id, bucket.day, bucket.type, bucket.count);
1673
+ const statement = this.db.$client.prepare(`INSERT INTO t_session_counts (f_session_id, f_day, f_turns, f_steps, f_user_inputs, f_tool_calls)
1674
+ VALUES (?, ?, ?, ?, ?, ?)
1675
+ ON CONFLICT(f_session_id, f_day) DO UPDATE SET
1676
+ f_turns = f_turns + excluded.f_turns,
1677
+ f_steps = f_steps + excluded.f_steps,
1678
+ f_user_inputs = f_user_inputs + excluded.f_user_inputs,
1679
+ f_tool_calls = f_tool_calls + excluded.f_tool_calls`);
1680
+ for (const bucket of buckets) statement.run(id, bucket.day, bucket.turns, bucket.steps, bucket.userInputs, bucket.toolCalls);
1644
1681
  }
1645
- /** 按会话重算活动计数(rewind / fork 之后):先删该会话的行,再从事件表重算。 */
1646
- async rebuildEventCounts(id) {
1647
- this.db.$client.prepare("DELETE FROM t_event_counts WHERE f_session_id = ?").run(id);
1648
- this.db.$client.prepare(`INSERT INTO t_event_counts (f_session_id, f_day, f_type, f_count)
1682
+ /** 按会话重算两张汇总表(rewind / fork 之后):先删该会话的行,再从事件表重算。 */
1683
+ async rebuildSessionStats(id) {
1684
+ this.db.$client.prepare("DELETE FROM t_session_usage WHERE f_session_id = ?").run(id);
1685
+ this.db.$client.prepare(`INSERT INTO t_session_usage (
1686
+ f_session_id, f_day, f_provider, f_model,
1687
+ f_input_tokens, f_output_tokens, f_cache_read_tokens, f_reasoning_tokens, f_total_tokens)
1688
+ SELECT b.f_session_id, u.f_day, coalesce(u.f_provider, ''), coalesce(u.f_model, ''),
1689
+ sum(u.f_input_tokens), sum(u.f_output_tokens), sum(u.f_cache_read_tokens),
1690
+ sum(u.f_reasoning_tokens), sum(u.f_total_tokens)
1691
+ FROM t_session_events b
1692
+ JOIN t_event_usage u ON u.f_event_id = b.f_event_id
1693
+ WHERE b.f_session_id = ?
1694
+ GROUP BY b.f_session_id, u.f_day, coalesce(u.f_provider, ''), coalesce(u.f_model, '')`).run(id);
1695
+ this.db.$client.prepare("DELETE FROM t_session_counts WHERE f_session_id = ?").run(id);
1696
+ this.db.$client.prepare(`INSERT INTO t_session_counts (
1697
+ f_session_id, f_day, f_turns, f_steps, f_user_inputs, f_tool_calls)
1649
1698
  SELECT b.f_session_id,
1650
1699
  date(e.f_created_at / 1000, 'unixepoch', 'localtime'),
1651
- e.f_type,
1652
- count(*)
1700
+ sum(CASE WHEN e.f_type = 'turn/start' THEN 1 ELSE 0 END),
1701
+ sum(CASE WHEN e.f_type = 'step/start' THEN 1 ELSE 0 END),
1702
+ sum(CASE WHEN e.f_type = 'user/message' THEN 1 ELSE 0 END),
1703
+ sum(CASE WHEN e.f_type = 'tool/call' THEN 1 ELSE 0 END)
1653
1704
  FROM t_session_events b
1654
1705
  JOIN t_events e ON e.f_event_id = b.f_event_id
1655
1706
  WHERE b.f_session_id = ? AND e.f_type IN (${COUNTED_EVENT_TYPE_SQL})
1656
- GROUP BY 1, 2, 3`).run(id);
1707
+ GROUP BY b.f_session_id, 2`).run(id);
1708
+ }
1709
+ /**
1710
+ * 全量重算用量行的引用标记:rewind 截断 / fork 复用 / 会话删除都会让「引用」跨会话变化,
1711
+ * 只按本会话判定不够。低频路径,一次全表 UPDATE(3 万行量级)。
1712
+ */
1713
+ async refreshEventUsageFlags() {
1714
+ const eventId = "t_event_usage.f_event_id";
1715
+ this.db.$client.exec(`UPDATE t_event_usage SET
1716
+ f_referenced = ${withEvent(EVENT_REFERENCED_SQL, eventId)},
1717
+ f_subagent = ${withEvent(EVENT_SUBAGENT_SQL, eventId)}`);
1657
1718
  }
1658
- async deleteEventCounts(id) {
1659
- this.db.$client.prepare("DELETE FROM t_event_counts WHERE f_session_id = ?").run(id);
1719
+ async deleteSessionStats(id) {
1720
+ this.db.$client.prepare("DELETE FROM t_session_usage WHERE f_session_id = ?").run(id);
1721
+ this.db.$client.prepare("DELETE FROM t_session_counts WHERE f_session_id = ?").run(id);
1660
1722
  }
1661
1723
  async vacuum() {
1662
1724
  await enqueueSqliteTx(this.dbPath, async () => {
@@ -1679,4 +1741,4 @@ var SqliteBackend = class SqliteBackend {
1679
1741
  }
1680
1742
  };
1681
1743
  //#endregion
1682
- export { WriteGuard as A, tSessions as C, tWorkspaces as D, tWorkspaceState as E, postgresTableDefs as O, tSessionProjcacheRows as S, tWorkspaceSessions as T, tEventUsage as _, addActivityCount as a, tSchemaMeta as b, emptyTotals as c, DEFAULT_BUSY_TIMEOUT_MS as d, EVENT_ENCODING as f, eventKind as g, eventDimensions as h, COUNTED_EVENT_TYPES as i, toPostgresSchema as k, localDayKey as l, SESSION_PERSISTENCE_SQLITE_APPLICATION_ID as m, openDatabase as n, addTokenTotals as o, SCHEMA_VERSION as p, createStorageRepository as r, addTotals as s, SqliteBackend as t, registerSessionUsage as u, tEvents as v, tStorageUnits as w, tSessionEvents as x, tPersistenceState as y };
1744
+ export { postgresTableDefs as C, tWorkspaces as S, WriteGuard as T, tSessionUsage as _, EVENT_ENCODING as a, tWorkspaceSessions as b, eventDimensions as c, tEvents as d, tPersistenceState as f, tSessionProjcacheRows as g, tSessionEvents as h, DEFAULT_BUSY_TIMEOUT_MS as i, eventKind as l, tSessionCounts 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, tSessions as v, toPostgresSchema as w, tWorkspaceState as x, tStorageUnits as y };