@morlay/session-rdb 0.0.21-alpha.3 → 0.0.21-alpha.5

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.
@@ -1,6 +1,6 @@
1
- import { C as WriteGuard, S as toPostgresSchema, a as EVENT_ENCODING, c as eventDimensions, i as DEFAULT_BUSY_TIMEOUT_MS, r as createStorageRepository, t as SqliteBackend, x as postgresTableDefs } from "./sqlite-fpvm5Dzs.mjs";
2
- import { _ as toJsonlArtifact, d as rowToMeta, f as scanRows, g as titleOfEventData, m as sessionInsertRow, p as sessionConflictRow, s as repairReadView, v as usageRowOf } from "./log-CnYct2Dv.mjs";
3
- import { c as isLegacyVersion, o as adoptLegacyRows, s as convertLegacyRows, t as SessionBranchRdb } from "./branch-5JzX9rUq.mjs";
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-C3LHLzYW.mjs";
2
+ import { _ as toJsonlArtifact, d as rowToMeta, f as scanRows, g as titleOfEventData, m as sessionInsertRow, p as sessionConflictRow, s as repairReadView, v as usageRowOf } from "./log-C-rbdFwK.mjs";
3
+ import { c as isLegacyVersion, o as adoptLegacyRows, s as convertLegacyRows, t as SessionBranchRdb } from "./branch-Dlmbkfjj.mjs";
4
4
  import { registerSessionImport } from "./import.mjs";
5
5
  import z from "@deepseek-ai/schemastery";
6
6
  import { randomUUID } from "node:crypto";
@@ -20,6 +20,13 @@ import { StorageError, storageBackendServiceKey } from "@deepseek-ai/dsh-storage
20
20
  import { Service } from "@deepseek-ai/cordis";
21
21
  //#region src/postgres.ts
22
22
  const postgresMigrationsDir = fileURLToPath(new URL("../drizzle/postgres/", import.meta.url));
23
+ const pgWriteQueues = /* @__PURE__ */ new Map();
24
+ /** 按介质(连接串 + schema)串行化写事务,与 SQLite 侧的 `enqueueSqliteTx` 同形。 */
25
+ function enqueuePgWrite(key, fn) {
26
+ const run = (pgWriteQueues.get(key) ?? Promise.resolve()).then(fn);
27
+ pgWriteQueues.set(key, run.then(() => void 0, () => void 0));
28
+ return run;
29
+ }
23
30
  /** pg 的 text 列窄化:非字符串(含 null)都不当作文本。 */
