@morlay/ui-conversation-message-actions 0.0.19-alpha.3 → 0.0.19-alpha.4

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.
package/dist/testing.mjs CHANGED
@@ -1,9 +1,9 @@
1
- import { t as SESSION_EDITOR_PATH } from "./shared-LK3nImWW.mjs";
1
+ import { t as SESSION_EDITOR_PATH } from "./shared-BBbOTwy4.mjs";
2
2
  import { SessionEditor } from "./index.mjs";
3
3
  import { createRequire } from "node:module";
4
4
  import { Context, Service } from "@deepseek-ai/cordis";
5
5
  import { KNOWN_SESSION_EVENT_TYPES, SESSION_FORMAT_VERSION, Session, Session as Session$1, SessionId, SessionId as SessionIdBrand, SessionLogOffset, SessionSeq, SessionSeq as SessionSeq$1, SessionStore, foldSurface, interruptedTurnClosers, isSurfaceEvent, snapshotSessionEvent } from "@deepseek-ai/dsh-session";
6
- import { SessionBranch, SessionBranchError, balanceRewindPrefix, buildTimeline, rewindKeepLength } from "@morlay/session-branch";
6
+ import { SessionBranch, SessionBranchError, balanceRewindPrefix, rewindKeepLength } from "@morlay/session-branch";
7
7
  import crypto, { createHash, randomUUID } from "node:crypto";
8
8
  import { TokenMeter } from "@deepseek-ai/dsh-token-meter";
9
9
  import SessionProjectionRegistry from "@deepseek-ai/dsh-session-projection";
@@ -18966,7 +18966,36 @@ const SURFACE_EVENT_TYPES = /* @__PURE__ */ new Set([
18966
18966
  "tool/result"
18967
18967
  ]);
18968
18968
  const METERING_EVENT_TYPES = /* @__PURE__ */ new Set(["compaction/summary", "compaction/prune"]);
18969
+ /**
18970
+ * 升序的 surface 事件 seq 索引:读视图修复里的溯源(replace 的 `sourceEventSeqs` 与
18971
+ * metering 的 `shadowedSeqs`)共用这一份,避免每个 replace 各扫一遍全量事件。
18972
+ * 前提是输入按 seq 有序(`readLog` 与 live snapshot 都如此)。
18973
+ */
18974
+ function surfaceSeqIndex(events) {
18975
+ const seqs = [];
18976
+ for (const event of events) if (SURFACE_EVENT_TYPES.has(event.type)) seqs.push(event.seq);
18977
+ return seqs;
18978
+ }
18979
+ /** `[startSeq, endSeq]` 内的 surface seq(含端点),二分定位后切片。 */
18980
+ function surfaceSeqsInRange(seqs, startSeq, endSeq) {
18981
+ let low = 0;
18982
+ let high = seqs.length;
18983
+ while (low < high) {
18984
+ const mid = low + high >> 1;
18985
+ if (seqs[mid] < startSeq) low = mid + 1;
18986
+ else high = mid;
18987
+ }
18988
+ const from = low;
18989
+ high = seqs.length;
18990
+ while (low < high) {
18991
+ const mid = low + high >> 1;
18992
+ if (seqs[mid] <= endSeq) low = mid + 1;
18993
+ else high = mid;
18994
+ }
18995
+ return seqs.slice(from, low);
18996
+ }
18969
18997
  function recomputeReplaceProvenance(events) {
18998
+ const seqs = surfaceSeqIndex(events);
18970
18999
  for (let i = 0; i < events.length; i++) {
18971
19000
  const raw = events[i];
18972
19001
  const op = raw.surfaceOp;
@@ -18978,9 +19007,7 @@ function recomputeReplaceProvenance(events) {
18978
19007
  raw.sourceEventSeqs = meteringData.shadowedSeqs;
18979
19008
  continue;
18980
19009
  }
18981
- const refs = [];
18982
- for (const candidate of events) if (candidate.seq >= startSeq && candidate.seq <= endSeq && SURFACE_EVENT_TYPES.has(candidate.type)) refs.push(candidate.seq);
18983
- raw.sourceEventSeqs = refs;
19010
+ raw.sourceEventSeqs = surfaceSeqsInRange(seqs, startSeq, endSeq);
18984
19011
  }
18985
19012
  }
18986
19013
  function isEventSeqLike(value) {
@@ -19133,6 +19160,7 @@ function repairRequestHeaders(events) {
19133
19160
  }
19134
19161
  }
19135
19162
  function syncMeteringRanges(events) {
19163
+ const seqs = surfaceSeqIndex(events);
19136
19164
  for (let i = 1; i < events.length; i++) {
19137
19165
  const metering = events[i - 1];
19138
19166
  if (!METERING_EVENT_TYPES.has(metering.type)) continue;
@@ -19145,7 +19173,7 @@ function syncMeteringRanges(events) {
19145
19173
  start: startSeq,
19146
19174
  end: endSeq
19147
19175
  };
19148
- data.shadowedSeqs = events.filter((candidate) => candidate.seq >= startSeq && candidate.seq <= endSeq && SURFACE_EVENT_TYPES.has(candidate.type)).map((candidate) => candidate.seq);
19176
+ data.shadowedSeqs = surfaceSeqsInRange(seqs, startSeq, endSeq);
19149
19177
  }
19150
19178
  }
19151
19179
  const PTC_EVENT_RENAMES = {
@@ -23310,7 +23338,7 @@ const eventUsage = {
23310
23338
  }
23311
23339
  },
23312
23340
  f_created_at: {
23313
- type: "integer",
23341
+ type: "bigint",
23314
23342
  notNull: true
23315
23343
  },
23316
23344
  f_provider: { type: "text" },
@@ -23676,6 +23704,152 @@ function migrate$1(db, config) {
23676
23704
  return migrateSync(readMigrationFiles(config), db.session, config);
23677
23705
  }
23678
23706
  //#endregion
