@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,6 +1,6 @@
1
- import { A as WriteGuard, O as postgresTableDefs, a as addActivityCount, c as emptyTotals, d as DEFAULT_BUSY_TIMEOUT_MS, f as EVENT_ENCODING, h as eventDimensions, i as COUNTED_EVENT_TYPES, k as toPostgresSchema, l as localDayKey, o as addTokenTotals, r as createStorageRepository, s as addTotals, t as SqliteBackend, u as registerSessionUsage } from "./sqlite-zcXVyMxr.mjs";
2
- import { _ as sessionInsertRow, b as toJsonlArtifact, g as sessionConflictRow, h as scanRows, m as rowToMeta, r as hasLegacyShape, u as repairReadView, x as usageRowOf, y as titleOfEventData } from "./log-qCNkDjh5.mjs";
3
- import { c as isLegacyVersion, o as adoptLegacyRows, s as convertLegacyRows, t as SessionBranchRdb } from "./branch-BjUR6wfe.mjs";
1
+ import { C as postgresTableDefs, T as WriteGuard, a as EVENT_ENCODING, c as eventDimensions, i as DEFAULT_BUSY_TIMEOUT_MS, r as createStorageRepository, t as SqliteBackend, w as toPostgresSchema } from "./sqlite-ChM_sHAG.mjs";
2
+ import { A as registerSessionUsage, C as COUNTED_EVENT_TYPES, D as addTotals, E as addTokenTotals, O as emptyTotals, S as usageRowOf, T as addActivityTotals, _ as sessionConflictRow, b as titleOfEventData, g as sealOwnEvents, h as scanRows, k as localDayKey, m as rowToMeta, r as needsShapeAdoption, u as repairReadView, v as sessionInsertRow, w as addActivityCount, x as toJsonlArtifact } from "./log-DTcvsMz7.mjs";
3
+ import { c as isLegacyVersion, o as adoptLegacyRows, s as convertLegacyRows, t as SessionBranchRdb } from "./branch-7eOh_tbO.mjs";
4
4
  import { registerSessionImport } from "./import.mjs";
5
5
  import z from "@deepseek-ai/schemastery";
6
6
  import { randomUUID } from "node:crypto";
@@ -54,6 +54,15 @@ function emptyActivity() {
54
54
  toolCalls: 0
55
55
  };
56
56
  }
57
+ /** `t_session_counts` 的四项计数折成活动计数(pg 的 sum 回落成字符串)。 */
58
+ function activityTotalsOf(row) {
59
+ return {
60
+ turns: numeric(row["turns"]),
61
+ steps: numeric(row["steps"]),
62
+ userInputs: numeric(row["user_inputs"]),
63
+ toolCalls: numeric(row["tool_calls"])
64
+ };
65
+ }
57
66
  /** `IN (...)` 用的类型字面量(与 `COUNTED_EVENT_TYPES` 同源)。 */
58
67
  const COUNTED_EVENT_TYPE_SQL = COUNTED_EVENT_TYPES.map((type) => `'${type}'`).join(", ");