24
31
  function text(value) {
25
32
  return typeof value === "string" ? value : null;
@@ -30,9 +37,8 @@ function numeric(value) {
30
37
  return Number.isFinite(parsed) ? parsed : 0;
31
38
  }
32
39
  /** SQL 的缺失求和是 NULL:没有 usage 字段的行使该字段计 0。 */
33
- function totalsOfRow(row) {
40
+ function tokenTotalsOf(row) {
34
41
  return {
35
- events: numeric(row["events"]),
36
42
  inputTokens: numeric(row["input_tokens"]),
37
43
  outputTokens: numeric(row["output_tokens"]),
38
44
  cacheReadTokens: numeric(row["cache_read_tokens"]),
@@ -40,6 +46,16 @@ function totalsOfRow(row) {
40
46
  totalTokens: numeric(row["total_tokens"])
41
47
  };
42
48
  }
49
+ function emptyActivity() {
50
+ return {
51
+ turns: 0,
52
+ steps: 0,
53
+ userInputs: 0,
54
+ toolCalls: 0
55
+ };
56
+ }
57
+ /** `IN (...)` 用的类型字面量(与 `COUNTED_EVENT_TYPES` 同源)。 */
58
+ const COUNTED_EVENT_TYPE_SQL = COUNTED_EVENT_TYPES.map((type) => `'${type}'`).join(", ");
43
59
  var PostgresBackend = class PostgresBackend {
44
60
  db;
45
61
  options;
@@ -60,9 +76,10 @@ var PostgresBackend = class PostgresBackend {
60
76
  this.rejectOpened = reject;
61
77
  });
62
78
  this.opened.catch(() => {});
79
+ const writeQueueKey = options.identityBase;
63
80
  this.storage = createStorageRepository({
64
81
  db: () => this.opened.then(() => this.txOverride ?? this.db),
65
- writeAtomically: (fn) => this.db.transaction(async (tx) => {
82
+ writeAtomically: (fn) => enqueuePgWrite(writeQueueKey, () => this.db.transaction(async (tx) => {
66
83
  const previous = this.txOverride;
67
84
  this.txOverride = tx;
68
85
  try {
@@ -70,7 +87,7 @@ var PostgresBackend = class PostgresBackend {
70
87
  } finally {
71
88
  this.txOverride = previous;
72
89
  }
73
- }, { isolationLevel: "serializable" }),
90
+ }, { isolationLevel: "serializable" })),
74
91
  tables: this.tables
75
92
  });
76
93
  }
@@ -90,6 +107,8 @@ var PostgresBackend = class PostgresBackend {
90
107
  if (await this.readMeta(this.db, "schema_version") === "2") await this.baselineV2();
91
108
  }
92
109
  await migrate(this.db, { migrationsFolder: postgresMigrationsDir });
110
+ await this.ensureEventCountsTable();
111
+ await this.backfillEventCounts();
93
112
  await this.backfillEventUsage();
94
113
  const storeId = await this.db.transaction(async (tx) => {
95
114
  await tx.insert(this.tables["t_persistence_state"]).values({
@@ -130,11 +149,14 @@ var PostgresBackend = class PostgresBackend {
130
149
  }
131
150
  async getEventTypeAt(id, sequence) {
132
151
  const table = this.tables["t_session_events"];
133
- return (await this.eventRows(this.db).where(and(eq(table.fSessionId, id), eq(table.fSequence, sequence))).limit(1).execute())[0]?.fType;
152
+ 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;
134
153
  }
135
154
  async getEventTypesBefore(id, beforeSequence, limit) {
136
155
  const table = this.tables["t_session_events"];
137
- return await this.eventRows(this.db).where(and(eq(table.fSessionId, id), lt(table.fSequence, beforeSequence))).orderBy(desc(table.fSequence)).limit(limit).execute();
156
+ return await this.db.select({
157
+ fSequence: table.fSequence,
158
+ fType: this.tables["t_events"].fType
159
+ }).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();
138
160
  }
139
161
  async listSessions() {
140
162
  return this.db.select().from(this.tables["t_sessions"]).execute();
@@ -292,61 +314,176 @@ var PostgresBackend = class PostgresBackend {
292
314
  }
293
315
  /** 用量聚合:与 SQLite 侧同形,读 `t_event_usage`,数值列回来是字符串。 */
294
316
  async usageReport(sinceMs) {
295
- const sinceClause = sinceMs === void 0 ? sql`` : sql` AND u.f_created_at >= ${sinceMs}`;
317
+ const usageSince = sinceMs === void 0 ? sql`` : sql` AND u.f_created_at >= ${sinceMs}`;
318
+ const daySince = sinceMs === void 0 ? sql`` : sql` AND c.f_day >= ${localDayKey(sinceMs)}`;
296
319
  const tEventUsage = this.tables["t_event_usage"];
297
320
  const tSessionEvents = this.tables["t_session_events"];
298
321
  const tSessions = this.tables["t_sessions"];
322
+ const eventCountsTable = this.qualifiedTable("t_event_counts");
323
+ const subagentOfUsage = sql`EXISTS (SELECT 1 FROM ${tSessionEvents} sb
324
+ JOIN ${tSessions} ss ON ss.f_session_id = sb.f_session_id
325
+ WHERE sb.f_event_id = u.f_event_id AND ss.f_origin = 'subagent')`;
326
+ const subagentOfCounts = sql`EXISTS (SELECT 1 FROM ${tSessionEvents} sb
327
+ JOIN ${tSessions} ss ON ss.f_session_id = sb.f_session_id
328
+ WHERE sb.f_session_id = c.f_session_id AND ss.f_origin = 'subagent')`;
329
+ const hasUsageSession = (column) => sql`EXISTS (SELECT 1 FROM ${tSessionEvents} ub
330
+ JOIN ${tEventUsage} uu ON uu.f_event_id = ub.f_event_id
331
+ WHERE ub.f_session_id = ${sql.raw(column)})`;
332
+ const tokenColumns = sql.raw(`sum(u.f_input_tokens) AS input_tokens,
333
+ sum(u.f_output_tokens) AS output_tokens,
334
+ sum(u.f_cache_read_tokens) AS cache_read_tokens,
335
+ sum(u.f_reasoning_tokens) AS reasoning_tokens,
336
+ sum(u.f_total_tokens) AS total_tokens`);
299
337
  const buckets = await this.db.execute(sql`
300
338
  SELECT to_char(to_timestamp(u.f_created_at / 1000.0), 'YYYY-MM-DD') AS day,
301
339
  u.f_provider AS provider,
302
340
  u.f_model AS model,
303
- EXISTS (SELECT 1 FROM ${tSessionEvents} sb
304
- JOIN ${tSessions} ss ON ss.f_session_id = sb.f_session_id
305
- WHERE sb.f_event_id = u.f_event_id AND ss.f_origin = 'subagent') AS subagent,
306
- count(*) AS events,
307
- sum(u.f_input_tokens) AS input_tokens,
308
- sum(u.f_output_tokens) AS output_tokens,
309
- sum(u.f_cache_read_tokens) AS cache_read_tokens,
310
- sum(u.f_reasoning_tokens) AS reasoning_tokens,
311
- sum(u.f_total_tokens) AS total_tokens
341
+ ${subagentOfUsage} AS subagent,
342
+ ${tokenColumns}
312
343
  FROM ${tEventUsage} u
313
- WHERE EXISTS (SELECT 1 FROM ${tSessionEvents} rb WHERE rb.f_event_id = u.f_event_id)${sinceClause}
344
+ WHERE EXISTS (SELECT 1 FROM ${tSessionEvents} rb WHERE rb.f_event_id = u.f_event_id)${usageSince}
314
345
  GROUP BY 1, 2, 3, 4
315
346
  `);
316
- const sessions = await this.db.execute(sql`
347
+ const tokenScope = await this.db.execute(sql`
348
+ SELECT ${subagentOfUsage} AS subagent,
349
+ ${tokenColumns}
350
+ FROM ${tEventUsage} u
351
+ WHERE EXISTS (SELECT 1 FROM ${tSessionEvents} rb WHERE rb.f_event_id = u.f_event_id)${usageSince}
352
+ GROUP BY 1
353
+ `);
354
+ const activityScope = await this.db.execute(sql`
355
+ SELECT ${subagentOfCounts} AS subagent,
356
+ c.f_type AS f_type,
357
+ sum(c.f_count) AS n
358
+ FROM ${sql.raw(eventCountsTable)} c
359
+ JOIN ${tSessions} s ON s.f_session_id = c.f_session_id
360
+ WHERE ${hasUsageSession("c.f_session_id")}${daySince}
361
+ GROUP BY 1, 2
362
+ `);
363
+ const tokenSessions = await this.db.execute(sql`
317
364
  SELECT b.f_session_id AS session_id,
318
365
  s.f_title AS title,
319
366
  (s.f_origin = 'subagent') AS subagent,
320
367
  (s.f_archived_at IS NOT NULL) AS archived,
321
- count(*) AS events,
322
- sum(u.f_input_tokens) AS input_tokens,
323
- sum(u.f_output_tokens) AS output_tokens,
324
- sum(u.f_cache_read_tokens) AS cache_read_tokens,
325
- sum(u.f_reasoning_tokens) AS reasoning_tokens,
326
- sum(u.f_total_tokens) AS total_tokens
368
+ ${tokenColumns}
327
369
  FROM ${tSessionEvents} b
328
370
  JOIN ${tEventUsage} u ON u.f_event_id = b.f_event_id
329
371
  JOIN ${tSessions} s ON s.f_session_id = b.f_session_id
330
- WHERE 1 = 1${sinceClause}
372
+ WHERE 1 = 1${usageSince}
331
373
  GROUP BY b.f_session_id, s.f_title, s.f_origin, s.f_archived_at
332
374
  `);
375
+ const activitySessions = await this.db.execute(sql`
376
+ SELECT c.f_session_id AS session_id,
377
+ c.f_type AS f_type,
378
+ sum(c.f_count) AS n
379
+ FROM ${sql.raw(eventCountsTable)} c
380
+ WHERE ${hasUsageSession("c.f_session_id")}${daySince}
381
+ GROUP BY c.f_session_id, c.f_type
382
+ `);
383
+ const human = emptyTotals();
384
+ const subagent = emptyTotals();
385
+ for (const row of tokenScope.rows) addTokenTotals(row["subagent"] === true ? subagent : human, tokenTotalsOf(row));
386
+ for (const row of activityScope.rows) addActivityCount(row["subagent"] === true ? subagent : human, String(row["f_type"]), numeric(row["n"]));
387
+ const totals = addTotals(addTotals(emptyTotals(), human), subagent);
388
+ const activityBySession = /* @__PURE__ */ new Map();
389
+ for (const row of activitySessions.rows) {
390
+ const key = String(row["session_id"]);
391
+ const target = activityBySession.get(key) ?? emptyActivity();
392
+ addActivityCount(target, String(row["f_type"]), numeric(row["n"]));
393
+ activityBySession.set(key, target);
394
+ }
333
395
  return {
396
+ totals,
397
+ subagent,
398
+ human,
334
399
  buckets: buckets.rows.map((row) => ({
335
400
  day: String(row["day"]),
336
401
  provider: text(row["provider"]),
337
402
  model: text(row["model"]),
338
403
  subagent: row["subagent"] === true,
339
- ...totalsOfRow(row)
404
+ ...tokenTotalsOf(row)
340
405
  })),
341
- sessions: sessions.rows.map((row) => ({
406
+ sessions: tokenSessions.rows.map((row) => ({
342
407
  sessionId: String(row["session_id"]),
343
408
  title: text(row["title"]),
344
409
  subagent: row["subagent"] === true,
345
410
  archived: row["archived"] === true,
346
- ...totalsOfRow(row)
411
+ ...tokenTotalsOf(row),
412
+ ...activityBySession.get(String(row["session_id"])) ?? emptyActivity()
347
413
  }))
348
414
  };
349
415
  }
416
+ /** 表名限定到配置的 schema(派生表不进 drizzle schema / 迁移,建表幂等)。 */
417
+ qualifiedTable(name) {
418
+ const schema = this.options.schema ?? "public";
419
+ return schema === "public" ? name : `"${schema}".${name}`;
420
+ }
421
+ async ensureEventCountsTable() {
422
+ const table = this.qualifiedTable("t_event_counts");
423
+ await this.db.execute(sql.raw(`CREATE TABLE IF NOT EXISTS ${table} (
424
+ f_id serial PRIMARY KEY,
425
+ f_session_id text NOT NULL,
426
+ f_day text NOT NULL,
427
+ f_type text NOT NULL,
428
+ f_count integer NOT NULL DEFAULT 0
429
+ )`));
430
+ 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)`));
431
+ await this.db.execute(sql.raw(`CREATE INDEX IF NOT EXISTS idx_event_counts_day ON ${table}(f_day)`));
432
+ }
433
+ /** 一次性回填活动计数(表为空时从事件表重算;幂等)。 */
434
+ async backfillEventCounts() {
435
+ const table = this.qualifiedTable("t_event_counts");
436
+ const existing = await this.db.execute(sql.raw(`SELECT count(*) AS n FROM ${table}`));
437
+ if (Number(existing.rows[0]?.["n"] ?? 0) > 0) return;
438
+ const tSessionEvents = this.tables["t_session_events"];
439
+ const tEvents = this.tables["t_events"];
440
+ await this.db.execute(sql`
441
+ INSERT INTO ${sql.raw(table)} (f_session_id, f_day, f_type, f_count)
442
+ SELECT ${tSessionEvents}.f_session_id,
443
+ to_char(to_timestamp(${tEvents}.f_created_at / 1000.0), 'YYYY-MM-DD'),
444
+ ${tEvents}.f_type,
445
+ count(*)
446
+ FROM ${tSessionEvents}
447
+ JOIN ${tEvents} ON ${tEvents}.f_event_id = ${tSessionEvents}.f_event_id
448
+ WHERE ${tEvents}.f_type IN (${sql.raw(COUNTED_EVENT_TYPE_SQL)})
449
+ GROUP BY 1, 2, 3
450
+ ON CONFLICT DO NOTHING
451
+ `);
452
+ }
453
+ /** 活动计数旁路累加:派生表,不在写事务里(失败可丢,表可销毁重建)。 */
454
+ async incrementEventCounts(id, buckets) {
455
+ if (buckets.length === 0) return;
456
+ const table = this.qualifiedTable("t_event_counts");
457
+ for (const bucket of buckets) await this.db.execute(sql`
458
+ INSERT INTO ${sql.raw(table)} (f_session_id, f_day, f_type, f_count)
459
+ VALUES (${id}, ${bucket.day}, ${bucket.type}, ${bucket.count})
460
+ ON CONFLICT (f_session_id, f_day, f_type)
461
+ DO UPDATE SET f_count = ${sql.raw(`${table}.f_count`)} + EXCLUDED.f_count
462
+ `);
463
+ }
464
+ /** 按会话重算活动计数(rewind / fork 之后):先删该会话的行,再从事件表重算。 */
465
+ async rebuildEventCounts(id) {
466
+ const table = this.qualifiedTable("t_event_counts");
467
+ const tSessionEvents = this.tables["t_session_events"];
468
+ const tEvents = this.tables["t_events"];
469
+ await this.db.execute(sql`DELETE FROM ${sql.raw(table)} WHERE f_session_id = ${id}`);
470
+ await this.db.execute(sql`
471
+ INSERT INTO ${sql.raw(table)} (f_session_id, f_day, f_type, f_count)
472
+ SELECT ${tSessionEvents}.f_session_id,
473
+ to_char(to_timestamp(${tEvents}.f_created_at / 1000.0), 'YYYY-MM-DD'),
474
+ ${tEvents}.f_type,
475
+ count(*)
476
+ FROM ${tSessionEvents}
477
+ JOIN ${tEvents} ON ${tEvents}.f_event_id = ${tSessionEvents}.f_event_id
478
+ WHERE ${tSessionEvents}.f_session_id = ${id}
479
+ AND ${tEvents}.f_type IN (${sql.raw(COUNTED_EVENT_TYPE_SQL)})
480
+ GROUP BY 1, 2, 3
481
+ `);
482
+ }
483
+ async deleteEventCounts(id) {
484
+ const table = this.qualifiedTable("t_event_counts");
485
+ await this.db.execute(sql`DELETE FROM ${sql.raw(table)} WHERE f_session_id = ${id}`);
486
+ }
350
487
  async getPrevBridge(exec, id, sequence) {
351
488
  return (await exec.select({
352
489
  fEventId: this.tables["t_session_events"].fEventId,
@@ -556,118 +693,6 @@ function registerSessionGc(ctx, persistence) {
556
693
  });
557
694
  }
558
695
  //#endregion
559
- //#region src/usage.ts
560
- const SESSION_USAGE_PATH = "/api/session.usage";
561
- const DAY_MS = 864e5;
562
- const ROLLING_DAYS = {
563
- "7d": 7,
564
- "30d": 30,
565
- "90d": 90
566
- };
567
- function localDayStart(now) {
568
- const start = new Date(now);
569
- start.setHours(0, 0, 0, 0);
570
- return start;
571
- }
572
- /**
573
- * 范围起点(含),不限时为 undefined。自然日 / 自然周按 host 的本地时区算,周边界是周一 00:00。
574
- * @param range - 语义键。
575
- * @param now - 当前时刻。
576
- * @returns 起点毫秒时间戳,或 undefined。
577
- */
578
- function resolveUsageSince(range, now) {
579
- switch (range) {
580
- case "all": return;
581
- case "day": return localDayStart(now).getTime();
582
- case "week": {
583
- const start = localDayStart(now);
584
- start.setDate(start.getDate() - (start.getDay() + 6) % 7);
585
- return start.getTime();
586
- }
587
- default: return now - ROLLING_DAYS[range] * DAY_MS;
588
- }
589
- }
590
- function parseUsageRange(value) {
591
- return value === "day" || value === "week" || value === "7d" || value === "30d" || value === "90d" ? value : "all";
592
- }
593
- function emptyTotals() {
594
- return {
595
- events: 0,
596
- inputTokens: 0,
597
- outputTokens: 0,
598
- cacheReadTokens: 0,
599
- reasoningTokens: 0,
600
- totalTokens: 0
601
- };
602
- }
603
- /** 折叠一组行(桶或会话行)的用量。 */
604
- function sumUsage(rows) {
605
- const totals = emptyTotals();
606
- for (const row of rows) {
607
- totals.events += row.events;
608
- totals.inputTokens += row.inputTokens;
609
- totals.outputTokens += row.outputTokens;
610
- totals.cacheReadTokens += row.cacheReadTokens;
611
- totals.reasoningTokens += row.reasoningTokens;
612
- totals.totalTokens += row.totalTokens;
613
- }
614
- return totals;
615
- }
616
- /**
617
- * 用量统计通道:一次请求回报总览(含 subagent 拆分)、按天 × 模型的桶与按会话的行。
618
- * 聚合只算被会话引用的事件行——fork 共享行因此只计一次,已删会话留下的孤儿行不计。
619
- */
620
- function registerSessionUsage(ctx, persistence) {
621
- ctx.inject(["webServer", "connection"], (webCtx) => {
622
- const webServer = webCtx.webServer;
623
- const connection = webCtx.get("connection");
624
- return webCtx.effect(() => webServer.register({
625
- kind: "exact",
626
- path: SESSION_USAGE_PATH,
627
- handler: async (req, res) => {
628
- const rejection = connection.requestRejection(req);
629
- if (rejection !== void 0) {
630
- res.writeHead(rejection);
631
- res.end(rejection === 401 ? "unauthorized" : "forbidden");
632
- return;
633
- }
634
- if (req.method !== "POST") {
635
- res.writeHead(405, { "content-type": "application/json" });
636
- res.end(JSON.stringify({ error: "method not allowed" }));
637
- return;
638
- }
639
- const chunks = [];
640
- for await (const chunk of req) chunks.push(chunk);
641
- const raw = Buffer.concat(chunks).toString("utf8");
642
- let envelope = {};
643
- if (raw !== "") try {
644
- envelope = JSON.parse(raw);
645
- } catch {
646
- res.writeHead(400, { "content-type": "application/json" });
647
- res.end(JSON.stringify({ error: "request body is not JSON" }));
648
- return;
649
- }
650
- try {
651
- const aggregate = await persistence.usageReport(resolveUsageSince(parseUsageRange(envelope.range), Date.now()));
652
- const totals = sumUsage(aggregate.buckets);
653
- const subagent = sumUsage(aggregate.buckets.filter((bucket) => bucket.subagent));
654
- const human = sumUsage(aggregate.buckets.filter((bucket) => !bucket.subagent));
655
- res.writeHead(200, { "content-type": "application/json" });
656
- res.end(JSON.stringify({
657
- totals,
658
- subagent,
659
- human,
660
- ...aggregate
661
- }));
662
- } catch (error) {
663
- res.writeHead(500, { "content-type": "application/json" });
664
- res.end(JSON.stringify({ error: error instanceof Error ? error.message : "usage report failed" }));
665
- }
666
- }
667
- }), `session-rdb: ${SESSION_USAGE_PATH} route`);
668
- });
669
- }
670
- //#endregion
671
696
  //#region src/session-query.ts
672
697
  var SessionQueryRdb = class extends SessionQueryEngine {
673
698
  constructor(ctx, config = {}) {
@@ -1478,7 +1503,10 @@ var SessionPersistenceRdb = class SessionPersistenceRdb extends SessionPersisten
1478
1503
  });
1479
1504
  this.liveBuffers.delete(id);
1480
1505
  this.liveReady.delete(id);
1506
+ this.liveFailures.delete(id);
1507
+ this.liveDropWarned.delete(id);
1481
1508
  this.reuseEventIds.delete(id);
1509
+ await this.deleteEventCounts(id);
1482
1510
  }
1483
1511
  /** GC 通道:回收已无桥接行引用的事件行(孤儿),返回删除行数。 */
1484
1512
  async collectOrphans() {
@@ -1494,7 +1522,10 @@ var SessionPersistenceRdb = class SessionPersistenceRdb extends SessionPersisten
1494
1522
  for (const id of deletable) {
1495
1523
  this.liveBuffers.delete(id);
1496
1524
  this.liveReady.delete(id);
1525
+ this.liveFailures.delete(id);
1526
+ this.liveDropWarned.delete(id);
1497
1527
  this.reuseEventIds.delete(id);
1528
+ await this.deleteEventCounts(id);
1498
1529
  }
1499
1530
  return deleted;
1500
1531
  }
@@ -1572,6 +1603,7 @@ var SessionPersistenceRdb = class SessionPersistenceRdb extends SessionPersisten
1572
1603
  });
1573
1604
  this.writeGuard.confirmHead(meta.id, confirmedHead);
1574
1605
  this.tracker.materialized(meta.id);
1606
+ await this.recordEventCounts(meta.id, events);
1575
1607
  return true;
1576
1608
  }
1577
1609
  async readLog(id, options = {}, signal) {
@@ -1620,6 +1652,35 @@ var SessionPersistenceRdb = class SessionPersistenceRdb extends SessionPersisten
1620
1652
  ...tornFrom !== void 0 ? { tornFrom } : {}
1621
1653
  };
1622
1654
  }
1655
+ /**
1656
+ * 活动计数**旁路累加**:不在写事务里、失败只 warn——`t_event_counts` 是可销毁重建的派生表,
1657
+ * 丢几次累加不影响可用性(需要时用 `rebuildEventCounts` 重算)。
1658
+ */
1659
+ async recordEventCounts(id, events) {
1660
+ const buckets = eventCountBuckets(events);
1661
+ if (buckets.length === 0) return;
1662
+ try {
1663
+ await this.backend.incrementEventCounts(id, buckets);
1664
+ } catch (error) {
1665
+ this.ctx.logger.warn(`session-rdb: activity counts for "${id}" not updated (${describeError(error)})`);
1666
+ }
1667
+ }
1668
+ /** 重算一个会话的活动计数(rewind / fork 之后;best-effort,同 `recordEventCounts`)。 */
1669
+ async rebuildEventCounts(id) {
1670
+ try {
1671
+ await this.backend.rebuildEventCounts(id);
1672
+ } catch (error) {
1673
+ this.ctx.logger.warn(`session-rdb: activity counts for "${id}" not rebuilt (${describeError(error)})`);
1674
+ }
1675
+ }
1676
+ /** 会话删除时清掉它的活动计数行(best-effort)。 */
1677
+ async deleteEventCounts(id) {
1678
+ try {
1679
+ await this.backend.deleteEventCounts(id);
1680
+ } catch (error) {
1681
+ this.ctx.logger.warn(`session-rdb: activity counts for "${id}" not deleted (${describeError(error)})`);
1682
+ }
1683
+ }
1623
1684
  async rewriteMigratedLog(id, log) {
1624
1685
  await this.backend.transaction(async (tx) => {
1625
1686
  await tx.deleteBridgeTail(id, 0);
@@ -1678,24 +1739,26 @@ var SessionPersistenceRdb = class SessionPersistenceRdb extends SessionPersisten
1678
1739
  }
1679
1740
  }
1680
1741
  async load(id, signal) {
1681
- const handle = await this.open(id, "read", signal === void 0 ? void 0 : { signal });
1682
- try {
1683
- const { events } = await handle.read(0, void 0, signal === void 0 ? void 0 : { signal });
1684
- const row = await this.backend.getSession(id);
1685
- if (row === void 0) throw new SessionPersistenceNotFoundError(id);
1686
- if (isLegacyVersion(row.fVersion)) return {
1687
- meta: handle.header,
1688
- inheritedEventCount: handle.inheritedEventCount,
1689
- events
1690
- };
1691
- return {
1692
- meta: rowToMeta(row),
1693
- inheritedEventCount: SessionLogOffset(row.fSeedLength ?? 0),
1694
- events
1742
+ signal?.throwIfAborted();
1743
+ await this.ready;
1744
+ signal?.throwIfAborted();
1745
+ const log = await this.readLog(id, {}, signal);
1746
+ if (log === void 0) {
1747
+ const pending = this.tracker.pendingOf(id);
1748
+ if (pending !== void 0) return {
1749
+ meta: pending.header,
1750
+ inheritedEventCount: SessionLogOffset(pending.inheritedEventCount),
1751
+ events: []
1695
1752
  };
1696
- } finally {
1697
- await handle.close();
1753
+ throw new SessionPersistenceNotFoundError(id);
1698
1754
  }
1755
+ repairReadView(log.events);
1756
+ validateStoredEvents(log.meta, log.events);
1757
+ return {
1758
+ meta: log.meta,
1759
+ inheritedEventCount: SessionLogOffset(log.inheritedEventCount),
1760
+ events: log.events
1761
+ };
1699
1762
  }
1700
1763
  async append(id, events) {
1701
1764
  const handle = await this.open(id, "write");
@@ -1738,22 +1801,28 @@ var SessionPersistenceRdb = class SessionPersistenceRdb extends SessionPersisten
1738
1801
  rowRevision(row) {
1739
1802
  return SessionPersistenceRevision(`${this.storeIdentity}:incarnation:${row.fIncarnation}:revision:${row.fRevision}`);
1740
1803
  }
1804
+ liveFailures = /* @__PURE__ */ new Map();
1805
+ liveDropWarned = /* @__PURE__ */ new Set();
1806
+ watchLiveReady(ctx, session, ready) {
1807
+ ready.catch((error) => {
1808
+ const failure = error instanceof Error ? error : new Error(typeof error === "string" ? error : `live session "${session.id}" persistence failed`);
1809
+ this.liveFailures.set(session.id, failure);
1810
+ this.liveBuffers.delete(session.id);
1811
+ ctx.logger.error(`session-rdb: live session "${session.id}" persistence unavailable (${failure.message}); its events will not be persisted`);
1812
+ });
1813
+ }
1741
1814
  installLiveRouting(ctx) {
1742
1815
  ctx.on("session/created", (session) => {
1743
1816
  this.liveBuffers.set(session.id, []);
1744
1817
  const ready = this.ensureLiveHandle(session);
1745
1818
  this.liveReady.set(session.id, ready);
1746
- ready.catch((error) => {
1747
- ctx.logger.warn(`session-rdb: live session "${session.id}" persistence init failed: ${String(error)}`);
1748
- });
1819
+ this.watchLiveReady(ctx, session, ready);
1749
1820
  });
1750
1821
  for (const session of ctx.sessions.list()) {
1751
1822
  this.liveBuffers.set(session.id, []);
1752
1823
  const ready = this.ensureLiveHandle(session);
1753
1824
  this.liveReady.set(session.id, ready);
1754
- ready.catch((error) => {
1755
- ctx.logger.warn(`session-rdb: live session "${session.id}" persistence init failed: ${String(error)}`);
1756
- });
1825
+ this.watchLiveReady(ctx, session, ready);
1757
1826
  }
1758
1827
  ctx.on("session/event", (session, event) => {
1759
1828
  const handle = this.tracker.writerOf(session.id);
@@ -1763,9 +1832,18 @@ var SessionPersistenceRdb = class SessionPersistenceRdb extends SessionPersisten
1763
1832
  });
1764
1833
  return;
1765
1834
  }
1835
+ if (this.liveFailures.has(session.id)) {
1836
+ if (!this.liveDropWarned.has(session.id)) {
1837
+ this.liveDropWarned.add(session.id);
1838
+ ctx.logger.warn(`session-rdb: dropping events for live session "${session.id}" because persistence failed earlier`);
1839
+ }
1840
+ return;
1841
+ }
1766
1842
  this.liveBuffers.get(session.id)?.push(structuredClone(event));
1767
1843
  });
1768
1844
  ctx.on("session/flush", (session) => {
1845
+ const failure = this.liveFailures.get(session.id);
1846
+ if (failure !== void 0) return Promise.reject(failure);
1769
1847
  const handle = this.tracker.writerOf(session.id);
1770
1848
  if (handle === void 0) {
1771
1849
  const ready = this.liveReady.get(session.id);
@@ -1782,6 +1860,8 @@ var SessionPersistenceRdb = class SessionPersistenceRdb extends SessionPersisten
1782
1860
  const ready = this.liveReady.get(session.id);
1783
1861
  this.liveBuffers.delete(session.id);
1784
1862
  this.liveReady.delete(session.id);
1863
+ this.liveFailures.delete(session.id);
1864
+ this.liveDropWarned.delete(session.id);
1785
1865
  const closeHandle = () => {
1786
1866
  const handle = this.tracker.writerOf(session.id);
1787
1867
  if (handle === void 0) return;
@@ -1797,6 +1877,10 @@ var SessionPersistenceRdb = class SessionPersistenceRdb extends SessionPersisten
1797
1877
  });
1798
1878
  ctx.effect(() => async () => {
1799
1879
  await Promise.allSettled(this.liveReady.values());
1880
+ this.liveBuffers.clear();
1881
+ this.liveReady.clear();
1882
+ this.liveFailures.clear();
1883
+ this.liveDropWarned.clear();
1800
1884
  await this.tracker.closeAll();
1801
1885
  await this.close();
1802
1886
  }, `${this.name} open handles`);
@@ -1835,6 +1919,26 @@ function seedCoversPrefix(seed, prefix) {
1835
1919
  return seedEvent !== void 0 && JSON.stringify(seedEvent) === JSON.stringify(event);
1836
1920
  });
1837
1921
  }
1922
+ /** 一批事件折成活动计数的桶:只数四种类型,按本地日聚合。 */
1923
+ function eventCountBuckets(events) {
1924
+ const counted = /* @__PURE__ */ new Map();
1925
+ for (const event of events) {
1926
+ if (!COUNTED_EVENT_TYPES.includes(event.type)) continue;
1927
+ const day = localDayKey(event.time);
1928
+ const key = `${day}#${event.type}`;
1929
+ const bucket = counted.get(key) ?? {
1930
+ day,
1931
+ type: event.type,
1932
+ count: 0
1933
+ };
1934
+ bucket.count += 1;
1935
+ counted.set(key, bucket);
1936
+ }
1937
+ return [...counted.values()];
1938
+ }
1939
+ function describeError(error) {
1940
+ return error instanceof Error ? error.message : "unknown error";
1941
+ }
1838
1942
  function createBackend(config) {
1839
1943
  if (config.type === "sqlite") return new SqliteBackend({
1840
1944
  path: config.path,
@@ -1,4 +1,4 @@
1
- import { A as tSessionProjcacheRows, C as eventDimensions, D as tPersistenceState, E as tEvents, F as tWorkspaces, I as WriteGuard, L as StorageRepository, M as tStorageUnits, N as tWorkspaceSessions, O as tSchemaMeta, P as tWorkspaceState, S as SESSION_PERSISTENCE_SQLITE_APPLICATION_ID, T as tEventUsage, _ as EVENT_ENCODING, a as UsageAggregate, b as JournalMode, g as DEFAULT_BUSY_TIMEOUT_MS, i as SessionRow, j as tSessions, k as tSessionEvents, n as BackendTx, r as EventRow, t as Backend, v as EventKind, w as eventKind, x as SCHEMA_VERSION, y as EventRole } from "./backend-DpdtxYpz.mjs";
1
+ import { A as tSessionEvents, C as SESSION_PERSISTENCE_SQLITE_APPLICATION_ID, D as tEvents, E as tEventUsage, F as tWorkspaceState, I as tWorkspaces, L as WriteGuard, M as tSessions, N as tStorageUnits, O as tPersistenceState, P as tWorkspaceSessions, R as StorageRepository, S as SCHEMA_VERSION, T as eventKind, _ as DEFAULT_BUSY_TIMEOUT_MS, a as SessionRow, b as EventRole, i as EventRow, j as tSessionProjcacheRows, k as tSchemaMeta, n as BackendTx, o as UsageAggregate, r as EventCountBucket, t as Backend, v as EVENT_ENCODING, w as eventDimensions, x as JournalMode, y as EventKind } from "./backend-a0xDrUFG.mjs";
2
2
  import { SessionId } from "@deepseek-ai/dsh-session";
3
3
  import { DatabaseSync } from "node:sqlite";
4
4
  //#region src/sqlite.d.ts
@@ -47,8 +47,17 @@ declare class SqliteBackend implements Backend {
47
47
  /** 父会话被删后留下的 subagent 会话:父已不在表里,或本来就没有父。 */
48
48
  listOrphanSubagentSessions(): Promise<SessionId[]>;
49
49
  private deleteSessions;
50
- /** 用量聚合:只读 `t_event_usage`;可选只算 `sinceMs` 之后的事件行。 */
50
+ /**
51
+ * 用量聚合:token 用量沿 `t_event_usage` 的用量行,活动计数(轮次 / 步骤 / 用户输入 / 工具调用)
52
+ * 沿 `t_events` 的事件类型数——两者限定在**有 token 用量的会话**里,时间范围各自按自己的
53
+ * `f_created_at`(同一时刻写入,口径一致)。
54
+ */
51
55
  usageReport(sinceMs?: number): Promise<UsageAggregate>;
56
+ /** 活动计数旁路累加:派生表 `t_event_counts`,不在写事务里(失败可丢,表可销毁重建)。 */
57
+ incrementEventCounts(id: SessionId, buckets: readonly EventCountBucket[]): Promise<void>;
58
+ /** 按会话重算活动计数(rewind / fork 之后):先删该会话的行,再从事件表重算。 */
59
+ rebuildEventCounts(id: SessionId): Promise<void>;
60
+ deleteEventCounts(id: SessionId): Promise<void>;
52
61
  vacuum(): Promise<void>;
53
62
  private eventRows;
54
63
  }
package/dist/storage.mjs CHANGED
@@ -1,2 +1,2 @@
1
- import { C as WriteGuard, _ as tStorageUnits, a as EVENT_ENCODING, b as tWorkspaces, c as eventDimensions, d as tEvents, f as tPersistenceState, g as tSessions, h as tSessionProjcacheRows, i as DEFAULT_BUSY_TIMEOUT_MS, l as eventKind, m as tSessionEvents, n as openDatabase, o as SCHEMA_VERSION, p as tSchemaMeta, s as SESSION_PERSISTENCE_SQLITE_APPLICATION_ID, t as SqliteBackend, u as tEventUsage, v as tWorkspaceSessions, y as tWorkspaceState } from "./sqlite-fpvm5Dzs.mjs";
1
+ import { A as WriteGuard, C as tSessions, D as tWorkspaces, E as tWorkspaceState, S as tSessionProjcacheRows, T as tWorkspaceSessions, _ as tEventUsage, b as tSchemaMeta, d as DEFAULT_BUSY_TIMEOUT_MS, f as EVENT_ENCODING, g as eventKind, h as eventDimensions, m as SESSION_PERSISTENCE_SQLITE_APPLICATION_ID, n as openDatabase, p as SCHEMA_VERSION, t as SqliteBackend, v as tEvents, w as tStorageUnits, x as tSessionEvents, y as tPersistenceState } from "./sqlite-C3LHLzYW.mjs";
2
2
  export { DEFAULT_BUSY_TIMEOUT_MS, EVENT_ENCODING, SCHEMA_VERSION, SESSION_PERSISTENCE_SQLITE_APPLICATION_ID, SqliteBackend, WriteGuard, eventDimensions, eventKind, openDatabase, tEventUsage, tEvents, tPersistenceState, tSchemaMeta, tSessionEvents, tSessionProjcacheRows, tSessions, tStorageUnits, tWorkspaceSessions, tWorkspaceState, tWorkspaces };
@@ -1,4 +1,4 @@
1
- import { h as truncateLiveSession } from "./backend-DpdtxYpz.mjs";
1
+ import { g as truncateLiveSession } from "./backend-a0xDrUFG.mjs";
2
2
  import { SessionPersistence } from "@deepseek-ai/dsh-session-persistence";
3
3
  import { Session, SessionEvent, SessionHeader, SessionId } from "@deepseek-ai/dsh-session";
4
4
  import { Context, Fiber } from "@deepseek-ai/cordis";