23707
+ //#region ../session-rdb/src/usage.ts
23708
+ const SESSION_USAGE_PATH = "/api/session.usage";
23709
+ const DAY_MS = 864e5;
23710
+ const ROLLING_DAYS = {
23711
+ "7d": 7,
23712
+ "30d": 30,
23713
+ "90d": 90
23714
+ };
23715
+ function localDayStart(now) {
23716
+ const start = new Date(now);
23717
+ start.setHours(0, 0, 0, 0);
23718
+ return start;
23719
+ }
23720
+ /**
23721
+ * 范围起点(含),不限时为 undefined。自然日 / 自然周按 host 的本地时区算,周边界是周一 00:00。
23722
+ * @param range - 语义键。
23723
+ * @param now - 当前时刻。
23724
+ * @returns 起点毫秒时间戳,或 undefined。
23725
+ */
23726
+ function resolveUsageSince(range, now) {
23727
+ switch (range) {
23728
+ case "all": return;
23729
+ case "day": return localDayStart(now).getTime();
23730
+ case "week": {
23731
+ const start = localDayStart(now);
23732
+ start.setDate(start.getDate() - (start.getDay() + 6) % 7);
23733
+ return start.getTime();
23734
+ }
23735
+ default: return now - ROLLING_DAYS[range] * DAY_MS;
23736
+ }
23737
+ }
23738
+ function parseUsageRange(value) {
23739
+ return value === "day" || value === "week" || value === "7d" || value === "30d" || value === "90d" ? value : "all";
23740
+ }
23741
+ /** 计入活动计数的事件类型(轮次 / 步骤 / 用户输入 / 工具调用)。 */
23742
+ const COUNTED_EVENT_TYPES = [
23743
+ "turn/start",
23744
+ "step/start",
23745
+ "user/message",
23746
+ "tool/call"
23747
+ ];
23748
+ /** 按事件类型把计数累加到活动指标上(表里按类型存,读的时候折成四项)。 */
23749
+ function addActivityCount(target, type, count) {
23750
+ switch (type) {
23751
+ case "turn/start":
23752
+ target.turns += count;
23753
+ return target;
23754
+ case "step/start":
23755
+ target.steps += count;
23756
+ return target;
23757
+ case "user/message":
23758
+ target.userInputs += count;
23759
+ return target;
23760
+ case "tool/call":
23761
+ target.toolCalls += count;
23762
+ return target;
23763
+ default: return target;
23764
+ }
23765
+ }
23766
+ /** 毫秒时间戳 → host 本地日(`YYYY-MM-DD`,与 SQL 的 localtime 口径一致)。 */
23767
+ function localDayKey(ms) {
23768
+ const date = new Date(ms);
23769
+ const pad = (value) => String(value).padStart(2, "0");
23770
+ return `${String(date.getFullYear())}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`;
23771
+ }
23772
+ function emptyTotals() {
23773
+ return {
23774
+ turns: 0,
23775
+ steps: 0,
23776
+ userInputs: 0,
23777
+ toolCalls: 0,
23778
+ inputTokens: 0,
23779
+ outputTokens: 0,
23780
+ cacheReadTokens: 0,
23781
+ reasoningTokens: 0,
23782
+ totalTokens: 0
23783
+ };
23784
+ }
23785
+ function addTokenTotals(target, row) {
23786
+ target.inputTokens += row.inputTokens;
23787
+ target.outputTokens += row.outputTokens;
23788
+ target.cacheReadTokens += row.cacheReadTokens;
23789
+ target.reasoningTokens += row.reasoningTokens;
23790
+ target.totalTokens += row.totalTokens;
23791
+ return target;
23792
+ }
23793
+ function addActivityTotals(target, row) {
23794
+ target.turns += row.turns;
23795
+ target.steps += row.steps;
23796
+ target.userInputs += row.userInputs;
23797
+ target.toolCalls += row.toolCalls;
23798
+ return target;
23799
+ }
23800
+ /** 把一行的整套指标累加进目标(后端合并「是否子代理」两组时用)。 */
23801
+ function addTotals(target, row) {
23802
+ addActivityTotals(target, row);
23803
+ addTokenTotals(target, row);
23804
+ return target;
23805
+ }
23806
+ /**
23807
+ * 用量统计通道:一次请求回报总览(含 subagent 拆分)、按天 × 模型的桶与按会话的行。
23808
+ * 聚合只算被会话引用的事件行——fork 共享行因此只计一次,已删会话留下的孤儿行不计。
23809
+ */
23810
+ function registerSessionUsage(ctx, persistence) {
23811
+ ctx.inject(["webServer", "connection"], (webCtx) => {
23812
+ const webServer = webCtx.webServer;
23813
+ const connection = webCtx.get("connection");
23814
+ return webCtx.effect(() => webServer.register({
23815
+ kind: "exact",
23816
+ path: SESSION_USAGE_PATH,
23817
+ handler: async (req, res) => {
23818
+ const rejection = connection.requestRejection(req);
23819
+ if (rejection !== void 0) {
23820
+ res.writeHead(rejection);
23821
+ res.end(rejection === 401 ? "unauthorized" : "forbidden");
23822
+ return;
23823
+ }
23824
+ if (req.method !== "POST") {
23825
+ res.writeHead(405, { "content-type": "application/json" });
23826
+ res.end(JSON.stringify({ error: "method not allowed" }));
23827
+ return;
23828
+ }
23829
+ const chunks = [];
23830
+ for await (const chunk of req) chunks.push(chunk);
23831
+ const raw = Buffer.concat(chunks).toString("utf8");
23832
+ let envelope = {};
23833
+ if (raw !== "") try {
23834
+ envelope = JSON.parse(raw);
23835
+ } catch {
23836
+ res.writeHead(400, { "content-type": "application/json" });
23837
+ res.end(JSON.stringify({ error: "request body is not JSON" }));
23838
+ return;
23839
+ }
23840
+ try {
23841
+ const report = await persistence.usageReport(resolveUsageSince(parseUsageRange(envelope.range), Date.now()));
23842
+ res.writeHead(200, { "content-type": "application/json" });
23843
+ res.end(JSON.stringify(report));
23844
+ } catch (error) {
23845
+ res.writeHead(500, { "content-type": "application/json" });
23846
+ res.end(JSON.stringify({ error: error instanceof Error ? error.message : "usage report failed" }));
23847
+ }
23848
+ }
23849
+ }), `session-rdb: ${SESSION_USAGE_PATH} route`);
23850
+ });
23851
+ }
23852
+ //#endregion
23679
23853
  //#region ../session-rdb/src/storage-takeover/repository.ts
