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

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 (37) hide show
  1. package/README.md +17 -10
  2. package/dist/artifact.d.mts +2 -101
  3. package/dist/artifact.mjs +2 -2
  4. package/dist/{branch-BjUR6wfe.mjs → branch-7eOh_tbO.mjs} +3 -3
  5. package/dist/deletion.d.mts +1 -1
  6. package/dist/deletion.mjs +1 -1
  7. package/dist/import.d.mts +1 -1
  8. package/dist/import.mjs +1 -1
  9. package/dist/index.d.mts +1 -1
  10. package/dist/index.mjs +3 -3
  11. package/dist/{backend-E0bmTXpz.d.mts → log-BbleqD1M.d.mts} +171 -24
  12. package/dist/{log-qCNkDjh5.mjs → log-DTcvsMz7.mjs} +189 -12
  13. package/dist/{sqlite-zcXVyMxr.mjs → sqlite-ChM_sHAG.mjs} +320 -259
  14. package/dist/{src-B9X8BKyh.mjs → src-BWqePRA7.mjs} +290 -162
  15. package/dist/storage.d.mts +20 -7
  16. package/dist/storage.mjs +2 -2
  17. package/dist/testing.d.mts +1 -1
  18. package/dist/testing.mjs +1 -1
  19. package/drizzle/postgres/20260923120000_v3_usage_materialized/migration.sql +48 -0
  20. package/drizzle/sqlite/20260923120000_v3_usage_materialized/migration.sql +48 -0
  21. package/package.json +20 -20
  22. package/src/backend.ts +41 -17
  23. package/src/branch.ts +4 -4
  24. package/src/drizzle/postgres-v3.ts +2 -0
  25. package/src/drizzle/sqlite-v3.ts +2 -0
  26. package/src/entities/v3/event-usage.ts +8 -1
  27. package/src/entities/v3/index.ts +8 -0
  28. package/src/entities/v3/session-counts.ts +23 -0
  29. package/src/entities/v3/session-usage.ts +29 -0
  30. package/src/index.ts +109 -43
  31. package/src/log.ts +58 -17
  32. package/src/postgres.ts +248 -149
  33. package/src/rows.ts +2 -1
  34. package/src/schema.ts +4 -0
  35. package/src/sqlite.ts +263 -124
  36. package/src/storage-takeover/repository.ts +1 -4
  37. package/src/usage.ts +10 -6