59
68
  var PostgresBackend = class PostgresBackend {
@@ -107,9 +116,7 @@ var PostgresBackend = class PostgresBackend {
107
116
  if (await this.readMeta(this.db, "schema_version") === "2") await this.baselineV2();
108
117
  }
109
118
  await migrate(this.db, { migrationsFolder: postgresMigrationsDir });
110
- await this.ensureEventCountsTable();
111
- await this.backfillEventCounts();
112
- await this.backfillEventUsage();
119
+ await this.backfillUsageTables();
113
120
  const storeId = await this.db.transaction(async (tx) => {
114
121
  await tx.insert(this.tables["t_persistence_state"]).values({
115
122
  fSingleton: 1,
@@ -170,7 +177,7 @@ var PostgresBackend = class PostgresBackend {
170
177
  getHead: (id) => this.getHead(tx, id),
171
178
  getSeedLength: (id) => this.getSeedLength(tx, id),
172
179
  updateSeedLength: (id, seedLength) => this.updateSeedLength(tx, id, seedLength),
173
- insertEvents: (events) => this.insertEvents(tx, events),
180
+ insertEvents: (events, usageRows) => this.insertEvents(tx, events, usageRows),
174
181
  insertBridges: (rows) => this.insertBridges(tx, rows),
175
182
  updateHead: (id, headEventId, headSequence) => this.updateHead(tx, id, headEventId, headSequence),
176
183
  bumpRevision: (id, lastEventAt) => this.bumpRevision(tx, id, lastEventAt),
@@ -207,14 +214,10 @@ var PostgresBackend = class PostgresBackend {
207
214
  await exec.update(this.tables["t_sessions"]).set({ fSeedLength: seedLength }).where(eq(this.tables["t_sessions"].fSessionId, id)).execute();
208
215
  }
209
216
  static INSERT_BATCH_ROWS = 1e3;
210
- async insertEvents(exec, events) {
217
+ async insertEvents(exec, events, usageRows) {
211
218
  if (events.length === 0) return;
212
219
  for (let i = 0; i < events.length; i += PostgresBackend.INSERT_BATCH_ROWS) await exec.insert(this.tables["t_events"]).values(events.slice(i, i + PostgresBackend.INSERT_BATCH_ROWS).map((event) => ({ ...event }))).execute();
213
- const usage = events.flatMap((event) => {
214
- const row = usageRowOf(event);
215
- return row === void 0 ? [] : [row];
216
- });
217
- if (usage.length > 0) await exec.insert(this.tables["t_event_usage"]).values(usage).onConflictDoNothing().execute();
220
+ if (usageRows.length > 0) for (let i = 0; i < usageRows.length; i += PostgresBackend.INSERT_BATCH_ROWS) await exec.insert(this.tables["t_event_usage"]).values(usageRows.slice(i, i + PostgresBackend.INSERT_BATCH_ROWS).map((row) => ({ ...row }))).onConflictDoNothing().execute();
218
221
  }
219
222
  async insertBridges(exec, rows) {
220
223
  if (rows.length === 0) return;
@@ -284,21 +287,41 @@ var PostgresBackend = class PostgresBackend {
284
287
  async vacuum() {
285
288
  await this.db.execute(sql`VACUUM ANALYZE`);
286
289
  }
287
- /** 一次性回填:旧库没写过 `t_event_usage`,建表后按事件行补一次(表非空即跳过)。 */
290
+ /** 派生统计表的回填:任一表为空即按事件表全量重算(幂等,迁移删表重建后自愈)。 */
291
+ async backfillUsageTables() {
292
+ await this.backfillEventUsage();
293
+ await this.backfillSessionUsage();
294
+ await this.backfillSessionCounts();
295
+ }
296
+ async tableIsEmpty(table) {
297
+ const existing = await this.db.execute(sql.raw(`SELECT count(*) AS n FROM ${table}`));
298
+ return Number(existing.rows[0]?.["n"] ?? 0) === 0;
299
+ }
300
+ /**
301
+ * 用量行:一条 `assistant/message` 事件行一行,物化本地日与两个归属标记。
302
+ * 本地日按数据库会话时区渲染(部署前提是它与 host 时区一致,`localDayKey` 同口径)。
303
+ */
288
304
  async backfillEventUsage() {
305
+ if (!await this.tableIsEmpty(this.qualifiedTable("t_event_usage"))) return;
289
306
  const tEventUsage = this.tables["t_event_usage"];
290
307
  const tEvents = this.tables["t_events"];
291
- const existing = await this.db.execute(sql`SELECT count(*) AS n FROM ${tEventUsage}`);
292
- if (Number(existing.rows[0]?.["n"] ?? 0) > 0) return;
308
+ const tSessionEvents = this.tables["t_session_events"];
309
+ const tSessions = this.tables["t_sessions"];
293
310
  await this.db.execute(sql`
294
311
  INSERT INTO ${tEventUsage} (
295
- f_event_id, f_created_at, f_provider, f_model,
312
+ f_event_id, f_created_at, f_day, f_provider, f_model, f_referenced, f_subagent,
296
313
  f_input_tokens, f_output_tokens, f_cache_read_tokens, f_reasoning_tokens, f_total_tokens)
297
314
  SELECT e.f_event_id, e.f_created_at,
315
+ to_char(to_timestamp(e.f_created_at / 1000.0), 'YYYY-MM-DD'),
298
316
  coalesce(e.f_data::json #>> '{data,message,source,provider}',
299
317
  e.f_data::json #>> '{message,source,provider}'),
300
318
  coalesce(e.f_data::json #>> '{data,message,source,model}',
301
319
  e.f_data::json #>> '{message,source,model}'),
320
+ (EXISTS (SELECT 1 FROM ${tSessionEvents} rb
321
+ WHERE rb.f_event_id = e.f_event_id))::int,
322
+ (EXISTS (SELECT 1 FROM ${tSessionEvents} sb
323
+ JOIN ${tSessions} ss ON ss.f_session_id = sb.f_session_id
324
+ WHERE sb.f_event_id = e.f_event_id AND ss.f_origin = 'subagent'))::int,
302
325
  coalesce((e.f_data::json #>> '{data,usage,inputTokens}')::integer,
303
326
  (e.f_data::json #>> '{usage,inputTokens}')::integer, 0),
304
327
  coalesce((e.f_data::json #>> '{data,usage,outputTokens}')::integer,
@@ -314,6 +337,47 @@ var PostgresBackend = class PostgresBackend {
314
337
  AND (e.f_data::json #> '{data,usage}' IS NOT NULL
315
338
  OR e.f_data::json #> '{usage}' IS NOT NULL)
316
339
  ON CONFLICT DO NOTHING
340
+ `);
341
+ }
342
+ /** 会话 × 本地日 × 模型的 token 汇总:从桥接行 × 用量行重算(含 fork 继承前缀)。 */
343
+ async backfillSessionUsage() {
344
+ if (!await this.tableIsEmpty(this.qualifiedTable("t_session_usage"))) return;
345
+ const tSessionUsage = this.tables["t_session_usage"];
346
+ const tSessionEvents = this.tables["t_session_events"];
347
+ const tEventUsage = this.tables["t_event_usage"];
348
+ await this.db.execute(sql`
349
+ INSERT INTO ${tSessionUsage} (
350
+ f_session_id, f_day, f_provider, f_model,
351
+ f_input_tokens, f_output_tokens, f_cache_read_tokens, f_reasoning_tokens, f_total_tokens)
352
+ SELECT b.f_session_id, u.f_day, coalesce(u.f_provider, ''), coalesce(u.f_model, ''),
353
+ sum(u.f_input_tokens), sum(u.f_output_tokens), sum(u.f_cache_read_tokens),
354
+ sum(u.f_reasoning_tokens), sum(u.f_total_tokens)
355
+ FROM ${tSessionEvents} b
356
+ JOIN ${tEventUsage} u ON u.f_event_id = b.f_event_id
357
+ GROUP BY 1, 2, 3, 4
358
+ ON CONFLICT DO NOTHING
359
+ `);
360
+ }
361
+ /** 会话 × 本地日的活动计数:从桥接行 × 事件类型重算。 */
362
+ async backfillSessionCounts() {
363
+ if (!await this.tableIsEmpty(this.qualifiedTable("t_session_counts"))) return;
364
+ const tSessionCounts = this.tables["t_session_counts"];
365
+ const tSessionEvents = this.tables["t_session_events"];
366
+ const tEvents = this.tables["t_events"];
367
+ await this.db.execute(sql`
368
+ INSERT INTO ${tSessionCounts} (
369
+ f_session_id, f_day, f_turns, f_steps, f_user_inputs, f_tool_calls)
370
+ SELECT b.f_session_id,
371
+ to_char(to_timestamp(e.f_created_at / 1000.0), 'YYYY-MM-DD'),
372
+ sum(CASE WHEN e.f_type = 'turn/start' THEN 1 ELSE 0 END),
373
+ sum(CASE WHEN e.f_type = 'step/start' THEN 1 ELSE 0 END),
374
+ sum(CASE WHEN e.f_type = 'user/message' THEN 1 ELSE 0 END),
375
+ sum(CASE WHEN e.f_type = 'tool/call' THEN 1 ELSE 0 END)
376
+ FROM ${tSessionEvents} b
377
+ JOIN ${tEvents} e ON e.f_event_id = b.f_event_id
378
+ WHERE e.f_type IN (${sql.raw(COUNTED_EVENT_TYPE_SQL)})
379
+ GROUP BY 1, 2
380
+ ON CONFLICT DO NOTHING
317
381
  `);
318
382
  }
319
383
  /** 用量聚合:与 SQLite 侧同形,读 `t_event_usage`,数值列回来是字符串。 */
@@ -366,85 +430,78 @@ var PostgresBackend = class PostgresBackend {
366
430
  total: numeric(counted.rows[0]?.["n"] ?? 0)
367
431
  };
368
432
  }
433
+ /**
434
+ * 用量聚合:与 SQLite 侧同形——只读三张派生统计表(用量行给去重口径的总量与「天 × 模型」桶,
435
+ * 两张会话汇总表给按会话的行),数值列回来是字符串。物化列让这里不做 `EXISTS`、不回连事件表。
436
+ */
369
437
  async usageReport(sinceMs) {
370
- const usageSince = sinceMs === void 0 ? sql`` : sql` AND u.f_created_at >= ${sinceMs}`;
371
- const daySince = sinceMs === void 0 ? sql`` : sql` AND c.f_day >= ${localDayKey(sinceMs)}`;
438
+ const usageSince = sinceMs === void 0 ? sql`` : sql` AND f_created_at >= ${sinceMs}`;
439
+ const dayFilter = (column) => sinceMs === void 0 ? sql`` : sql` AND ${sql.raw(column)} >= ${localDayKey(sinceMs)}`;
372
440
  const tEventUsage = this.tables["t_event_usage"];
373
- const tSessionEvents = this.tables["t_session_events"];
441
+ const tSessionUsage = this.tables["t_session_usage"];
442
+ const tSessionCounts = this.tables["t_session_counts"];
374
443
  const tSessions = this.tables["t_sessions"];
375
- const eventCountsTable = this.qualifiedTable("t_event_counts");
376
- const subagentOfUsage = sql`EXISTS (SELECT 1 FROM ${tSessionEvents} sb
377
- JOIN ${tSessions} ss ON ss.f_session_id = sb.f_session_id
378
- WHERE sb.f_event_id = u.f_event_id AND ss.f_origin = 'subagent')`;
379
- const subagentOfCounts = sql`EXISTS (SELECT 1 FROM ${tSessionEvents} sb
380
- JOIN ${tSessions} ss ON ss.f_session_id = sb.f_session_id
381
- WHERE sb.f_session_id = c.f_session_id AND ss.f_origin = 'subagent')`;
382
- const hasUsageSession = (column) => sql`EXISTS (SELECT 1 FROM ${tSessionEvents} ub
383
- JOIN ${tEventUsage} uu ON uu.f_event_id = ub.f_event_id
384
- WHERE ub.f_session_id = ${sql.raw(column)})`;
385
- const tokenColumns = sql.raw(`sum(u.f_input_tokens) AS input_tokens,
386
- sum(u.f_output_tokens) AS output_tokens,
387
- sum(u.f_cache_read_tokens) AS cache_read_tokens,
388
- sum(u.f_reasoning_tokens) AS reasoning_tokens,
389
- sum(u.f_total_tokens) AS total_tokens`);
444
+ const tokenColumns = (alias) => sql.raw(`sum(${alias}.f_input_tokens) AS input_tokens,
445
+ sum(${alias}.f_output_tokens) AS output_tokens,
446
+ sum(${alias}.f_cache_read_tokens) AS cache_read_tokens,
447
+ sum(${alias}.f_reasoning_tokens) AS reasoning_tokens,
448
+ sum(${alias}.f_total_tokens) AS total_tokens`);
390
449
  const buckets = await this.db.execute(sql`
391
- SELECT to_char(to_timestamp(u.f_created_at / 1000.0), 'YYYY-MM-DD') AS day,
392
- u.f_provider AS provider,
393
- u.f_model AS model,
394
- ${subagentOfUsage} AS subagent,
395
- ${tokenColumns}
396
- FROM ${tEventUsage} u
397
- WHERE EXISTS (SELECT 1 FROM ${tSessionEvents} rb WHERE rb.f_event_id = u.f_event_id)${usageSince}
450
+ SELECT a.f_day AS day,
451
+ a.f_provider AS provider,
452
+ a.f_model AS model,
453
+ (a.f_subagent = 1) AS subagent,
454
+ ${tokenColumns("a")}
455
+ FROM ${tEventUsage} a
456
+ WHERE a.f_referenced = 1${usageSince}
398
457
  GROUP BY 1, 2, 3, 4
399
458
  `);
400
459
  const tokenScope = await this.db.execute(sql`
401
- SELECT ${subagentOfUsage} AS subagent,
402
- ${tokenColumns}
403
- FROM ${tEventUsage} u
404
- WHERE EXISTS (SELECT 1 FROM ${tSessionEvents} rb WHERE rb.f_event_id = u.f_event_id)${usageSince}
460
+ SELECT (a.f_subagent = 1) AS subagent,
461
+ ${tokenColumns("a")}
462
+ FROM ${tEventUsage} a
463
+ WHERE a.f_referenced = 1${usageSince}
405
464
  GROUP BY 1
406
465
  `);
407
466
  const activityScope = await this.db.execute(sql`
408
- SELECT ${subagentOfCounts} AS subagent,
409
- c.f_type AS f_type,
410
- sum(c.f_count) AS n
411
- FROM ${sql.raw(eventCountsTable)} c
467
+ SELECT (s.f_origin = 'subagent') AS subagent,
468
+ sum(c.f_turns) AS turns,
469
+ sum(c.f_steps) AS steps,
470
+ sum(c.f_user_inputs) AS user_inputs,
471
+ sum(c.f_tool_calls) AS tool_calls
472
+ FROM ${tSessionCounts} c
412
473
  JOIN ${tSessions} s ON s.f_session_id = c.f_session_id
413
- WHERE ${hasUsageSession("c.f_session_id")}${daySince}
414
- GROUP BY 1, 2
474
+ WHERE EXISTS (SELECT 1 FROM ${tSessionUsage} ub WHERE ub.f_session_id = c.f_session_id)${dayFilter("c.f_day")}
475
+ GROUP BY 1
415
476
  `);
416
477
  const tokenSessions = await this.db.execute(sql`
417
- SELECT b.f_session_id AS session_id,
478
+ SELECT u.f_session_id AS session_id,
418
479
  s.f_title AS title,
419
480
  (s.f_origin = 'subagent') AS subagent,
420
481
  (s.f_archived_at IS NOT NULL) AS archived,
421
- ${tokenColumns}
422
- FROM ${tSessionEvents} b
423
- JOIN ${tEventUsage} u ON u.f_event_id = b.f_event_id
424
- JOIN ${tSessions} s ON s.f_session_id = b.f_session_id
425
- WHERE 1 = 1${usageSince}
426
- GROUP BY b.f_session_id, s.f_title, s.f_origin, s.f_archived_at
482
+ ${tokenColumns("u")}
483
+ FROM ${tSessionUsage} u
484
+ JOIN ${tSessions} s ON s.f_session_id = u.f_session_id
485
+ WHERE 1 = 1${dayFilter("u.f_day")}
486
+ GROUP BY u.f_session_id, s.f_title, s.f_origin, s.f_archived_at
427
487
  `);
428
488
  const activitySessions = await this.db.execute(sql`
429
489
  SELECT c.f_session_id AS session_id,
430
- c.f_type AS f_type,
431
- sum(c.f_count) AS n
432
- FROM ${sql.raw(eventCountsTable)} c
433
- WHERE ${hasUsageSession("c.f_session_id")}${daySince}
434
- GROUP BY c.f_session_id, c.f_type
490
+ sum(c.f_turns) AS turns,
491
+ sum(c.f_steps) AS steps,
492
+ sum(c.f_user_inputs) AS user_inputs,
493
+ sum(c.f_tool_calls) AS tool_calls
494
+ FROM ${tSessionCounts} c
495
+ WHERE 1 = 1${dayFilter("c.f_day")}
496
+ GROUP BY c.f_session_id
435
497
  `);
436
498
  const human = emptyTotals();
437
499
  const subagent = emptyTotals();
438
500
  for (const row of tokenScope.rows) addTokenTotals(row["subagent"] === true ? subagent : human, tokenTotalsOf(row));
439
- for (const row of activityScope.rows) addActivityCount(row["subagent"] === true ? subagent : human, String(row["f_type"]), numeric(row["n"]));
501
+ for (const row of activityScope.rows) addActivityTotals(row["subagent"] === true ? subagent : human, activityTotalsOf(row));
440
502
  const totals = addTotals(addTotals(emptyTotals(), human), subagent);
441
503
  const activityBySession = /* @__PURE__ */ new Map();
442
- for (const row of activitySessions.rows) {
443
- const key = String(row["session_id"]);
444
- const target = activityBySession.get(key) ?? emptyActivity();
445
- addActivityCount(target, String(row["f_type"]), numeric(row["n"]));
446
- activityBySession.set(key, target);
447
- }
504
+ for (const row of activitySessions.rows) activityBySession.set(String(row["session_id"]), activityTotalsOf(row));
448
505
  return {
449
506
  totals,
450
507
  subagent,
@@ -466,76 +523,100 @@ var PostgresBackend = class PostgresBackend {
466
523
  }))
467
524
  };
468
525
  }
469
- /** 表名限定到配置的 schema(派生表不进 drizzle schema / 迁移,建表幂等)。 */
526
+ /** 表名限定到配置的 schema。 */
470
527
  qualifiedTable(name) {
471
528
  const schema = this.options.schema ?? "public";
472
529
  return schema === "public" ? name : `"${schema}".${name}`;
473
530
  }
474
- async ensureEventCountsTable() {
475
- const table = this.qualifiedTable("t_event_counts");
476
- await this.db.execute(sql.raw(`CREATE TABLE IF NOT EXISTS ${table} (
477
- f_id serial PRIMARY KEY,
478
- f_session_id text NOT NULL,
479
- f_day text NOT NULL,
480
- f_type text NOT NULL,
481
- f_count integer NOT NULL DEFAULT 0
482
- )`));
483
- await this.db.execute(sql.raw(`CREATE UNIQUE INDEX IF NOT EXISTS uq_event_counts_session_day_type ON ${table}(f_session_id, f_day, f_type)`));
484
- await this.db.execute(sql.raw(`CREATE INDEX IF NOT EXISTS idx_event_counts_day ON ${table}(f_day)`));
485
- }
486
- /** 一次性回填活动计数(表为空时从事件表重算;幂等)。 */
487
- async backfillEventCounts() {
488
- const table = this.qualifiedTable("t_event_counts");
489
- const existing = await this.db.execute(sql.raw(`SELECT count(*) AS n FROM ${table}`));
490
- if (Number(existing.rows[0]?.["n"] ?? 0) > 0) return;
491
- const tSessionEvents = this.tables["t_session_events"];
492
- const tEvents = this.tables["t_events"];
493
- await this.db.execute(sql`
494
- INSERT INTO ${sql.raw(table)} (f_session_id, f_day, f_type, f_count)
495
- SELECT ${tSessionEvents}.f_session_id,
496
- to_char(to_timestamp(${tEvents}.f_created_at / 1000.0), 'YYYY-MM-DD'),
497
- ${tEvents}.f_type,
498
- count(*)
499
- FROM ${tSessionEvents}
500
- JOIN ${tEvents} ON ${tEvents}.f_event_id = ${tSessionEvents}.f_event_id
501
- WHERE ${tEvents}.f_type IN (${sql.raw(COUNTED_EVENT_TYPE_SQL)})
502
- GROUP BY 1, 2, 3
503
- ON CONFLICT DO NOTHING
504
- `);
531
+ /** token 用量旁路累加:派生表 `t_session_usage`,不在写事务里(失败可丢,表可销毁重建)。 */
532
+ async incrementSessionUsage(id, buckets) {
533
+ if (buckets.length === 0) return;
534
+ const tSessionUsage = this.tables["t_session_usage"];
535
+ for (const bucket of buckets) await this.db.execute(sql`
536
+ INSERT INTO ${tSessionUsage} (
537
+ f_session_id, f_day, f_provider, f_model,
538
+ f_input_tokens, f_output_tokens, f_cache_read_tokens, f_reasoning_tokens, f_total_tokens)
539
+ VALUES (${id}, ${bucket.day}, ${bucket.provider ?? ""}, ${bucket.model ?? ""},
540
+ ${bucket.inputTokens}, ${bucket.outputTokens}, ${bucket.cacheReadTokens},
541
+ ${bucket.reasoningTokens}, ${bucket.totalTokens})
542
+ ON CONFLICT (f_session_id, f_day, f_provider, f_model) DO UPDATE SET
543
+ f_input_tokens = ${tSessionUsage}.f_input_tokens + EXCLUDED.f_input_tokens,
544
+ f_output_tokens = ${tSessionUsage}.f_output_tokens + EXCLUDED.f_output_tokens,
545
+ f_cache_read_tokens = ${tSessionUsage}.f_cache_read_tokens + EXCLUDED.f_cache_read_tokens,
546
+ f_reasoning_tokens = ${tSessionUsage}.f_reasoning_tokens + EXCLUDED.f_reasoning_tokens,
547
+ f_total_tokens = ${tSessionUsage}.f_total_tokens + EXCLUDED.f_total_tokens
548
+ `);
505
549
  }
506
- /** 活动计数旁路累加:派生表,不在写事务里(失败可丢,表可销毁重建)。 */
507
- async incrementEventCounts(id, buckets) {
550
+ /** 活动计数旁路累加:派生表 `t_session_counts`,同上(四项计数一次落)。 */
551
+ async incrementSessionCounts(id, buckets) {
508
552
  if (buckets.length === 0) return;
509
- const table = this.qualifiedTable("t_event_counts");
553
+ const tSessionCounts = this.tables["t_session_counts"];
510
554
  for (const bucket of buckets) await this.db.execute(sql`
511
- INSERT INTO ${sql.raw(table)} (f_session_id, f_day, f_type, f_count)
512
- VALUES (${id}, ${bucket.day}, ${bucket.type}, ${bucket.count})
513
- ON CONFLICT (f_session_id, f_day, f_type)
514
- DO UPDATE SET f_count = ${sql.raw(`${table}.f_count`)} + EXCLUDED.f_count
555
+ INSERT INTO ${tSessionCounts} (
556
+ f_session_id, f_day, f_turns, f_steps, f_user_inputs, f_tool_calls)
557
+ VALUES (${id}, ${bucket.day}, ${bucket.turns}, ${bucket.steps},
558
+ ${bucket.userInputs}, ${bucket.toolCalls})
559
+ ON CONFLICT (f_session_id, f_day) DO UPDATE SET
560
+ f_turns = ${tSessionCounts}.f_turns + EXCLUDED.f_turns,
561
+ f_steps = ${tSessionCounts}.f_steps + EXCLUDED.f_steps,
562
+ f_user_inputs = ${tSessionCounts}.f_user_inputs + EXCLUDED.f_user_inputs,
563
+ f_tool_calls = ${tSessionCounts}.f_tool_calls + EXCLUDED.f_tool_calls
515
564
  `);
516
565
  }
517
- /** 按会话重算活动计数(rewind / fork 之后):先删该会话的行,再从事件表重算。 */
518
- async rebuildEventCounts(id) {
519
- const table = this.qualifiedTable("t_event_counts");
566
+ /** 按会话重算两张汇总表(rewind / fork 之后):先删该会话的行,再从事件表重算。 */
567
+ async rebuildSessionStats(id) {
568
+ const tSessionUsage = this.tables["t_session_usage"];
569
+ const tSessionCounts = this.tables["t_session_counts"];
520
570
  const tSessionEvents = this.tables["t_session_events"];
571
+ const tEventUsage = this.tables["t_event_usage"];
521
572
  const tEvents = this.tables["t_events"];
522
- await this.db.execute(sql`DELETE FROM ${sql.raw(table)} WHERE f_session_id = ${id}`);
573
+ await this.db.execute(sql`DELETE FROM ${tSessionUsage} WHERE f_session_id = ${id}`);
523
574
  await this.db.execute(sql`
524
- INSERT INTO ${sql.raw(table)} (f_session_id, f_day, f_type, f_count)
525
- SELECT ${tSessionEvents}.f_session_id,
526
- to_char(to_timestamp(${tEvents}.f_created_at / 1000.0), 'YYYY-MM-DD'),
527
- ${tEvents}.f_type,
528
- count(*)
529
- FROM ${tSessionEvents}
530
- JOIN ${tEvents} ON ${tEvents}.f_event_id = ${tSessionEvents}.f_event_id
531
- WHERE ${tSessionEvents}.f_session_id = ${id}
532
- AND ${tEvents}.f_type IN (${sql.raw(COUNTED_EVENT_TYPE_SQL)})
533
- GROUP BY 1, 2, 3
575
+ INSERT INTO ${tSessionUsage} (
576
+ f_session_id, f_day, f_provider, f_model,
577
+ f_input_tokens, f_output_tokens, f_cache_read_tokens, f_reasoning_tokens, f_total_tokens)
578
+ SELECT b.f_session_id, u.f_day, coalesce(u.f_provider, ''), coalesce(u.f_model, ''),
579
+ sum(u.f_input_tokens), sum(u.f_output_tokens), sum(u.f_cache_read_tokens),
580
+ sum(u.f_reasoning_tokens), sum(u.f_total_tokens)
581
+ FROM ${tSessionEvents} b
582
+ JOIN ${tEventUsage} u ON u.f_event_id = b.f_event_id
583
+ WHERE b.f_session_id = ${id}
584
+ GROUP BY 1, 2, 3, 4
585
+ `);
586
+ await this.db.execute(sql`DELETE FROM ${tSessionCounts} WHERE f_session_id = ${id}`);
587
+ await this.db.execute(sql`
588
+ INSERT INTO ${tSessionCounts} (
589
+ f_session_id, f_day, f_turns, f_steps, f_user_inputs, f_tool_calls)
590
+ SELECT b.f_session_id,
591
+ to_char(to_timestamp(e.f_created_at / 1000.0), 'YYYY-MM-DD'),
592
+ sum(CASE WHEN e.f_type = 'turn/start' THEN 1 ELSE 0 END),
593
+ sum(CASE WHEN e.f_type = 'step/start' THEN 1 ELSE 0 END),
594
+ sum(CASE WHEN e.f_type = 'user/message' THEN 1 ELSE 0 END),
595
+ sum(CASE WHEN e.f_type = 'tool/call' THEN 1 ELSE 0 END)
596
+ FROM ${tSessionEvents} b
597
+ JOIN ${tEvents} e ON e.f_event_id = b.f_event_id
598
+ WHERE b.f_session_id = ${id} AND e.f_type IN (${sql.raw(COUNTED_EVENT_TYPE_SQL)})
599
+ GROUP BY 1, 2
534
600
  `);
535
601
  }
536
- async deleteEventCounts(id) {
537
- const table = this.qualifiedTable("t_event_counts");
538
- await this.db.execute(sql`DELETE FROM ${sql.raw(table)} WHERE f_session_id = ${id}`);
602
+ /** 全量重算用量行的引用标记(rewind / fork / 会话删除之后)。 */
603
+ async refreshEventUsageFlags() {
604
+ const tEventUsage = this.tables["t_event_usage"];
605
+ const tSessionEvents = this.tables["t_session_events"];
606
+ const tSessions = this.tables["t_sessions"];
607
+ await this.db.execute(sql`
608
+ UPDATE ${tEventUsage} SET
609
+ f_referenced = (EXISTS (SELECT 1 FROM ${tSessionEvents} rb
610
+ WHERE rb.f_event_id = ${tEventUsage}.f_event_id))::int,
611
+ f_subagent = (EXISTS (SELECT 1 FROM ${tSessionEvents} sb
612
+ JOIN ${tSessions} ss ON ss.f_session_id = sb.f_session_id
613
+ WHERE sb.f_event_id = ${tEventUsage}.f_event_id
614
+ AND ss.f_origin = 'subagent'))::int
615
+ `);
616
+ }
617
+ async deleteSessionStats(id) {
618
+ await this.db.execute(sql`DELETE FROM ${this.tables["t_session_usage"]} WHERE f_session_id = ${id}`);
619
+ await this.db.execute(sql`DELETE FROM ${this.tables["t_session_counts"]} WHERE f_session_id = ${id}`);
539
620
  }
540
621
  async getPrevBridge(exec, id, sequence) {
541
622
  return (await exec.select({
@@ -1657,7 +1738,7 @@ var SessionPersistenceRdb = class extends SessionPersistence {
1657
1738
  this.liveFailures.delete(id);
1658
1739
  this.liveDropWarned.delete(id);
1659
1740
  this.reuseEventIds.delete(id);
1660
- await this.deleteEventCounts(id);
1741
+ await this.dropSessionStats(id);
1661
1742
  }
1662
1743
  /** GC 通道:回收已无桥接行引用的事件行(孤儿),返回删除行数。 */
1663
1744
  async collectOrphans() {
@@ -1676,7 +1757,7 @@ var SessionPersistenceRdb = class extends SessionPersistence {
1676
1757
  this.liveFailures.delete(id);
1677
1758
  this.liveDropWarned.delete(id);
1678
1759
  this.reuseEventIds.delete(id);
1679
- await this.deleteEventCounts(id);
1760
+ await this.dropSessionStats(id);
1680
1761
  }
1681
1762
  return deleted;
1682
1763
  }
@@ -1727,9 +1808,11 @@ var SessionPersistenceRdb = class extends SessionPersistence {
1727
1808
  async appendBatch(meta, inheritedEventCount, events, tornTruncateTo) {
1728
1809
  await this.ready;
1729
1810
  if (events.length === 0) return false;
1811
+ sealOwnEvents(events);
1730
1812
  validateStoredEvents(meta, [...events]);
1731
1813
  const reuse = this.reuseEventIds.get(meta.id);
1732
1814
  let confirmedHead = -1;
1815
+ let usageRows = [];
1733
1816
  await this.backend.transaction(async (tx) => {
1734
1817
  if (tornTruncateTo !== void 0) {
1735
1818
  await tx.deleteBridgeTail(meta.id, tornTruncateTo);
@@ -1743,18 +1826,19 @@ var SessionPersistenceRdb = class extends SessionPersistence {
1743
1826
  }, randomUUID());
1744
1827
  const head = await tx.getHead(meta.id);
1745
1828
  this.writeGuard.assertNoConcurrentWriter(meta.id, head.fHeadSequence);
1746
- const { headEventId, headSequence } = await appendEventTail(tx, meta, events, {
1829
+ const appended = await appendEventTail(tx, meta, events, {
1747
1830
  parentId: head.fHeadEventId,
1748
1831
  nextSeq: head.fHeadSequence + 1
1749
1832
  }, reuse);
1750
- await tx.updateHead(meta.id, headEventId, headSequence);
1833
+ usageRows = appended.usageRows;
1834
+ await tx.updateHead(meta.id, appended.headEventId, appended.headSequence);
1751
1835
  await tx.bumpRevision(meta.id, maxEventTime(events));
1752
- confirmedHead = headSequence;
1836
+ confirmedHead = appended.headSequence;
1753
1837
  });
1754
1838
  if (reuse !== void 0) this.reuseEventIds.delete(meta.id);
1755
1839
  this.writeGuard.confirmHead(meta.id, confirmedHead);
1756
1840
  this.tracker.materialized(meta.id);
1757
- await this.recordEventCounts(meta.id, events);
1841
+ await this.recordSessionStats(meta.id, events, usageRows);
1758
1842
  return true;
1759
1843
  }
1760
1844
  async readLog(id, options = {}, signal) {
@@ -1792,7 +1876,7 @@ var SessionPersistenceRdb = class extends SessionPersistence {
1792
1876
  };
1793
1877
  }
1794
1878
  const { preserved, tornFrom } = scanRows(eventRows, options.fromSeq ?? 0);
1795
- if (hasLegacyShape(preserved)) {
1879
+ if (needsShapeAdoption(preserved)) {
1796
1880
  const adopted = adoptLegacyRows(row, eventRows);
1797
1881
  return {
1798
1882
  meta: adopted.meta,
@@ -1817,32 +1901,43 @@ var SessionPersistenceRdb = class extends SessionPersistence {
1817
1901
  };
1818
1902
  }
1819
1903
  /**
1820
- * 活动计数**旁路累加**:不在写事务里、失败只 warn——`t_event_counts` 是可销毁重建的派生表,
1821
- * 丢几次累加不影响可用性(需要时用 `rebuildEventCounts` 重算)。
1904
+ * 统计衍生表的**旁路累加**:不在写事务里、失败只 warn——两张会话汇总表都是可销毁重建的
1905
+ * 派生表,丢几次累加不影响可用性(需要时用 `rebuildSessionStats` 重算)。
1906
+ * token 走写路径已折好的用量行,活动计数走本批事件类型。
1822
1907
  */
1823
- async recordEventCounts(id, events) {
1824
- const buckets = eventCountBuckets(events);
1825
- if (buckets.length === 0) return;
1826
- try {
1827
- await this.backend.incrementEventCounts(id, buckets);
1908
+ async recordSessionStats(id, events, usageRows) {
1909
+ const usageBuckets = sessionUsageBuckets(usageRows);
1910
+ if (usageBuckets.length > 0) try {
1911
+ await this.backend.incrementSessionUsage(id, usageBuckets);
1912
+ } catch (error) {
1913
+ this.ctx.logger.warn(`session-rdb: token usage for "${id}" not updated (${describeError(error)})`);
1914
+ }
1915
+ const countBuckets = sessionCountBuckets(events);
1916
+ if (countBuckets.length > 0) try {
1917
+ await this.backend.incrementSessionCounts(id, countBuckets);
1828
1918
  } catch (error) {
1829
1919
  this.ctx.logger.warn(`session-rdb: activity counts for "${id}" not updated (${describeError(error)})`);
1830
1920
  }
1831
1921
  }
1832
- /** 重算一个会话的活动计数(rewind / fork 之后;best-effort,同 `recordEventCounts`)。 */
1833
- async rebuildEventCounts(id) {
1922
+ /**
1923
+ * 重算一个会话的统计(rewind / fork 之后;best-effort,同 `recordSessionStats`):
1924
+ * 两张汇总表按会话重算,用量行的引用标记全量重算(引用可能跨会话消失)。
1925
+ */
1926
+ async rebuildSessionStats(id) {
1834
1927
  try {
1835
- await this.backend.rebuildEventCounts(id);
1928
+ await this.backend.rebuildSessionStats(id);
1929
+ await this.backend.refreshEventUsageFlags();
1836
1930
  } catch (error) {
1837
- this.ctx.logger.warn(`session-rdb: activity counts for "${id}" not rebuilt (${describeError(error)})`);
1931
+ this.ctx.logger.warn(`session-rdb: session stats for "${id}" not rebuilt (${describeError(error)})`);
1838
1932
  }
1839
1933
  }
1840
- /** 会话删除时清掉它的活动计数行(best-effort)。 */
1841
- async deleteEventCounts(id) {
1934
+ /** 会话删除时清掉它的汇总行,并重算用量行的引用标记(被删会话引用的行要立刻退出统计)。 */
1935
+ async dropSessionStats(id) {
1842
1936
  try {
1843
- await this.backend.deleteEventCounts(id);
1937
+ await this.backend.deleteSessionStats(id);
1938
+ await this.backend.refreshEventUsageFlags();
1844
1939
  } catch (error) {
1845
- this.ctx.logger.warn(`session-rdb: activity counts for "${id}" not deleted (${describeError(error)})`);
1940
+ this.ctx.logger.warn(`session-rdb: session stats for "${id}" not deleted (${describeError(error)})`);
1846
1941
  }
1847
1942
  }
1848
1943
  async rewriteMigratedLog(id, log) {
@@ -1855,13 +1950,14 @@ var SessionPersistenceRdb = class extends SessionPersistence {
1855
1950
  meta: log.meta,
1856
1951
  inheritedEventCount: SessionLogOffset(log.inheritedEventCount)
1857
1952
  }, randomUUID());
1858
- const { headEventId, headSequence } = await appendEventTail(tx, log.meta, log.events, {
1953
+ const appended = await appendEventTail(tx, log.meta, log.events, {
1859
1954
  parentId: "",
1860
1955
  nextSeq: 0
1861
1956
  });
1862
- await tx.updateHead(id, headEventId, headSequence);
1957
+ await tx.updateHead(id, appended.headEventId, appended.headSequence);
1863
1958
  await tx.bumpRevision(id, maxEventTime(log.events));
1864
1959
  });
1960
+ await this.rebuildSessionStats(id);
1865
1961
  }
1866
1962
  async listSnapshots(signal) {
1867
1963
  signal?.throwIfAborted();
@@ -2091,19 +2187,46 @@ function seedCoversPrefix(seed, prefix) {
2091
2187
  return seedEvent !== void 0 && JSON.stringify(seedEvent) === JSON.stringify(event);
2092
2188
  });
2093
2189
  }
2094
- /** 一批事件折成活动计数的桶:只数四种类型,按本地日聚合。 */
2095
- function eventCountBuckets(events) {
2190
+ /** 一批事件折成活动计数的桶:只数四种类型,按本地日聚合成四项。 */
2191
+ function sessionCountBuckets(events) {
2096
2192
  const counted = /* @__PURE__ */ new Map();
2097
2193
  for (const event of events) {
2098
2194
  if (!COUNTED_EVENT_TYPES.includes(event.type)) continue;
2099
2195
  const day = localDayKey(event.time);
2100
- const key = `${day}#${event.type}`;
2101
- const bucket = counted.get(key) ?? {
2196
+ const bucket = counted.get(day) ?? {
2102
2197
  day,
2103
- type: event.type,
2104
- count: 0
2198
+ turns: 0,
2199
+ steps: 0,
2200
+ userInputs: 0,
2201
+ toolCalls: 0
2105
2202
  };
2106
- bucket.count += 1;
2203
+ addActivityCount(bucket, event.type, 1);
2204
+ counted.set(day, bucket);
2205
+ }
2206
+ return [...counted.values()];
2207
+ }
2208
+ /** 一批用量行折成 token 桶:按「本地日 × 模型」聚合(模型未知落空串,与表口径一致)。 */
2209
+ function sessionUsageBuckets(usageRows) {
2210
+ const counted = /* @__PURE__ */ new Map();
2211
+ for (const row of usageRows) {
2212
+ const provider = row.fProvider ?? "";
2213
+ const model = row.fModel ?? "";
2214
+ const key = `${row.fDay}#${provider}#${model}`;
2215
+ const bucket = counted.get(key) ?? {
2216
+ day: row.fDay,
2217
+ provider,
2218
+ model,
2219
+ inputTokens: 0,
2220
+ outputTokens: 0,
2221
+ cacheReadTokens: 0,
2222
+ reasoningTokens: 0,
2223
+ totalTokens: 0
2224
+ };
2225
+ bucket.inputTokens += row.fInputTokens;
2226
+ bucket.outputTokens += row.fOutputTokens;
2227
+ bucket.cacheReadTokens += row.fCacheReadTokens;
2228
+ bucket.reasoningTokens += row.fReasoningTokens;
2229
+ bucket.totalTokens += row.fTotalTokens;
2107
2230
  counted.set(key, bucket);
2108
2231
  }
2109
2232
  return [...counted.values()];
@@ -2135,6 +2258,7 @@ async function appendEventTail(tx, meta, events, anchor, reuse) {
2135
2258
  let parentId = anchor.parentId;
2136
2259
  let nextSeq = anchor.nextSeq;
2137
2260
  const eventRows = [];
2261
+ const usageRows = [];
2138
2262
  const bridgeRows = [];
2139
2263
  for (const event of events) {
2140
2264
  const reusedId = reuse?.get(event.seq);
@@ -2142,7 +2266,7 @@ async function appendEventTail(tx, meta, events, anchor, reuse) {
2142
2266
  if (reusedId === void 0) {
2143
2267
  const { kind, role, name, actionId } = eventDimensions(event);
2144
2268
  const { data, surfaceOp: _surfaceOp, sourceEventSeqs: _sourceEventSeqs, ...envelope } = event;
2145
- eventRows.push({
2269
+ const row = {
2146
2270
  fEventId: eventId,
2147
2271
  fParentId: parentId,
2148
2272
  fType: event.type,
@@ -2156,7 +2280,10 @@ async function appendEventTail(tx, meta, events, anchor, reuse) {
2156
2280
  data
2157
2281
  }),
2158
2282
  fCreatedAt: event.time
2159
- });
2283
+ };
2284
+ eventRows.push(row);
2285
+ const usageRow = usageRowOf(row, meta.origin === "subagent");
2286
+ if (usageRow !== void 0) usageRows.push(usageRow);
2160
2287
  }
2161
2288
  const surfaceOp = event.surfaceOp === void 0 ? null : JSON.stringify(event.surfaceOp);
2162
2289
  bridgeRows.push({
@@ -2168,12 +2295,13 @@ async function appendEventTail(tx, meta, events, anchor, reuse) {
2168
2295
  parentId = eventId;
2169
2296
  nextSeq++;
2170
2297
  }
2171
- if (eventRows.length > 0) await tx.insertEvents(eventRows);
2298
+ if (eventRows.length > 0) await tx.insertEvents(eventRows, usageRows);
2172
2299
  await tx.insertBridges(bridgeRows);
2173
2300
  if (events.some((event) => event.type === "session/title")) await tx.refreshTitle(meta.id);
2174
2301
  return {
2175
2302
  headEventId: parentId,
2176
- headSequence: nextSeq - 1
2303
+ headSequence: nextSeq - 1,
2304
+ usageRows
2177
2305
  };
2178
2306
  }
2179
2307
  //#endregion