23680
23854
  async function allRows(query) {
23681
23855
  const q = query;
@@ -23926,6 +24100,32 @@ function createStorageRepository(host) {
23926
24100
  }
23927
24101
  //#endregion
23928
24102
  //#region ../session-rdb/src/sqlite.ts
24103
+ /** 活动计数的派生表:可销毁重建(不在 drizzle schema / 迁移里,建表幂等)。 */
24104
+ function ensureEventCountsTable(db) {
24105
+ db.exec(`CREATE TABLE IF NOT EXISTS t_event_counts (
24106
+ f_id INTEGER PRIMARY KEY AUTOINCREMENT,
24107
+ f_session_id TEXT NOT NULL,
24108
+ f_day TEXT NOT NULL,
24109
+ f_type TEXT NOT NULL,
24110
+ f_count INTEGER NOT NULL DEFAULT 0
24111
+ );
24112
+ CREATE UNIQUE INDEX IF NOT EXISTS uq_event_counts_session_day_type
24113
+ ON t_event_counts(f_session_id, f_day, f_type);
24114
+ CREATE INDEX IF NOT EXISTS idx_event_counts_day ON t_event_counts(f_day);`);
24115
+ }
24116
+ /** 一次性回填活动计数(表为空时从事件表重算;幂等)。 */
24117
+ function backfillEventCounts(db) {
24118
+ if (db.prepare("SELECT count(*) AS n FROM t_event_counts").get().n > 0) return;
24119
+ db.exec(`INSERT OR IGNORE INTO t_event_counts (f_session_id, f_day, f_type, f_count)
24120
+ SELECT b.f_session_id,
24121
+ date(e.f_created_at / 1000, 'unixepoch', 'localtime'),
24122
+ e.f_type,
24123
+ count(*)
24124
+ FROM t_session_events b
24125
+ JOIN t_events e ON e.f_event_id = b.f_event_id
24126
+ WHERE e.f_type IN (${COUNTED_EVENT_TYPE_SQL$1})
24127
+ GROUP BY 1, 2, 3`);
24128
+ }
23929
24129
  /**
23930
24130
  * 一次性回填:旧库的用量从未写过 `t_event_usage`,建表后按事件行补一次(幂等:表非空即跳过)。
23931
24131
  */
@@ -23955,9 +24155,8 @@ function backfillEventUsage(db) {
23955
24155
  OR json_extract(e.f_data, '$.usage') IS NOT NULL)`);
23956
24156
  }
23957
24157
  /** SQL 的缺失求和是 NULL:没有 usage 字段的行使该字段计 0。 */
23958
- function totalsOf(row) {
24158
+ function tokenTotalsOf$1(row) {
23959
24159
  return {
23960
- events: row.events,
23961
24160
  inputTokens: row.input_tokens ?? 0,
23962
24161
  outputTokens: row.output_tokens ?? 0,
23963
24162
  cacheReadTokens: row.cache_read_tokens ?? 0,
@@ -23965,6 +24164,26 @@ function totalsOf(row) {
23965
24164
  totalTokens: row.total_tokens ?? 0
23966
24165
  };
23967
24166
  }
24167
+ /** 一行的整套指标:token 用量 + 活动计数(轮次 / 步骤 / 用户输入 / 工具调用)。 */
24168
+ function emptyActivity$1() {
24169
+ return {
24170
+ turns: 0,
24171
+ steps: 0,
24172
+ userInputs: 0,
24173
+ toolCalls: 0
24174
+ };
24175
+ }
24176
+ /** 有 token 用量的会话:活动计数与列表都限定在这一批会话里,口径一致。 */
24177
+ function hasUsageSession(column) {
24178
+ return `EXISTS (SELECT 1 FROM t_session_events ub
24179
+ JOIN t_event_usage uu ON uu.f_event_id = ub.f_event_id
24180
+ WHERE ub.f_session_id = ${column})`;
24181
+ }
24182
+ const SUBAGENT_OF_EVENT = `EXISTS (SELECT 1 FROM t_session_events sb
24183
+ JOIN t_sessions ss ON ss.f_session_id = sb.f_session_id
24184
+ WHERE sb.f_event_id = %EVENT% AND ss.f_origin = 'subagent')`;
24185
+ /** `IN (...)` 用的类型字面量(与 `COUNTED_EVENT_TYPES` 同源)。 */
24186
+ const COUNTED_EVENT_TYPE_SQL$1 = COUNTED_EVENT_TYPES.map((type) => `'${type}'`).join(", ");
23968
24187
  const sqliteMigrationsDir = fileURLToPath(new URL("../drizzle/sqlite/", import.meta.url));
23969
24188
  const sqliteTxQueues = /* @__PURE__ */ new Map();
23970
24189
  function enqueueSqliteTx(path, fn) {
@@ -23987,6 +24206,8 @@ async function openDatabase(path, journalMode, busyTimeout = DEFAULT_BUSY_TIMEOU
23987
24206
  const { user_version: onDisk } = db.prepare("PRAGMA user_version").get();
23988
24207
  if (onDisk === 2 && !hasMigrationsTable(db)) await baselineV2(db);
23989
24208
  migrate$1(dbx, { migrationsFolder: sqliteMigrationsDir });
24209
+ ensureEventCountsTable(db);
24210
+ backfillEventCounts(db);
23990
24211
  backfillEventUsage(db);
23991
24212
  dbx.insert(tPersistenceState).values({
23992
24213
  fSingleton: 1,
@@ -24127,10 +24348,13 @@ var SqliteBackend = class SqliteBackend {
24127
24348
  return (fromSequence === void 0 ? this.eventRows().where(eq(tSessionEvents.fSessionId, id)) : this.eventRows().where(and(eq(tSessionEvents.fSessionId, id), gte(tSessionEvents.fSequence, fromSequence)))).orderBy(tSessionEvents.fSequence).all();
24128
24349
  }
24129
24350
  async getEventTypeAt(id, sequence) {
24130
- return this.eventRows().where(and(eq(tSessionEvents.fSessionId, id), eq(tSessionEvents.fSequence, sequence))).get()?.fType;
24351
+ return this.db.select({ fType: tEvents.fType }).from(tSessionEvents).innerJoin(tEvents, eq(tSessionEvents.fEventId, tEvents.fEventId)).where(and(eq(tSessionEvents.fSessionId, id), eq(tSessionEvents.fSequence, sequence))).get()?.fType;
24131
24352
  }
24132
24353
  async getEventTypesBefore(id, beforeSequence, limit) {
24133
- return this.eventRows().where(and(eq(tSessionEvents.fSessionId, id), lt(tSessionEvents.fSequence, beforeSequence))).orderBy(desc(tSessionEvents.fSequence)).limit(limit).all();
24354
+ return this.db.select({
24355
+ fSequence: tSessionEvents.fSequence,
24356
+ fType: tEvents.fType
24357
+ }).from(tSessionEvents).innerJoin(tEvents, eq(tSessionEvents.fEventId, tEvents.fEventId)).where(and(eq(tSessionEvents.fSessionId, id), lt(tSessionEvents.fSequence, beforeSequence))).orderBy(desc(tSessionEvents.fSequence)).limit(limit).all();
24134
24358
  }
24135
24359
  async listSessions() {
24136
24360
  return this.db.select().from(tSessions).all();
@@ -24258,30 +24482,49 @@ var SqliteBackend = class SqliteBackend {
24258
24482
  const info = this.db.delete(tSessions).where(inArray(tSessions.fSessionId, ids)).run();
24259
24483
  return Number(info.changes);
24260
24484
  }
24261
- /** 用量聚合:只读 `t_event_usage`;可选只算 `sinceMs` 之后的事件行。 */
24485
+ /**
24486
+ * 用量聚合:token 用量沿 `t_event_usage` 的用量行,活动计数(轮次 / 步骤 / 用户输入 / 工具调用)
24487
+ * 沿 `t_events` 的事件类型数——两者限定在**有 token 用量的会话**里,时间范围各自按自己的
24488
+ * `f_created_at`(同一时刻写入,口径一致)。
24489
+ */
24262
24490
  async usageReport(sinceMs) {
24263
- const sinceClause = sinceMs === void 0 ? "" : " AND u.f_created_at >= ?";
24264
- const sinceParams = sinceMs === void 0 ? [] : [sinceMs];
24491
+ const usageSince = sinceMs === void 0 ? "" : " AND u.f_created_at >= ?";
24492
+ const params = sinceMs === void 0 ? [] : [sinceMs];
24493
+ const daySince = sinceMs === void 0 ? "" : " AND c.f_day >= ?";
24494
+ const dayParams = sinceMs === void 0 ? [] : [localDayKey(sinceMs)];
24495
+ const subagentOfUsage = SUBAGENT_OF_EVENT.replace("%EVENT%", "u.f_event_id");
24265
24496
  const buckets = this.db.$client.prepare(`SELECT date(u.f_created_at / 1000, 'unixepoch', 'localtime') AS day,
24266
24497
  u.f_provider AS provider,
24267
24498
  u.f_model AS model,
24268
- EXISTS (SELECT 1 FROM t_session_events sb
24269
- JOIN t_sessions ss ON ss.f_session_id = sb.f_session_id
24270
- WHERE sb.f_event_id = u.f_event_id AND ss.f_origin = 'subagent') AS subagent,
24271
- count(*) AS events,
24499
+ ${subagentOfUsage} AS subagent,
24500
+ sum(u.f_input_tokens) AS input_tokens,
24501
+ sum(u.f_output_tokens) AS output_tokens,
24502
+ sum(u.f_cache_read_tokens) AS cache_read_tokens,
24503
+ sum(u.f_reasoning_tokens) AS reasoning_tokens,
24504
+ sum(u.f_total_tokens) AS total_tokens
24505
+ FROM t_event_usage u
24506
+ WHERE EXISTS (SELECT 1 FROM t_session_events rb WHERE rb.f_event_id = u.f_event_id)${usageSince}
24507
+ GROUP BY day, provider, model, subagent`).all(...params);
24508
+ const tokenScope = this.db.$client.prepare(`SELECT ${subagentOfUsage} AS subagent,
24272
24509
  sum(u.f_input_tokens) AS input_tokens,
24273
24510
  sum(u.f_output_tokens) AS output_tokens,
24274
24511
  sum(u.f_cache_read_tokens) AS cache_read_tokens,
24275
24512
  sum(u.f_reasoning_tokens) AS reasoning_tokens,
24276
24513
  sum(u.f_total_tokens) AS total_tokens
24277
24514
  FROM t_event_usage u
24278
- WHERE EXISTS (SELECT 1 FROM t_session_events rb WHERE rb.f_event_id = u.f_event_id)${sinceClause}
24279
- GROUP BY day, provider, model, subagent`).all(...sinceParams);
24280
- const sessions = this.db.$client.prepare(`SELECT b.f_session_id AS session_id,
24515
+ WHERE EXISTS (SELECT 1 FROM t_session_events rb WHERE rb.f_event_id = u.f_event_id)${usageSince}
24516
+ GROUP BY subagent`).all(...params);
24517
+ const activityScope = this.db.$client.prepare(`SELECT (s.f_origin = 'subagent') AS subagent,
24518
+ c.f_type AS f_type,
24519
+ sum(c.f_count) AS n
24520
+ FROM t_event_counts c
24521
+ JOIN t_sessions s ON s.f_session_id = c.f_session_id
24522
+ WHERE ${hasUsageSession("c.f_session_id")}${daySince}
24523
+ GROUP BY subagent, c.f_type`).all(...dayParams);
24524
+ const tokenSessions = this.db.$client.prepare(`SELECT b.f_session_id AS session_id,
24281
24525
  s.f_title AS title,
24282
24526
  (s.f_origin = 'subagent') AS subagent,
24283
24527
  (s.f_archived_at IS NOT NULL) AS archived,
24284
- count(*) AS events,
24285
24528
  sum(u.f_input_tokens) AS input_tokens,
24286
24529
  sum(u.f_output_tokens) AS output_tokens,
24287
24530
  sum(u.f_cache_read_tokens) AS cache_read_tokens,
@@ -24290,25 +24533,69 @@ var SqliteBackend = class SqliteBackend {
24290
24533
  FROM t_session_events b
24291
24534
  JOIN t_event_usage u ON u.f_event_id = b.f_event_id
24292
24535
  JOIN t_sessions s ON s.f_session_id = b.f_session_id
24293
- WHERE 1 = 1${sinceClause}
24294
- GROUP BY b.f_session_id, s.f_title, s.f_origin, s.f_archived_at`).all(...sinceParams);
24536
+ WHERE 1 = 1${usageSince}
24537
+ GROUP BY b.f_session_id, s.f_title, s.f_origin, s.f_archived_at`).all(...params);
24538
+ const activitySessions = this.db.$client.prepare(`SELECT c.f_session_id AS session_id,
24539
+ c.f_type AS f_type,
24540
+ sum(c.f_count) AS n
24541
+ FROM t_event_counts c
24542
+ WHERE ${hasUsageSession("c.f_session_id")}${daySince}
24543
+ GROUP BY c.f_session_id, c.f_type`).all(...dayParams);
24544
+ const human = emptyTotals();
24545
+ const subagent = emptyTotals();
24546
+ for (const row of tokenScope) addTokenTotals(row.subagent === 1 ? subagent : human, tokenTotalsOf$1(row));
24547
+ for (const row of activityScope) addActivityCount(row.subagent === 1 ? subagent : human, row.f_type, row.n ?? 0);
24548
+ const totals = addTotals(addTotals(emptyTotals(), human), subagent);
24549
+ const activityBySession = /* @__PURE__ */ new Map();
24550
+ for (const row of activitySessions) {
24551
+ const target = activityBySession.get(row.session_id) ?? emptyActivity$1();
24552
+ addActivityCount(target, row.f_type, row.n ?? 0);
24553
+ activityBySession.set(row.session_id, target);
24554
+ }
24295
24555
  return {
24556
+ totals,
24557
+ subagent,
24558
+ human,
24296
24559
  buckets: buckets.map((row) => ({
24297
24560
  day: row.day,
24298
24561
  provider: row.provider,
24299
24562
  model: row.model,
24300
24563
  subagent: row.subagent === 1,
24301
- ...totalsOf(row)
24564
+ ...tokenTotalsOf$1(row)
24302
24565
  })),
24303
- sessions: sessions.map((row) => ({
24566
+ sessions: tokenSessions.map((row) => ({
24304
24567
  sessionId: row.session_id,
24305
24568
  title: row.title,
24306
24569
  subagent: row.subagent === 1,
24307
24570
  archived: row.archived === 1,
24308
- ...totalsOf(row)
24571
+ ...tokenTotalsOf$1(row),
24572
+ ...activityBySession.get(row.session_id) ?? emptyActivity$1()
24309
24573
  }))
24310
24574
  };
24311
24575
  }
24576
+ /** 活动计数旁路累加:派生表 `t_event_counts`,不在写事务里(失败可丢,表可销毁重建)。 */
24577
+ async incrementEventCounts(id, buckets) {
24578
+ if (buckets.length === 0) return;
24579
+ const statement = this.db.$client.prepare(`INSERT INTO t_event_counts (f_session_id, f_day, f_type, f_count) VALUES (?, ?, ?, ?)
24580
+ ON CONFLICT(f_session_id, f_day, f_type) DO UPDATE SET f_count = f_count + excluded.f_count`);
24581
+ for (const bucket of buckets) statement.run(id, bucket.day, bucket.type, bucket.count);
24582
+ }
24583
+ /** 按会话重算活动计数(rewind / fork 之后):先删该会话的行,再从事件表重算。 */
24584
+ async rebuildEventCounts(id) {
24585
+ this.db.$client.prepare("DELETE FROM t_event_counts WHERE f_session_id = ?").run(id);
24586
+ this.db.$client.prepare(`INSERT INTO t_event_counts (f_session_id, f_day, f_type, f_count)
24587
+ SELECT b.f_session_id,
24588
+ date(e.f_created_at / 1000, 'unixepoch', 'localtime'),
24589
+ e.f_type,
24590
+ count(*)
24591
+ FROM t_session_events b
24592
+ JOIN t_events e ON e.f_event_id = b.f_event_id
24593
+ WHERE b.f_session_id = ? AND e.f_type IN (${COUNTED_EVENT_TYPE_SQL$1})
24594
+ GROUP BY 1, 2, 3`).run(id);
24595
+ }
24596
+ async deleteEventCounts(id) {
24597
+ this.db.$client.prepare("DELETE FROM t_event_counts WHERE f_session_id = ?").run(id);
24598
+ }
24312
24599
  async vacuum() {
24313
24600
  await enqueueSqliteTx(this.dbPath, async () => {
24314
24601
  this.db.$client.exec("VACUUM");
@@ -24337,6 +24624,13 @@ async function migrate(db, config) {
24337
24624
  //#endregion
24338
24625
  //#region ../session-rdb/src/postgres.ts
24339
24626
  const postgresMigrationsDir = fileURLToPath(new URL("../drizzle/postgres/", import.meta.url));
24627
+ const pgWriteQueues = /* @__PURE__ */ new Map();
24628
+ /** 按介质(连接串 + schema)串行化写事务,与 SQLite 侧的 `enqueueSqliteTx` 同形。 */
24629
+ function enqueuePgWrite(key, fn) {
24630
+ const run = (pgWriteQueues.get(key) ?? Promise.resolve()).then(fn);
24631
+ pgWriteQueues.set(key, run.then(() => void 0, () => void 0));
24632
+ return run;
24633
+ }
24340
24634
  /** pg 的 text 列窄化:非字符串(含 null)都不当作文本。 */
24341
24635
  function text(value) {
24342
24636
  return typeof value === "string" ? value : null;
@@ -24347,9 +24641,8 @@ function numeric(value) {
24347
24641
  return Number.isFinite(parsed) ? parsed : 0;
24348
24642
  }
24349
24643
  /** SQL 的缺失求和是 NULL:没有 usage 字段的行使该字段计 0。 */
24350
- function totalsOfRow(row) {
24644
+ function tokenTotalsOf(row) {
24351
24645
  return {
24352
- events: numeric(row["events"]),
24353
24646
  inputTokens: numeric(row["input_tokens"]),
24354
24647
  outputTokens: numeric(row["output_tokens"]),
24355
24648
  cacheReadTokens: numeric(row["cache_read_tokens"]),
@@ -24357,6 +24650,16 @@ function totalsOfRow(row) {
24357
24650
  totalTokens: numeric(row["total_tokens"])
24358
24651
  };
24359
24652
  }
24653
+ function emptyActivity() {
24654
+ return {
24655
+ turns: 0,
24656
+ steps: 0,
24657
+ userInputs: 0,
24658
+ toolCalls: 0
24659
+ };
24660
+ }
24661
+ /** `IN (...)` 用的类型字面量(与 `COUNTED_EVENT_TYPES` 同源)。 */
24662
+ const COUNTED_EVENT_TYPE_SQL = COUNTED_EVENT_TYPES.map((type) => `'${type}'`).join(", ");
24360
24663
  var PostgresBackend = class PostgresBackend {
24361
24664
  db;
24362
24665
  options;
@@ -24377,9 +24680,10 @@ var PostgresBackend = class PostgresBackend {
24377
24680
  this.rejectOpened = reject;
24378
24681
  });
24379
24682
  this.opened.catch(() => {});
24683
+ const writeQueueKey = options.identityBase;
24380
24684
  this.storage = createStorageRepository({
24381
24685
  db: () => this.opened.then(() => this.txOverride ?? this.db),
24382
- writeAtomically: (fn) => this.db.transaction(async (tx) => {
24686
+ writeAtomically: (fn) => enqueuePgWrite(writeQueueKey, () => this.db.transaction(async (tx) => {
24383
24687
  const previous = this.txOverride;
24384
24688
  this.txOverride = tx;
24385
24689
  try {
@@ -24387,7 +24691,7 @@ var PostgresBackend = class PostgresBackend {
24387
24691
  } finally {
24388
24692
  this.txOverride = previous;
24389
24693
  }
24390
- }, { isolationLevel: "serializable" }),
24694
+ }, { isolationLevel: "serializable" })),
24391
24695
  tables: this.tables
24392
24696
  });
24393
24697
  }
@@ -24407,6 +24711,8 @@ var PostgresBackend = class PostgresBackend {
24407
24711
  if (await this.readMeta(this.db, "schema_version") === "2") await this.baselineV2();
24408
24712
  }
24409
24713
  await migrate(this.db, { migrationsFolder: postgresMigrationsDir });
24714
+ await this.ensureEventCountsTable();
24715
+ await this.backfillEventCounts();
24410
24716
  await this.backfillEventUsage();
24411
24717
  const storeId = await this.db.transaction(async (tx) => {
24412
24718
  await tx.insert(this.tables["t_persistence_state"]).values({
@@ -24447,11 +24753,14 @@ var PostgresBackend = class PostgresBackend {
24447
24753
  }
24448
24754
  async getEventTypeAt(id, sequence) {
24449
24755
  const table = this.tables["t_session_events"];
24450
- return (await this.eventRows(this.db).where(and(eq(table.fSessionId, id), eq(table.fSequence, sequence))).limit(1).execute())[0]?.fType;
24756
+ return (await this.db.select({ fType: this.tables["t_events"].fType }).from(table).innerJoin(this.tables["t_events"], eq(table.fEventId, this.tables["t_events"].fEventId)).where(and(eq(table.fSessionId, id), eq(table.fSequence, sequence))).limit(1).execute())[0]?.fType;
24451
24757
  }
24452
24758
  async getEventTypesBefore(id, beforeSequence, limit) {
24453
24759
  const table = this.tables["t_session_events"];
24454
- return await this.eventRows(this.db).where(and(eq(table.fSessionId, id), lt(table.fSequence, beforeSequence))).orderBy(desc(table.fSequence)).limit(limit).execute();
24760
+ return await this.db.select({
24761
+ fSequence: table.fSequence,
24762
+ fType: this.tables["t_events"].fType
24763
+ }).from(table).innerJoin(this.tables["t_events"], eq(table.fEventId, this.tables["t_events"].fEventId)).where(and(eq(table.fSessionId, id), lt(table.fSequence, beforeSequence))).orderBy(desc(table.fSequence)).limit(limit).execute();
24455
24764
  }
24456
24765
  async listSessions() {
24457
24766
  return this.db.select().from(this.tables["t_sessions"]).execute();
@@ -24609,61 +24918,176 @@ var PostgresBackend = class PostgresBackend {
24609
24918
  }
24610
24919
  /** 用量聚合:与 SQLite 侧同形,读 `t_event_usage`,数值列回来是字符串。 */
24611
24920
  async usageReport(sinceMs) {
24612
- const sinceClause = sinceMs === void 0 ? sql`` : sql` AND u.f_created_at >= ${sinceMs}`;
24921
+ const usageSince = sinceMs === void 0 ? sql`` : sql` AND u.f_created_at >= ${sinceMs}`;
24922
+ const daySince = sinceMs === void 0 ? sql`` : sql` AND c.f_day >= ${localDayKey(sinceMs)}`;
24613
24923
  const tEventUsage = this.tables["t_event_usage"];
24614
24924
  const tSessionEvents = this.tables["t_session_events"];
24615
24925
  const tSessions = this.tables["t_sessions"];
24926
+ const eventCountsTable = this.qualifiedTable("t_event_counts");
24927
+ const subagentOfUsage = sql`EXISTS (SELECT 1 FROM ${tSessionEvents} sb
24928
+ JOIN ${tSessions} ss ON ss.f_session_id = sb.f_session_id
24929
+ WHERE sb.f_event_id = u.f_event_id AND ss.f_origin = 'subagent')`;
24930
+ const subagentOfCounts = sql`EXISTS (SELECT 1 FROM ${tSessionEvents} sb
24931
+ JOIN ${tSessions} ss ON ss.f_session_id = sb.f_session_id
24932
+ WHERE sb.f_session_id = c.f_session_id AND ss.f_origin = 'subagent')`;
24933
+ const hasUsageSession = (column) => sql`EXISTS (SELECT 1 FROM ${tSessionEvents} ub
24934
+ JOIN ${tEventUsage} uu ON uu.f_event_id = ub.f_event_id
24935
+ WHERE ub.f_session_id = ${sql.raw(column)})`;
24936
+ const tokenColumns = sql.raw(`sum(u.f_input_tokens) AS input_tokens,
24937
+ sum(u.f_output_tokens) AS output_tokens,
24938
+ sum(u.f_cache_read_tokens) AS cache_read_tokens,
24939
+ sum(u.f_reasoning_tokens) AS reasoning_tokens,
24940
+ sum(u.f_total_tokens) AS total_tokens`);
24616
24941
  const buckets = await this.db.execute(sql`
24617
24942
  SELECT to_char(to_timestamp(u.f_created_at / 1000.0), 'YYYY-MM-DD') AS day,
24618
24943
  u.f_provider AS provider,
24619
24944
  u.f_model AS model,
24620
- EXISTS (SELECT 1 FROM ${tSessionEvents} sb
24621
- JOIN ${tSessions} ss ON ss.f_session_id = sb.f_session_id
24622
- WHERE sb.f_event_id = u.f_event_id AND ss.f_origin = 'subagent') AS subagent,
24623
- count(*) AS events,
24624
- sum(u.f_input_tokens) AS input_tokens,
24625
- sum(u.f_output_tokens) AS output_tokens,
24626
- sum(u.f_cache_read_tokens) AS cache_read_tokens,
24627
- sum(u.f_reasoning_tokens) AS reasoning_tokens,
24628
- sum(u.f_total_tokens) AS total_tokens
24945
+ ${subagentOfUsage} AS subagent,
24946
+ ${tokenColumns}
24629
24947
  FROM ${tEventUsage} u
24630
- WHERE EXISTS (SELECT 1 FROM ${tSessionEvents} rb WHERE rb.f_event_id = u.f_event_id)${sinceClause}
24948
+ WHERE EXISTS (SELECT 1 FROM ${tSessionEvents} rb WHERE rb.f_event_id = u.f_event_id)${usageSince}
24631
24949
  GROUP BY 1, 2, 3, 4
24632
24950
  `);
24633
- const sessions = await this.db.execute(sql`
24951
+ const tokenScope = await this.db.execute(sql`
24952
+ SELECT ${subagentOfUsage} AS subagent,
24953
+ ${tokenColumns}
24954
+ FROM ${tEventUsage} u
24955
+ WHERE EXISTS (SELECT 1 FROM ${tSessionEvents} rb WHERE rb.f_event_id = u.f_event_id)${usageSince}
24956
+ GROUP BY 1
24957
+ `);
24958
+ const activityScope = await this.db.execute(sql`
24959
+ SELECT ${subagentOfCounts} AS subagent,
24960
+ c.f_type AS f_type,
24961
+ sum(c.f_count) AS n
24962
+ FROM ${sql.raw(eventCountsTable)} c
24963
+ JOIN ${tSessions} s ON s.f_session_id = c.f_session_id
24964
+ WHERE ${hasUsageSession("c.f_session_id")}${daySince}
24965
+ GROUP BY 1, 2
24966
+ `);
24967
+ const tokenSessions = await this.db.execute(sql`
24634
24968
  SELECT b.f_session_id AS session_id,
24635
24969
  s.f_title AS title,
24636
24970
  (s.f_origin = 'subagent') AS subagent,
24637
24971
  (s.f_archived_at IS NOT NULL) AS archived,
24638
- count(*) AS events,
24639
- sum(u.f_input_tokens) AS input_tokens,
24640
- sum(u.f_output_tokens) AS output_tokens,
24641
- sum(u.f_cache_read_tokens) AS cache_read_tokens,
24642
- sum(u.f_reasoning_tokens) AS reasoning_tokens,
24643
- sum(u.f_total_tokens) AS total_tokens
24972
+ ${tokenColumns}
24644
24973
  FROM ${tSessionEvents} b
24645
24974
  JOIN ${tEventUsage} u ON u.f_event_id = b.f_event_id
24646
24975
  JOIN ${tSessions} s ON s.f_session_id = b.f_session_id
24647
- WHERE 1 = 1${sinceClause}
24976
+ WHERE 1 = 1${usageSince}
24648
24977
  GROUP BY b.f_session_id, s.f_title, s.f_origin, s.f_archived_at
24649
24978
  `);
24979
+ const activitySessions = await this.db.execute(sql`
24980
+ SELECT c.f_session_id AS session_id,
24981
+ c.f_type AS f_type,
24982
+ sum(c.f_count) AS n
24983
+ FROM ${sql.raw(eventCountsTable)} c
24984
+ WHERE ${hasUsageSession("c.f_session_id")}${daySince}
24985
+ GROUP BY c.f_session_id, c.f_type
24986
+ `);
24987
+ const human = emptyTotals();
24988
+ const subagent = emptyTotals();
24989
+ for (const row of tokenScope.rows) addTokenTotals(row["subagent"] === true ? subagent : human, tokenTotalsOf(row));
24990
+ for (const row of activityScope.rows) addActivityCount(row["subagent"] === true ? subagent : human, String(row["f_type"]), numeric(row["n"]));
24991
+ const totals = addTotals(addTotals(emptyTotals(), human), subagent);
24992
+ const activityBySession = /* @__PURE__ */ new Map();
24993
+ for (const row of activitySessions.rows) {
24994
+ const key = String(row["session_id"]);
24995
+ const target = activityBySession.get(key) ?? emptyActivity();
24996
+ addActivityCount(target, String(row["f_type"]), numeric(row["n"]));
24997
+ activityBySession.set(key, target);
24998
+ }
24650
24999
  return {
25000
+ totals,
25001
+ subagent,
25002
+ human,
24651
25003
  buckets: buckets.rows.map((row) => ({
24652
25004
  day: String(row["day"]),
24653
25005
  provider: text(row["provider"]),
24654
25006
  model: text(row["model"]),
24655
25007
  subagent: row["subagent"] === true,
24656
- ...totalsOfRow(row)
25008
+ ...tokenTotalsOf(row)
24657
25009
  })),
24658
- sessions: sessions.rows.map((row) => ({
25010
+ sessions: tokenSessions.rows.map((row) => ({
24659
25011
  sessionId: String(row["session_id"]),
24660
25012
  title: text(row["title"]),
24661
25013
  subagent: row["subagent"] === true,
24662
25014
  archived: row["archived"] === true,
24663
- ...totalsOfRow(row)
25015
+ ...tokenTotalsOf(row),
25016
+ ...activityBySession.get(String(row["session_id"])) ?? emptyActivity()
24664
25017
  }))
24665
25018
  };
24666
25019
  }
25020
+ /** 表名限定到配置的 schema(派生表不进 drizzle schema / 迁移,建表幂等)。 */
25021
+ qualifiedTable(name) {
25022
+ const schema = this.options.schema ?? "public";
25023
+ return schema === "public" ? name : `"${schema}".${name}`;
25024
+ }
25025
+ async ensureEventCountsTable() {
25026
+ const table = this.qualifiedTable("t_event_counts");
25027
+ await this.db.execute(sql.raw(`CREATE TABLE IF NOT EXISTS ${table} (
25028
+ f_id serial PRIMARY KEY,
25029
+ f_session_id text NOT NULL,
25030
+ f_day text NOT NULL,
25031
+ f_type text NOT NULL,
25032
+ f_count integer NOT NULL DEFAULT 0
25033
+ )`));
25034
+ 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)`));
25035
+ await this.db.execute(sql.raw(`CREATE INDEX IF NOT EXISTS idx_event_counts_day ON ${table}(f_day)`));
25036
+ }
25037
+ /** 一次性回填活动计数(表为空时从事件表重算;幂等)。 */
25038
+ async backfillEventCounts() {
25039
+ const table = this.qualifiedTable("t_event_counts");
25040
+ const existing = await this.db.execute(sql.raw(`SELECT count(*) AS n FROM ${table}`));
25041
+ if (Number(existing.rows[0]?.["n"] ?? 0) > 0) return;
25042
+ const tSessionEvents = this.tables["t_session_events"];
25043
+ const tEvents = this.tables["t_events"];
25044
+ await this.db.execute(sql`
25045
+ INSERT INTO ${sql.raw(table)} (f_session_id, f_day, f_type, f_count)
25046
+ SELECT ${tSessionEvents}.f_session_id,
25047
+ to_char(to_timestamp(${tEvents}.f_created_at / 1000.0), 'YYYY-MM-DD'),
25048
+ ${tEvents}.f_type,
25049
+ count(*)
25050
+ FROM ${tSessionEvents}
25051
+ JOIN ${tEvents} ON ${tEvents}.f_event_id = ${tSessionEvents}.f_event_id
25052
+ WHERE ${tEvents}.f_type IN (${sql.raw(COUNTED_EVENT_TYPE_SQL)})
25053
+ GROUP BY 1, 2, 3
25054
+ ON CONFLICT DO NOTHING
25055
+ `);
25056
+ }
25057
+ /** 活动计数旁路累加:派生表,不在写事务里(失败可丢,表可销毁重建)。 */
25058
+ async incrementEventCounts(id, buckets) {
25059
+ if (buckets.length === 0) return;
25060
+ const table = this.qualifiedTable("t_event_counts");
25061
+ for (const bucket of buckets) await this.db.execute(sql`
25062
+ INSERT INTO ${sql.raw(table)} (f_session_id, f_day, f_type, f_count)
25063
+ VALUES (${id}, ${bucket.day}, ${bucket.type}, ${bucket.count})
25064
+ ON CONFLICT (f_session_id, f_day, f_type)
25065
+ DO UPDATE SET f_count = ${sql.raw(`${table}.f_count`)} + EXCLUDED.f_count
25066
+ `);
25067
+ }
25068
+ /** 按会话重算活动计数(rewind / fork 之后):先删该会话的行,再从事件表重算。 */
25069
+ async rebuildEventCounts(id) {
25070
+ const table = this.qualifiedTable("t_event_counts");
25071
+ const tSessionEvents = this.tables["t_session_events"];
25072
+ const tEvents = this.tables["t_events"];
25073
+ await this.db.execute(sql`DELETE FROM ${sql.raw(table)} WHERE f_session_id = ${id}`);
25074
+ await this.db.execute(sql`
25075
+ INSERT INTO ${sql.raw(table)} (f_session_id, f_day, f_type, f_count)
25076
+ SELECT ${tSessionEvents}.f_session_id,
25077
+ to_char(to_timestamp(${tEvents}.f_created_at / 1000.0), 'YYYY-MM-DD'),
25078
+ ${tEvents}.f_type,
25079
+ count(*)
25080
+ FROM ${tSessionEvents}
25081
+ JOIN ${tEvents} ON ${tEvents}.f_event_id = ${tSessionEvents}.f_event_id
25082
+ WHERE ${tSessionEvents}.f_session_id = ${id}
25083
+ AND ${tEvents}.f_type IN (${sql.raw(COUNTED_EVENT_TYPE_SQL)})
25084
+ GROUP BY 1, 2, 3
25085
+ `);
25086
+ }
25087
+ async deleteEventCounts(id) {
25088
+ const table = this.qualifiedTable("t_event_counts");
25089
+ await this.db.execute(sql`DELETE FROM ${sql.raw(table)} WHERE f_session_id = ${id}`);
25090
+ }
24667
25091
  async getPrevBridge(exec, id, sequence) {
24668
25092
  return (await exec.select({
24669
25093
  fEventId: this.tables["t_session_events"].fEventId,
@@ -24884,6 +25308,7 @@ var SessionBranchRdbProvider = class {
24884
25308
  } finally {
24885
25309
  await handle.close();
24886
25310
  }
25311
+ await this.persistence.rebuildEventCounts(childId);
24887
25312
  return childId;
24888
25313
  }
24889
25314
  async rewind(id, toBoundary, signal) {
@@ -24956,6 +25381,7 @@ var SessionBranchRdbProvider = class {
24956
25381
  headSeq: keepLength - 1,
24957
25382
  snapshotEvents: () => []
24958
25383
  });
25384
+ await this.persistence.rebuildEventCounts(id);
24959
25385
  return {
24960
25386
  header: rowToMeta(row),
24961
25387
  revision: await internals.readStoredRevision(id)
@@ -25058,16 +25484,6 @@ var SessionBranchRdb = class extends SessionBranch {
25058
25484
  rewind(id, toBoundary, signal) {
25059
25485
  return this.provider.rewind(id, toBoundary, signal);
25060
25486
  }
25061
- async timeline(sessionId, signal) {
25062
- const persistence = this.ctx.sessionPersistence;
25063
- const snapshots = await persistence.listSnapshots(signal);
25064
- const readOwnEvents = async (id, fromSeq, s) => {
25065
- const live = this.ctx.sessions.get(id);
25066
- if (live !== void 0) return live.snapshotEvents().slice(fromSeq);
25067
- return (await persistence.internals().readFrom(id, fromSeq, s)).events;
25068
- };
25069
- return buildTimeline(snapshots, readOwnEvents, sessionId, signal);
25070
- }
25071
25487
  };
25072
25488
  //#endregion
25073
25489
  //#region ../../../node_modules/.pnpm/fflate@0.8.3/node_modules/fflate/esm/index.mjs
@@ -26632,118 +27048,6 @@ function registerSessionGc(ctx, persistence) {
26632
27048
  });
26633
27049
  }
26634
27050
  //#endregion
26635
- //#region ../session-rdb/src/usage.ts
26636
- const SESSION_USAGE_PATH = "/api/session.usage";
26637
- const DAY_MS = 864e5;
26638
- const ROLLING_DAYS = {
26639
- "7d": 7,
26640
- "30d": 30,
26641
- "90d": 90
26642
- };
26643
- function localDayStart(now) {
26644
- const start = new Date(now);
26645
- start.setHours(0, 0, 0, 0);
26646
- return start;
26647
- }
26648
- /**
26649
- * 范围起点(含),不限时为 undefined。自然日 / 自然周按 host 的本地时区算,周边界是周一 00:00。
26650
- * @param range - 语义键。
26651
- * @param now - 当前时刻。
26652
- * @returns 起点毫秒时间戳,或 undefined。
26653
- */
26654
- function resolveUsageSince(range, now) {
26655
- switch (range) {
26656
- case "all": return;
26657
- case "day": return localDayStart(now).getTime();
26658
- case "week": {
26659
- const start = localDayStart(now);
26660
- start.setDate(start.getDate() - (start.getDay() + 6) % 7);
26661
- return start.getTime();
26662
- }
26663
- default: return now - ROLLING_DAYS[range] * DAY_MS;
26664
- }
26665
- }
26666
- function parseUsageRange(value) {
26667
- return value === "day" || value === "week" || value === "7d" || value === "30d" || value === "90d" ? value : "all";
26668
- }
26669
- function emptyTotals() {
26670
- return {
26671
- events: 0,
26672
- inputTokens: 0,
26673
- outputTokens: 0,
26674
- cacheReadTokens: 0,
26675
- reasoningTokens: 0,
26676
- totalTokens: 0
26677
- };
26678
- }
26679
- /** 折叠一组行(桶或会话行)的用量。 */
26680
- function sumUsage(rows) {
26681
- const totals = emptyTotals();
26682
- for (const row of rows) {
26683
- totals.events += row.events;
26684
- totals.inputTokens += row.inputTokens;
26685
- totals.outputTokens += row.outputTokens;
26686
- totals.cacheReadTokens += row.cacheReadTokens;
26687
- totals.reasoningTokens += row.reasoningTokens;
26688
- totals.totalTokens += row.totalTokens;
26689
- }
26690
- return totals;
26691
- }
26692
- /**
26693
- * 用量统计通道:一次请求回报总览(含 subagent 拆分)、按天 × 模型的桶与按会话的行。
26694
- * 聚合只算被会话引用的事件行——fork 共享行因此只计一次,已删会话留下的孤儿行不计。
26695
- */
26696
- function registerSessionUsage(ctx, persistence) {
26697
- ctx.inject(["webServer", "connection"], (webCtx) => {
26698
- const webServer = webCtx.webServer;
26699
- const connection = webCtx.get("connection");
26700
- return webCtx.effect(() => webServer.register({
26701
- kind: "exact",
26702
- path: SESSION_USAGE_PATH,
26703
- handler: async (req, res) => {
26704
- const rejection = connection.requestRejection(req);
26705
- if (rejection !== void 0) {
26706
- res.writeHead(rejection);
26707
- res.end(rejection === 401 ? "unauthorized" : "forbidden");
26708
- return;
26709
- }
26710
- if (req.method !== "POST") {
26711
- res.writeHead(405, { "content-type": "application/json" });
26712
- res.end(JSON.stringify({ error: "method not allowed" }));
26713
- return;
26714
- }
26715
- const chunks = [];
26716
- for await (const chunk of req) chunks.push(chunk);
26717
- const raw = Buffer.concat(chunks).toString("utf8");
26718
- let envelope = {};
26719
- if (raw !== "") try {
26720
- envelope = JSON.parse(raw);
26721
- } catch {
26722
- res.writeHead(400, { "content-type": "application/json" });
26723
- res.end(JSON.stringify({ error: "request body is not JSON" }));
26724
- return;
26725
- }
26726
- try {
26727
- const aggregate = await persistence.usageReport(resolveUsageSince(parseUsageRange(envelope.range), Date.now()));
26728
- const totals = sumUsage(aggregate.buckets);
26729
- const subagent = sumUsage(aggregate.buckets.filter((bucket) => bucket.subagent));
26730
- const human = sumUsage(aggregate.buckets.filter((bucket) => !bucket.subagent));
26731
- res.writeHead(200, { "content-type": "application/json" });
26732
- res.end(JSON.stringify({
26733
- totals,
26734
- subagent,
26735
- human,
26736
- ...aggregate
26737
- }));
26738
- } catch (error) {
26739
- res.writeHead(500, { "content-type": "application/json" });
26740
- res.end(JSON.stringify({ error: error instanceof Error ? error.message : "usage report failed" }));
26741
- }
26742
- }
26743
- }), `session-rdb: ${SESSION_USAGE_PATH} route`);
26744
- });
26745
- }
26746
- //#endregion
26747
27051
  //#region ../../../vendor/deepseek-harness/packages/session/session-format-catalog/lib/types/message-projections.js
26748
27052
  /** Pure first-party message interpreters for detached current-format replay, including browser readers. */
26749
27053
  /** Installed interpretation definitions; recovery listeners are mounted separately by their owning plugins. */
@@ -34512,7 +34816,10 @@ var SessionPersistenceRdb = class SessionPersistenceRdb extends SessionPersisten
34512
34816
  });
34513
34817
  this.liveBuffers.delete(id);
34514
34818
  this.liveReady.delete(id);
34819
+ this.liveFailures.delete(id);
34820
+ this.liveDropWarned.delete(id);
34515
34821
  this.reuseEventIds.delete(id);
34822
+ await this.deleteEventCounts(id);
34516
34823
  }
34517
34824
  /** GC 通道:回收已无桥接行引用的事件行(孤儿),返回删除行数。 */
34518
34825
  async collectOrphans() {
@@ -34528,7 +34835,10 @@ var SessionPersistenceRdb = class SessionPersistenceRdb extends SessionPersisten
34528
34835
  for (const id of deletable) {
34529
34836
  this.liveBuffers.delete(id);
34530
34837
  this.liveReady.delete(id);
34838
+ this.liveFailures.delete(id);
34839
+ this.liveDropWarned.delete(id);
34531
34840
  this.reuseEventIds.delete(id);
34841
+ await this.deleteEventCounts(id);
34532
34842
  }
34533
34843
  return deleted;
34534
34844
  }
@@ -34606,6 +34916,7 @@ var SessionPersistenceRdb = class SessionPersistenceRdb extends SessionPersisten
34606
34916
  });
34607
34917
  this.writeGuard.confirmHead(meta.id, confirmedHead);
34608
34918
  this.tracker.materialized(meta.id);
34919
+ await this.recordEventCounts(meta.id, events);
34609
34920
  return true;
34610
34921
  }
34611
34922
  async readLog(id, options = {}, signal) {
@@ -34654,6 +34965,35 @@ var SessionPersistenceRdb = class SessionPersistenceRdb extends SessionPersisten
34654
34965
  ...tornFrom !== void 0 ? { tornFrom } : {}
34655
34966
  };
34656
34967
  }
34968
+ /**
34969
+ * 活动计数**旁路累加**:不在写事务里、失败只 warn——`t_event_counts` 是可销毁重建的派生表,
34970
+ * 丢几次累加不影响可用性(需要时用 `rebuildEventCounts` 重算)。
34971
+ */
34972
+ async recordEventCounts(id, events) {
34973
+ const buckets = eventCountBuckets(events);
34974
+ if (buckets.length === 0) return;
34975
+ try {
34976
+ await this.backend.incrementEventCounts(id, buckets);
34977
+ } catch (error) {
34978
+ this.ctx.logger.warn(`session-rdb: activity counts for "${id}" not updated (${describeError(error)})`);
34979
+ }
34980
+ }
34981
+ /** 重算一个会话的活动计数(rewind / fork 之后;best-effort,同 `recordEventCounts`)。 */
34982
+ async rebuildEventCounts(id) {
34983
+ try {
34984
+ await this.backend.rebuildEventCounts(id);
34985
+ } catch (error) {
34986
+ this.ctx.logger.warn(`session-rdb: activity counts for "${id}" not rebuilt (${describeError(error)})`);
34987
+ }
34988
+ }
34989
+ /** 会话删除时清掉它的活动计数行(best-effort)。 */
34990
+ async deleteEventCounts(id) {
34991
+ try {
34992
+ await this.backend.deleteEventCounts(id);
34993
+ } catch (error) {
34994
+ this.ctx.logger.warn(`session-rdb: activity counts for "${id}" not deleted (${describeError(error)})`);
34995
+ }
34996
+ }
34657
34997
  async rewriteMigratedLog(id, log) {
34658
34998
  await this.backend.transaction(async (tx) => {
34659
34999
  await tx.deleteBridgeTail(id, 0);
@@ -34712,24 +35052,26 @@ var SessionPersistenceRdb = class SessionPersistenceRdb extends SessionPersisten
34712
35052
  }
34713
35053
  }
34714
35054
  async load(id, signal) {
34715
- const handle = await this.open(id, "read", signal === void 0 ? void 0 : { signal });
34716
- try {
34717
- const { events } = await handle.read(0, void 0, signal === void 0 ? void 0 : { signal });
34718
- const row = await this.backend.getSession(id);
34719
- if (row === void 0) throw new SessionPersistenceNotFoundError(id);
34720
- if (isLegacyVersion(row.fVersion)) return {
34721
- meta: handle.header,
34722
- inheritedEventCount: handle.inheritedEventCount,
34723
- events
34724
- };
34725
- return {
34726
- meta: rowToMeta(row),
34727
- inheritedEventCount: SessionLogOffset(row.fSeedLength ?? 0),
34728
- events
35055
+ signal?.throwIfAborted();
35056
+ await this.ready;
35057
+ signal?.throwIfAborted();
35058
+ const log = await this.readLog(id, {}, signal);
35059
+ if (log === void 0) {
35060
+ const pending = this.tracker.pendingOf(id);
35061
+ if (pending !== void 0) return {
35062
+ meta: pending.header,
35063
+ inheritedEventCount: SessionLogOffset(pending.inheritedEventCount),
35064
+ events: []
34729
35065
  };
34730
- } finally {
34731
- await handle.close();
35066
+ throw new SessionPersistenceNotFoundError(id);
34732
35067
  }
35068
+ repairReadView(log.events);
35069
+ validateStoredEvents(log.meta, log.events);
35070
+ return {
35071
+ meta: log.meta,
35072
+ inheritedEventCount: SessionLogOffset(log.inheritedEventCount),
35073
+ events: log.events
35074
+ };
34733
35075
  }
34734
35076
  async append(id, events) {
34735
35077
  const handle = await this.open(id, "write");
@@ -34772,22 +35114,28 @@ var SessionPersistenceRdb = class SessionPersistenceRdb extends SessionPersisten
34772
35114
  rowRevision(row) {
34773
35115
  return SessionPersistenceRevision(`${this.storeIdentity}:incarnation:${row.fIncarnation}:revision:${row.fRevision}`);
34774
35116
  }
35117
+ liveFailures = /* @__PURE__ */ new Map();
35118
+ liveDropWarned = /* @__PURE__ */ new Set();
35119
+ watchLiveReady(ctx, session, ready) {
35120
+ ready.catch((error) => {
35121
+ const failure = error instanceof Error ? error : new Error(typeof error === "string" ? error : `live session "${session.id}" persistence failed`);
35122
+ this.liveFailures.set(session.id, failure);
35123
+ this.liveBuffers.delete(session.id);
35124
+ ctx.logger.error(`session-rdb: live session "${session.id}" persistence unavailable (${failure.message}); its events will not be persisted`);
35125
+ });
35126
+ }
34775
35127
  installLiveRouting(ctx) {
34776
35128
  ctx.on("session/created", (session) => {
34777
35129
  this.liveBuffers.set(session.id, []);
34778
35130
  const ready = this.ensureLiveHandle(session);
34779
35131
  this.liveReady.set(session.id, ready);
34780
- ready.catch((error) => {
34781
- ctx.logger.warn(`session-rdb: live session "${session.id}" persistence init failed: ${String(error)}`);
34782
- });
35132
+ this.watchLiveReady(ctx, session, ready);
34783
35133
  });
34784
35134
  for (const session of ctx.sessions.list()) {
34785
35135
  this.liveBuffers.set(session.id, []);
34786
35136
  const ready = this.ensureLiveHandle(session);
34787
35137
  this.liveReady.set(session.id, ready);
34788
- ready.catch((error) => {
34789
- ctx.logger.warn(`session-rdb: live session "${session.id}" persistence init failed: ${String(error)}`);
34790
- });
35138
+ this.watchLiveReady(ctx, session, ready);
34791
35139
  }
34792
35140
  ctx.on("session/event", (session, event) => {
34793
35141
  const handle = this.tracker.writerOf(session.id);
@@ -34797,9 +35145,18 @@ var SessionPersistenceRdb = class SessionPersistenceRdb extends SessionPersisten
34797
35145
  });
34798
35146
  return;
34799
35147
  }
35148
+ if (this.liveFailures.has(session.id)) {
35149
+ if (!this.liveDropWarned.has(session.id)) {
35150
+ this.liveDropWarned.add(session.id);
35151
+ ctx.logger.warn(`session-rdb: dropping events for live session "${session.id}" because persistence failed earlier`);
35152
+ }
35153
+ return;
35154
+ }
34800
35155
  this.liveBuffers.get(session.id)?.push(structuredClone(event));
34801
35156
  });
34802
35157
  ctx.on("session/flush", (session) => {
35158
+ const failure = this.liveFailures.get(session.id);
35159
+ if (failure !== void 0) return Promise.reject(failure);
34803
35160
  const handle = this.tracker.writerOf(session.id);
34804
35161
  if (handle === void 0) {
34805
35162
  const ready = this.liveReady.get(session.id);
@@ -34816,6 +35173,8 @@ var SessionPersistenceRdb = class SessionPersistenceRdb extends SessionPersisten
34816
35173
  const ready = this.liveReady.get(session.id);
34817
35174
  this.liveBuffers.delete(session.id);
34818
35175
  this.liveReady.delete(session.id);
35176
+ this.liveFailures.delete(session.id);
35177
+ this.liveDropWarned.delete(session.id);
34819
35178
  const closeHandle = () => {
34820
35179
  const handle = this.tracker.writerOf(session.id);
34821
35180
  if (handle === void 0) return;
@@ -34831,6 +35190,10 @@ var SessionPersistenceRdb = class SessionPersistenceRdb extends SessionPersisten
34831
35190
  });
34832
35191
  ctx.effect(() => async () => {
34833
35192
  await Promise.allSettled(this.liveReady.values());
35193
+ this.liveBuffers.clear();
35194
+ this.liveReady.clear();
35195
+ this.liveFailures.clear();
35196
+ this.liveDropWarned.clear();
34834
35197
  await this.tracker.closeAll();
34835
35198
  await this.close();
34836
35199
  }, `${this.name} open handles`);
@@ -34869,6 +35232,26 @@ function seedCoversPrefix(seed, prefix) {
34869
35232
  return seedEvent !== void 0 && JSON.stringify(seedEvent) === JSON.stringify(event);
34870
35233
  });
34871
35234
  }
35235
+ /** 一批事件折成活动计数的桶:只数四种类型,按本地日聚合。 */
35236
+ function eventCountBuckets(events) {
35237
+ const counted = /* @__PURE__ */ new Map();
35238
+ for (const event of events) {
35239
+ if (!COUNTED_EVENT_TYPES.includes(event.type)) continue;
35240
+ const day = localDayKey(event.time);
35241
+ const key = `${day}#${event.type}`;
35242
+ const bucket = counted.get(key) ?? {
35243
+ day,
35244
+ type: event.type,
35245
+ count: 0
35246
+ };
35247
+ bucket.count += 1;
35248
+ counted.set(key, bucket);
35249
+ }
35250
+ return [...counted.values()];
35251
+ }
35252
+ function describeError(error) {
35253
+ return error instanceof Error ? error.message : "unknown error";
35254
+ }
34872
35255
  function createBackend(config) {
34873
35256
  if (config.type === "sqlite") return new SqliteBackend({
34874
35257
  path: config.path,