@@ -1,4 +1,4 @@
1
- import { _ as sessionInsertRow, g as sessionConflictRow, x as usageRowOf, y as titleOfEventData } from "./log-qCNkDjh5.mjs";
1
+ import { C as COUNTED_EVENT_TYPES, D as addTotals, E as addTokenTotals, O as emptyTotals, T as addActivityTotals, _ as sessionConflictRow, b as titleOfEventData, k as localDayKey, v as sessionInsertRow } from "./log-DTcvsMz7.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";
@@ -453,7 +453,9 @@ const sessionProjcacheRows = {
453
453
  //#region src/entities/v3/event-usage.ts
454
454
  /**
455
455
  * 用量日志:一条 `assistant/message` 事件一行(按事件行唯一,fork 共享行不重复)。
456
- * 统计只读这张表,不再逐行解析事件的 JSON;事件行本身仍留在 `t_events`。
456
+ * 读侧维度全部物化在行上——本地日、是否被会话引用、是否被子代理会话引用——统计因此不做
457
+ * `EXISTS` 判定、也不回连事件表;事件行本身仍留在 `t_events`。
458
+ * 取舍见 ADR-统计衍生表物化归属与汇总。
457
459
  */
458
460
  const eventUsage = {
459
461
  name: "t_event_usage",
@@ -471,8 +473,22 @@ const eventUsage = {
471
473
  type: "bigint",
472
474
  notNull: true
473
475
  },
476
+ f_day: {
477
+ type: "text",
478
+ notNull: true
479
+ },
474
480
  f_provider: { type: "text" },
475
481
  f_model: { type: "text" },
482
+ f_referenced: {
483
+ type: "integer",
484
+ notNull: true,
485
+ default: 0
486
+ },
487
+ f_subagent: {
488
+ type: "integer",
489
+ notNull: true,
490
+ default: 0
491
+ },
476
492
  f_input_tokens: {
477
493
  type: "integer",
478
494
  notNull: true,
@@ -502,6 +518,118 @@ const eventUsage = {
502
518
  indexes: { idx_event_usage_created_at: { columns: ["f_created_at"] } }
503
519
  };
504
520
  //#endregion
521
+ //#region src/entities/v3/session-usage.ts
522
+ /**
523
+ * 会话 × 本地日 × 模型的 token 汇总(派生统计表):**按会话的行**读它,因此保住
524
+ * 「fork 子会话含继承前缀」的口径(各会话行之和 ≥ 总量)而不回连事件表。
525
+ * 旁路累加、可按会话重建,读侧只读;取舍见 ADR-统计衍生表物化归属与汇总。
526
+ */
527
+ const sessionUsage = {
528
+ name: "t_session_usage",
529
+ columns: {
530
+ f_session_id: {
531
+ type: "text",
532
+ notNull: true,
533
+ references: {
534
+ table: "t_sessions",
535
+ column: "f_session_id",
536
+ onDelete: "cascade"
537
+ }
538
+ },
539
+ f_day: {
540
+ type: "text",
541
+ notNull: true
542
+ },
543
+ f_provider: {
544
+ type: "text",
545
+ notNull: true,
546
+ default: ""
547
+ },
548
+ f_model: {
549
+ type: "text",
550
+ notNull: true,
551
+ default: ""
552
+ },
553
+ f_input_tokens: {
554
+ type: "integer",
555
+ notNull: true,
556
+ default: 0
557
+ },
558
+ f_output_tokens: {
559
+ type: "integer",
560
+ notNull: true,
561
+ default: 0
562
+ },
563
+ f_cache_read_tokens: {
564
+ type: "integer",
565
+ notNull: true,
566
+ default: 0
567
+ },
568
+ f_reasoning_tokens: {
569
+ type: "integer",
570
+ notNull: true,
571
+ default: 0
572
+ },
573
+ f_total_tokens: {
574
+ type: "integer",
575
+ notNull: true,
576
+ default: 0
577
+ }
578
+ },
579
+ uniques: { uq_session_usage_key: { columns: [
580
+ "f_session_id",
581
+ "f_day",
582
+ "f_provider",
583
+ "f_model"
584
+ ] } }
585
+ };
586
+ //#endregion
587
+ //#region src/entities/v3/session-counts.ts
588
+ /**
589
+ * 会话 × 本地日的活动计数(派生统计表):轮次 / 步骤 / 用户输入 / 工具调用四列直接读,
590
+ * 不再存事件类型、也不在读取时折。旁路累加、可按会话重建,读侧只读;
591
+ * 取舍见 ADR-统计衍生表物化归属与汇总。
592
+ */
593
+ const sessionCounts = {
594
+ name: "t_session_counts",
595
+ columns: {
596
+ f_session_id: {
597
+ type: "text",
598
+ notNull: true,
599
+ references: {
600
+ table: "t_sessions",
601
+ column: "f_session_id",
602
+ onDelete: "cascade"
603
+ }
604
+ },
605
+ f_day: {
606
+ type: "text",
607
+ notNull: true
608
+ },
609
+ f_turns: {
610
+ type: "integer",
611
+ notNull: true,
612
+ default: 0
613
+ },
614
+ f_steps: {
615
+ type: "integer",
616
+ notNull: true,
617
+ default: 0
618
+ },
619
+ f_user_inputs: {
620
+ type: "integer",
621
+ notNull: true,
622
+ default: 0
623
+ },
624
+ f_tool_calls: {
625
+ type: "integer",
626
+ notNull: true,
627
+ default: 0
628
+ }
629
+ },
630
+ uniques: { uq_session_counts_key: { columns: ["f_session_id", "f_day"] } }
631
+ };
632
+ //#endregion
505
633
  //#region src/entities/v3/index.ts
506
634
  const sqliteTableDefs = [
507
635
  persistenceState,
@@ -514,7 +642,9 @@ const sqliteTableDefs = [
514
642
  workspaceSessions,
515
643
  workspaceState,
516
644
  sessionProjcacheRows,
517
- eventUsage
645
+ eventUsage,
646
+ sessionUsage,
647
+ sessionCounts
518
648
  ];
519
649
  const postgresTableDefs = [
520
650
  persistenceState,
@@ -527,7 +657,9 @@ const postgresTableDefs = [
527
657
  workspaceSessions,
528
658
  workspaceState,
529
659
  sessionProjcacheRows,
530
- eventUsage
660
+ eventUsage,
661
+ sessionUsage,
662
+ sessionCounts
531
663
  ];
532
664
  ({ ...sessionEvents$1 }), { ...sessionEvents$1.columns };
533
665
  //#endregion
@@ -547,6 +679,8 @@ const tWorkspaceSessions = sqliteTables["t_workspace_sessions"];
547
679
  const tWorkspaceState = sqliteTables["t_workspace_state"];
548
680
  const tSessionProjcacheRows = sqliteTables["t_session_projcache_row"];
549
681
  const tEventUsage = sqliteTables["t_event_usage"];
682
+ const tSessionUsage = sqliteTables["t_session_usage"];
683
+ const tSessionCounts = sqliteTables["t_session_counts"];
550
684
  const DEFAULT_BUSY_TIMEOUT_MS = 5e3;
551
685
  function eventKind(event) {
552
686
  switch (event.type) {
@@ -703,152 +837,6 @@ function eventDimensions(event) {
703
837
  }
704
838
  }
705
839
  //#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
840
  //#region src/storage-takeover/repository.ts
853
841
  async function allRows(query) {
854
842
  const q = query;
@@ -1107,45 +1095,44 @@ function createStorageRepository(host) {
1107
1095
  }
1108
1096
  //#endregion
1109
1097
  //#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`);
1098
+ /** 用量行的「被引用」物化表达式(`%EVENT%` 换成事件 id 列)。 */
1099
+ const EVENT_REFERENCED_SQL = `EXISTS (SELECT 1 FROM t_session_events rb
1100
+ WHERE rb.f_event_id = %EVENT%)`;
1101
+ /** 用量行的「被 subagent 会话引用」物化表达式(`%EVENT%` 换成事件 id 列)。 */
1102
+ const EVENT_SUBAGENT_SQL = `EXISTS (SELECT 1 FROM t_session_events sb
1103
+ JOIN t_sessions ss ON ss.f_session_id = sb.f_session_id
1104
+ WHERE sb.f_event_id = %EVENT% AND ss.f_origin = 'subagent')`;
1105
+ /** `IN (...)` 用的类型字面量(与 `COUNTED_EVENT_TYPES` 同源)。 */
1106
+ const COUNTED_EVENT_TYPE_SQL = COUNTED_EVENT_TYPES.map((type) => `'${type}'`).join(", ");
1107
+ function withEvent(expression, column) {
1108
+ return expression.replace("%EVENT%", column);
1135
1109
  }
1136
1110
  /**
1137
- * 一次性回填:旧库的用量从未写过 `t_event_usage`,建表后按事件行补一次(幂等:表非空即跳过)。
1111
+ * 派生统计表的回填:任一表为空即按事件表全量重算(幂等;迁移删表重建后、手动清空后都靠它自愈)。
1112
+ * 顺序有依赖:用量行(带物化列)先落,两张会话汇总表再从它 / 事件表重算。
1138
1113
  */
1114
+ function backfillUsageTables(db) {
1115
+ backfillEventUsage(db);
1116
+ backfillSessionUsage(db);
1117
+ backfillSessionCounts(db);
1118
+ }
1119
+ function tableIsEmpty(db, table) {
1120
+ return db.prepare(`SELECT count(*) AS n FROM ${table}`).get().n === 0;
1121
+ }
1122
+ /** 用量行:一条 `assistant/message` 事件行一行,物化本地日与两个归属标记。 */
1139
1123
  function backfillEventUsage(db) {
1140
- if (db.prepare("SELECT count(*) AS n FROM t_event_usage").get().n > 0) return;
1124
+ if (!tableIsEmpty(db, "t_event_usage")) return;
1141
1125
  db.exec(`INSERT OR IGNORE INTO t_event_usage (
1142
- f_event_id, f_created_at, f_provider, f_model,
1126
+ f_event_id, f_created_at, f_day, f_provider, f_model, f_referenced, f_subagent,
1143
1127
  f_input_tokens, f_output_tokens, f_cache_read_tokens, f_reasoning_tokens, f_total_tokens)
1144
1128
  SELECT e.f_event_id, e.f_created_at,
1129
+ date(e.f_created_at / 1000, 'unixepoch', 'localtime'),
1145
1130
  coalesce(json_extract(e.f_data, '$.data.message.source.provider'),
1146
1131
  json_extract(e.f_data, '$.message.source.provider')),
1147
1132
  coalesce(json_extract(e.f_data, '$.data.message.source.model'),
1148
1133
  json_extract(e.f_data, '$.message.source.model')),
1134
+ ${withEvent(EVENT_REFERENCED_SQL, "e.f_event_id")},
1135
+ ${withEvent(EVENT_SUBAGENT_SQL, "e.f_event_id")},
1149
1136
  coalesce(json_extract(e.f_data, '$.data.usage.inputTokens'),
1150
1137
  json_extract(e.f_data, '$.usage.inputTokens'), 0),
1151
1138
  coalesce(json_extract(e.f_data, '$.data.usage.outputTokens'),
@@ -1161,6 +1148,35 @@ function backfillEventUsage(db) {
1161
1148
  AND (json_extract(e.f_data, '$.data.usage') IS NOT NULL
1162
1149
  OR json_extract(e.f_data, '$.usage') IS NOT NULL)`);
1163
1150
  }
1151
+ /** 会话 × 本地日 × 模型的 token 汇总:从桥接行 × 用量行重算(含 fork 继承前缀)。 */
1152
+ function backfillSessionUsage(db) {
1153
+ if (!tableIsEmpty(db, "t_session_usage")) return;
1154
+ db.exec(`INSERT OR IGNORE INTO t_session_usage (
1155
+ f_session_id, f_day, f_provider, f_model,
1156
+ f_input_tokens, f_output_tokens, f_cache_read_tokens, f_reasoning_tokens, f_total_tokens)
1157
+ SELECT b.f_session_id, u.f_day, coalesce(u.f_provider, ''), coalesce(u.f_model, ''),
1158
+ sum(u.f_input_tokens), sum(u.f_output_tokens), sum(u.f_cache_read_tokens),
1159
+ sum(u.f_reasoning_tokens), sum(u.f_total_tokens)
1160
+ FROM t_session_events b
1161
+ JOIN t_event_usage u ON u.f_event_id = b.f_event_id
1162
+ GROUP BY b.f_session_id, u.f_day, coalesce(u.f_provider, ''), coalesce(u.f_model, '')`);
1163
+ }
1164
+ /** 会话 × 本地日的活动计数:从桥接行 × 事件类型重算。 */
1165
+ function backfillSessionCounts(db) {
1166
+ if (!tableIsEmpty(db, "t_session_counts")) return;
1167
+ db.exec(`INSERT OR IGNORE INTO t_session_counts (
1168
+ f_session_id, f_day, f_turns, f_steps, f_user_inputs, f_tool_calls)
1169
+ SELECT b.f_session_id,
1170
+ date(e.f_created_at / 1000, 'unixepoch', 'localtime'),
1171
+ sum(CASE WHEN e.f_type = 'turn/start' THEN 1 ELSE 0 END),
1172
+ sum(CASE WHEN e.f_type = 'step/start' THEN 1 ELSE 0 END),
1173
+ sum(CASE WHEN e.f_type = 'user/message' THEN 1 ELSE 0 END),
1174
+ sum(CASE WHEN e.f_type = 'tool/call' THEN 1 ELSE 0 END)
1175
+ FROM t_session_events b
1176
+ JOIN t_events e ON e.f_event_id = b.f_event_id
1177
+ WHERE e.f_type IN (${COUNTED_EVENT_TYPE_SQL})
1178
+ GROUP BY b.f_session_id, 2`);
1179
+ }
1164
1180
  function sessionListRowOf(row) {
1165
1181
  return {
1166
1182
  sessionId: typeof row["session_id"] === "string" ? row["session_id"] : String(row["session_id"]),
@@ -1184,6 +1200,15 @@ function tokenTotalsOf(row) {
1184
1200
  totalTokens: row.total_tokens ?? 0
1185
1201
  };
1186
1202
  }
1203
+ /** `t_session_counts` 的原始行(列名是 SQL 别名)折成四项活动计数。 */
1204
+ function activityTotalsOf(row) {
1205
+ return {
1206
+ turns: row.turns ?? 0,
1207
+ steps: row.steps ?? 0,
1208
+ userInputs: row.user_inputs ?? 0,
1209
+ toolCalls: row.tool_calls ?? 0
1210
+ };
1211
+ }
1187
1212
  /** 一行的整套指标:token 用量 + 活动计数(轮次 / 步骤 / 用户输入 / 工具调用)。 */
1188
1213
  function emptyActivity() {
1189
1214
  return {
@@ -1193,17 +1218,10 @@ function emptyActivity() {
1193
1218
  toolCalls: 0
1194
1219
  };
1195
1220
  }
1196
- /** 有 token 用量的会话:活动计数与列表都限定在这一批会话里,口径一致。 */
1221
+ /** 有 token 用量的会话:活动计数与列表都限定在这一批会话里,口径一致(读汇总表,几百行)。 */
1197
1222
  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})`;
1223
+ return `EXISTS (SELECT 1 FROM t_session_usage ub WHERE ub.f_session_id = ${column})`;
1201
1224
  }
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
1225
  const sqliteMigrationsDir = fileURLToPath(new URL("../drizzle/sqlite/", import.meta.url));
1208
1226
  const sqliteTxQueues = /* @__PURE__ */ new Map();
1209
1227
  function enqueueSqliteTx(path, fn) {
@@ -1226,9 +1244,7 @@ async function openDatabase(path, journalMode, busyTimeout = DEFAULT_BUSY_TIMEOU
1226
1244
  const { user_version: onDisk } = db.prepare("PRAGMA user_version").get();
1227
1245
  if (onDisk === 2 && !hasMigrationsTable(db)) await baselineV2(db);
1228
1246
  migrate(dbx, { migrationsFolder: sqliteMigrationsDir });
1229
- ensureEventCountsTable(db);
1230
- backfillEventCounts(db);
1231
- backfillEventUsage(db);
1247
+ backfillUsageTables(db);
1232
1248
  dbx.insert(tPersistenceState).values({
1233
1249
  fSingleton: 1,
1234
1250
  fStoreId: randomUUID()
@@ -1399,7 +1415,7 @@ var SqliteBackend = class SqliteBackend {
1399
1415
  getHead: (id) => this.getHead(id),
1400
1416
  getSeedLength: (id) => this.getSeedLength(id),
1401
1417
  updateSeedLength: (id, seedLength) => this.updateSeedLength(id, seedLength),
1402
- insertEvents: (events) => this.insertEvents(events),
1418
+ insertEvents: (events, usageRows) => this.insertEvents(events, usageRows),
1403
1419
  insertBridges: (rows) => this.insertBridges(rows),
1404
1420
  updateHead: (id, headEventId, headSequence) => this.updateHead(id, headEventId, headSequence),
1405
1421
  bumpRevision: (id, lastEventAt) => this.bumpRevision(id, lastEventAt),
@@ -1432,14 +1448,10 @@ var SqliteBackend = class SqliteBackend {
1432
1448
  this.db.update(tSessions).set({ fSeedLength: seedLength }).where(eq(tSessions.fSessionId, id)).run();
1433
1449
  }
1434
1450
  static INSERT_BATCH_ROWS = 1e3;
1435
- async insertEvents(events) {
1451
+ async insertEvents(events, usageRows) {
1436
1452
  if (events.length === 0) return;
1437
1453
  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();
1454
+ if (usageRows.length > 0) this.db.insert(tEventUsage).values(usageRows.map((row) => ({ ...row }))).onConflictDoNothing().run();
1443
1455
  }
1444
1456
  async insertBridges(rows) {
1445
1457
  if (rows.length === 0) return;
@@ -1549,41 +1561,49 @@ var SqliteBackend = class SqliteBackend {
1549
1561
  total: Number(counted.n)
1550
1562
  };
1551
1563
  }
1564
+ /**
1565
+ * 用量聚合:只读三张派生统计表——事件级用量行给去重口径的总量与「天 × 模型」桶,
1566
+ * `t_session_usage` / `t_session_counts` 给按会话的行(含 fork 继承前缀)。
1567
+ * 归属、本地日与「是否被引用」都是物化列,这里不再现算、也不回连事件表。
1568
+ * 事件级按毫秒时间戳过滤、汇总表按本地日过滤——`sinceMs` 对齐到本地零点时两者等价。
1569
+ */
1552
1570
  async usageReport(sinceMs) {
1553
- const usageSince = sinceMs === void 0 ? "" : " AND u.f_created_at >= ?";
1571
+ const usageSince = sinceMs === void 0 ? "" : " AND f_created_at >= ?";
1554
1572
  const params = sinceMs === void 0 ? [] : [sinceMs];
1555
- const daySince = sinceMs === void 0 ? "" : " AND c.f_day >= ?";
1573
+ const daySince = sinceMs === void 0 ? "" : " AND %COL% >= ?";
1556
1574
  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);
1575
+ const dayFilter = (column) => daySince.replace("%COL%", column);
1576
+ const buckets = this.db.$client.prepare(`SELECT f_day AS day,
1577
+ f_provider AS provider,
1578
+ f_model AS model,
1579
+ f_subagent AS subagent,
1580
+ sum(f_input_tokens) AS input_tokens,
1581
+ sum(f_output_tokens) AS output_tokens,
1582
+ sum(f_cache_read_tokens) AS cache_read_tokens,
1583
+ sum(f_reasoning_tokens) AS reasoning_tokens,
1584
+ sum(f_total_tokens) AS total_tokens
1585
+ FROM t_event_usage
1586
+ WHERE f_referenced = 1${usageSince}
1587
+ GROUP BY f_day, f_provider, f_model, f_subagent`).all(...params);
1588
+ const tokenScope = this.db.$client.prepare(`SELECT f_subagent AS subagent,
1589
+ sum(f_input_tokens) AS input_tokens,
1590
+ sum(f_output_tokens) AS output_tokens,
1591
+ sum(f_cache_read_tokens) AS cache_read_tokens,
1592
+ sum(f_reasoning_tokens) AS reasoning_tokens,
1593
+ sum(f_total_tokens) AS total_tokens
1594
+ FROM t_event_usage
1595
+ WHERE f_referenced = 1${usageSince}
1596
+ GROUP BY f_subagent`).all(...params);
1579
1597
  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
1598
+ sum(c.f_turns) AS turns,
1599
+ sum(c.f_steps) AS steps,
1600
+ sum(c.f_user_inputs) AS user_inputs,
1601
+ sum(c.f_tool_calls) AS tool_calls
1602
+ FROM t_session_counts c
1583
1603
  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,
1604
+ WHERE ${hasUsageSession("c.f_session_id")}${dayFilter("c.f_day")}
1605
+ GROUP BY subagent`).all(...dayParams);
1606
+ const tokenSessions = this.db.$client.prepare(`SELECT u.f_session_id AS session_id,
1587
1607
  s.f_title AS title,
1588
1608
  (s.f_origin = 'subagent') AS subagent,
1589
1609
  (s.f_archived_at IS NOT NULL) AS archived,
@@ -1592,28 +1612,25 @@ var SqliteBackend = class SqliteBackend {
1592
1612
  sum(u.f_cache_read_tokens) AS cache_read_tokens,
1593
1613
  sum(u.f_reasoning_tokens) AS reasoning_tokens,
1594
1614
  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);
1615
+ FROM t_session_usage u
1616
+ JOIN t_sessions s ON s.f_session_id = u.f_session_id
1617
+ WHERE 1 = 1${dayFilter("u.f_day")}
1618
+ GROUP BY u.f_session_id, s.f_title, s.f_origin, s.f_archived_at`).all(...dayParams);
1619
+ const activitySessions = this.db.$client.prepare(`SELECT f_session_id AS session_id,
1620
+ sum(f_turns) AS turns,
1621
+ sum(f_steps) AS steps,
1622
+ sum(f_user_inputs) AS user_inputs,
1623
+ sum(f_tool_calls) AS tool_calls
1624
+ FROM t_session_counts
1625
+ WHERE 1 = 1${dayFilter("f_day")}
1626
+ GROUP BY f_session_id`).all(...dayParams);
1606
1627
  const human = emptyTotals();
1607
1628
  const subagent = emptyTotals();
1608
1629
  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);
1630
+ for (const row of activityScope) addActivityTotals(row.subagent === 1 ? subagent : human, activityTotalsOf(row));
1610
1631
  const totals = addTotals(addTotals(emptyTotals(), human), subagent);
1611
1632
  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
- }
1633
+ for (const row of activitySessions) activityBySession.set(row.session_id, activityTotalsOf(row));
1617
1634
  return {
1618
1635
  totals,
1619
1636
  subagent,
@@ -1635,28 +1652,72 @@ var SqliteBackend = class SqliteBackend {
1635
1652
  }))
1636
1653
  };
1637
1654
  }
1638
- /** 活动计数旁路累加:派生表 `t_event_counts`,不在写事务里(失败可丢,表可销毁重建)。 */
1639
- async incrementEventCounts(id, buckets) {
1655
+ /** token 用量旁路累加:派生表 `t_session_usage`,不在写事务里(失败可丢,表可销毁重建)。 */
1656
+ async incrementSessionUsage(id, buckets) {
1657
+ if (buckets.length === 0) return;
1658
+ const statement = this.db.$client.prepare(`INSERT INTO t_session_usage (f_session_id, f_day, f_provider, f_model,
1659
+ f_input_tokens, f_output_tokens, f_cache_read_tokens, f_reasoning_tokens, f_total_tokens)
1660
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
1661
+ ON CONFLICT(f_session_id, f_day, f_provider, f_model) DO UPDATE SET
1662
+ f_input_tokens = f_input_tokens + excluded.f_input_tokens,
1663
+ f_output_tokens = f_output_tokens + excluded.f_output_tokens,
1664
+ f_cache_read_tokens = f_cache_read_tokens + excluded.f_cache_read_tokens,
1665
+ f_reasoning_tokens = f_reasoning_tokens + excluded.f_reasoning_tokens,
1666
+ f_total_tokens = f_total_tokens + excluded.f_total_tokens`);
1667
+ for (const bucket of buckets) statement.run(id, bucket.day, bucket.provider ?? "", bucket.model ?? "", bucket.inputTokens, bucket.outputTokens, bucket.cacheReadTokens, bucket.reasoningTokens, bucket.totalTokens);
1668
+ }
1669
+ /** 活动计数旁路累加:派生表 `t_session_counts`,同上(四项计数一次落)。 */
1670
+ async incrementSessionCounts(id, buckets) {
1640
1671
  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);
1672
+ 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)
1673
+ VALUES (?, ?, ?, ?, ?, ?)
1674
+ ON CONFLICT(f_session_id, f_day) DO UPDATE SET
1675
+ f_turns = f_turns + excluded.f_turns,
1676
+ f_steps = f_steps + excluded.f_steps,
1677
+ f_user_inputs = f_user_inputs + excluded.f_user_inputs,
1678
+ f_tool_calls = f_tool_calls + excluded.f_tool_calls`);
1679
+ for (const bucket of buckets) statement.run(id, bucket.day, bucket.turns, bucket.steps, bucket.userInputs, bucket.toolCalls);
1644
1680
  }
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)
1681
+ /** 按会话重算两张汇总表(rewind / fork 之后):先删该会话的行,再从事件表重算。 */
1682
+ async rebuildSessionStats(id) {
1683
+ this.db.$client.prepare("DELETE FROM t_session_usage WHERE f_session_id = ?").run(id);
1684
+ this.db.$client.prepare(`INSERT INTO t_session_usage (
1685
+ f_session_id, f_day, f_provider, f_model,
1686
+ f_input_tokens, f_output_tokens, f_cache_read_tokens, f_reasoning_tokens, f_total_tokens)
1687
+ SELECT b.f_session_id, u.f_day, coalesce(u.f_provider, ''), coalesce(u.f_model, ''),
1688
+ sum(u.f_input_tokens), sum(u.f_output_tokens), sum(u.f_cache_read_tokens),
1689
+ sum(u.f_reasoning_tokens), sum(u.f_total_tokens)
1690
+ FROM t_session_events b
1691
+ JOIN t_event_usage u ON u.f_event_id = b.f_event_id
1692
+ WHERE b.f_session_id = ?
1693
+ GROUP BY b.f_session_id, u.f_day, coalesce(u.f_provider, ''), coalesce(u.f_model, '')`).run(id);
1694
+ this.db.$client.prepare("DELETE FROM t_session_counts WHERE f_session_id = ?").run(id);
1695
+ this.db.$client.prepare(`INSERT INTO t_session_counts (
1696
+ f_session_id, f_day, f_turns, f_steps, f_user_inputs, f_tool_calls)
1649
1697
  SELECT b.f_session_id,
1650
1698
  date(e.f_created_at / 1000, 'unixepoch', 'localtime'),
1651
- e.f_type,
1652
- count(*)
1699
+ sum(CASE WHEN e.f_type = 'turn/start' THEN 1 ELSE 0 END),
1700
+ sum(CASE WHEN e.f_type = 'step/start' THEN 1 ELSE 0 END),
1701
+ sum(CASE WHEN e.f_type = 'user/message' THEN 1 ELSE 0 END),
1702
+ sum(CASE WHEN e.f_type = 'tool/call' THEN 1 ELSE 0 END)
1653
1703
  FROM t_session_events b
1654
1704
  JOIN t_events e ON e.f_event_id = b.f_event_id
1655
1705
  WHERE b.f_session_id = ? AND e.f_type IN (${COUNTED_EVENT_TYPE_SQL})
1656
- GROUP BY 1, 2, 3`).run(id);
1706
+ GROUP BY b.f_session_id, 2`).run(id);
1707
+ }
1708
+ /**
1709
+ * 全量重算用量行的引用标记:rewind 截断 / fork 复用 / 会话删除都会让「引用」跨会话变化,
1710
+ * 只按本会话判定不够。低频路径,一次全表 UPDATE(3 万行量级)。
1711
+ */
1712
+ async refreshEventUsageFlags() {
1713
+ const eventId = "t_event_usage.f_event_id";
1714
+ this.db.$client.exec(`UPDATE t_event_usage SET
1715
+ f_referenced = ${withEvent(EVENT_REFERENCED_SQL, eventId)},
1716
+ f_subagent = ${withEvent(EVENT_SUBAGENT_SQL, eventId)}`);
1657
1717
  }
1658
- async deleteEventCounts(id) {
1659
- this.db.$client.prepare("DELETE FROM t_event_counts WHERE f_session_id = ?").run(id);
1718
+ async deleteSessionStats(id) {
1719
+ this.db.$client.prepare("DELETE FROM t_session_usage WHERE f_session_id = ?").run(id);
1720
+ this.db.$client.prepare("DELETE FROM t_session_counts WHERE f_session_id = ?").run(id);
1660
1721
  }
1661
1722
  async vacuum() {
1662
1723
  await enqueueSqliteTx(this.dbPath, async () => {
@@ -1679,4 +1740,4 @@ var SqliteBackend = class SqliteBackend {
1679
1740
  }
1680
1741
  };
1681
1742
  //#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 };
1743
+ 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 };