@morlay/session-rdb 0.1.0 → 0.1.2

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 (48) hide show
  1. package/README.md +17 -166
  2. package/dist/artifact.d.mts +2 -2
  3. package/dist/artifact.mjs +3 -2
  4. package/dist/{branch-D4nVzQQM.mjs → branch-IQii6Muq.mjs} +1 -8
  5. package/dist/deletion.d.mts +1 -2
  6. package/dist/deletion.mjs +2 -1
  7. package/dist/export.d.mts +1 -3
  8. package/dist/export.mjs +1 -4
  9. package/dist/gc.d.mts +1 -3
  10. package/dist/gc.mjs +1 -3
  11. package/dist/import.d.mts +1 -2
  12. package/dist/import.mjs +2 -7
  13. package/dist/index.d.mts +1 -1
  14. package/dist/index.mjs +4 -3
  15. package/dist/{log-BWVFYAh5.mjs → log-D5C8-TGs.mjs} +0 -52
  16. package/dist/{log-CUjKQbOR.d.mts → log-tZMKFRKi.d.mts} +17 -168
  17. package/dist/routes-DGBlBE_I.mjs +10 -0
  18. package/dist/{sqlite-SCiSnwaz.mjs → sqlite-CMMSn5kE.mjs} +24 -62
  19. package/dist/{src-BZ1tffd5.mjs → src-dsd2H70D.mjs} +59 -97
  20. package/dist/storage.d.mts +2 -22
  21. package/dist/storage.mjs +1 -1
  22. package/dist/testing.d.mts +1 -1
  23. package/dist/testing.mjs +1 -1
  24. package/dist/usage.d.mts +1 -1
  25. package/dist/usage.mjs +1 -17
  26. package/package.json +19 -19
  27. package/src/backend.ts +35 -54
  28. package/src/branch.ts +3 -5
  29. package/src/deletion.ts +2 -1
  30. package/src/entities/v3/event-usage.ts +3 -6
  31. package/src/entities/v3/session-counts.ts +3 -5
  32. package/src/entities/v3/session-usage.ts +3 -5
  33. package/src/export.ts +5 -4
  34. package/src/gc.ts +4 -3
  35. package/src/import.ts +5 -6
  36. package/src/index.ts +47 -45
  37. package/src/legacy.ts +3 -5
  38. package/src/log.ts +24 -53
  39. package/src/postgres.ts +70 -61
  40. package/src/routes.ts +16 -0
  41. package/src/rows.ts +11 -15
  42. package/src/session-query.ts +3 -7
  43. package/src/sqlite.ts +70 -58
  44. package/src/storage-takeover/projection-cache.ts +6 -8
  45. package/src/storage-takeover/repository.ts +2 -2
  46. package/src/storage-takeover/types.ts +1 -1
  47. package/src/usage.ts +22 -34
  48. package/src/write-guard.ts +1 -1
package/src/sqlite.ts CHANGED
@@ -52,7 +52,7 @@ import type { StorageRepository } from "./storage-takeover/types.ts";
52
52
 
53
53
  type SqliteDb = NodeSQLiteDatabase & { $client: DatabaseSync };
54
54
 
55
- /** 用量聚合的原始行(列名是 SQL 别名)。 */
55
+ // 用量聚合的原始行(列名是 SQL 别名)。
56
56
  interface RawBucketRow {
57
57
  day: string;
58
58
  provider: string | null;
@@ -65,17 +65,17 @@ interface RawBucketRow {
65
65
  total_tokens: number | null;
66
66
  }
67
67
 
68
- /** 「是否子代理」两组的 token 合计行。 */
68
+ // 「是否子代理」两组的 token 合计行。
69
69
  interface RawScopeRow extends RawBucketRow {}
70
70
 
71
- /** 按会话的行:token 一条查询、活动一条查询,按 session_id 合并。 */
71
+ // 按会话的行:token 一条查询、活动一条查询,按 session_id 合并。
72
72
  interface RawSessionTokenRow extends RawBucketRow {
73
73
  session_id: string;
74
74
  title: string | null;
75
75
  archived: number;
76
76
  }
77
77
 
78
- /** `t_session_counts` 的原始行(四项计数,列名是 SQL 别名)。 */
78
+ // `t_session_counts` 的原始行(四项计数,列名是 SQL 别名)。
79
79
  interface RawCountRow {
80
80
  turns: number | null;
81
81
  steps: number | null;
@@ -83,36 +83,37 @@ interface RawCountRow {
83
83
  tool_calls: number | null;
84
84
  }
85
85
 
86
- /** 按会话的活动计数行。 */
86
+ // 按会话的活动计数行。
87
87
  interface RawSessionCountRow extends RawCountRow {
88
88
  session_id: string;
89
89
  }
90
90
 
91
- /** 活动总览行:是否子代理 + 四项计数。 */
91
+ // 活动总览行:是否子代理 + 四项计数。
92
92
  interface RawActivityScopeRow extends RawCountRow {
93
93
  subagent: number | null;
94
94
  }
95
95
 
96
- /** 用量行的「被引用」物化表达式(`%EVENT%` 换成事件 id 列)。 */
96
+ // 用量行的「被引用」物化表达式(`%EVENT%` 换成事件 id 列)。
97
97
  const EVENT_REFERENCED_SQL = `EXISTS (SELECT 1 FROM t_session_events rb
98
98
  WHERE rb.f_event_id = %EVENT%)`;
99
99
 
100
- /** 用量行的「被 subagent 会话引用」物化表达式(`%EVENT%` 换成事件 id 列)。 */
100
+ // 用量行的「被 subagent 会话引用」物化表达式(`%EVENT%` 换成事件 id 列)。
101
101
  const EVENT_SUBAGENT_SQL = `EXISTS (SELECT 1 FROM t_session_events sb
102
102
  JOIN t_sessions ss ON ss.f_session_id = sb.f_session_id
103
103
  WHERE sb.f_event_id = %EVENT% AND ss.f_origin = 'subagent')`;
104
104
 
105
- /** `IN (...)` 用的类型字面量(与 `COUNTED_EVENT_TYPES` 同源)。 */
105
+ // `IN (...)` 用的类型字面量(与 `COUNTED_EVENT_TYPES` 同源)。
106
106
  const COUNTED_EVENT_TYPE_SQL = COUNTED_EVENT_TYPES.map((type) => `'${type}'`).join(", ");
107
107
 
108
+ // 引用标记的分批重算:一次绑定的参数有上限,按块走。
109
+ const REFRESH_FLAG_CHUNK = 400;
110
+
108
111
  function withEvent(expression: string, column: string): string {
109
112
  return expression.replace("%EVENT%", column);
110
113
  }
111
114
 
112
- /**
113
- * 派生统计表的回填:任一表为空即按事件表全量重算(幂等;迁移删表重建后、手动清空后都靠它自愈)。
114
- * 顺序有依赖:用量行(带物化列)先落,两张会话汇总表再从它 / 事件表重算。
115
- */
115
+ // 派生统计表的回填:任一表为空即按事件表全量重算(幂等;迁移删表重建后、手动清空后都靠它自愈)。
116
+ // 顺序有依赖:用量行(带物化列)先落,两张会话汇总表再从它 / 事件表重算。
116
117
  function backfillUsageTables(db: DatabaseSync): void {
117
118
  backfillEventUsage(db);
118
119
  backfillSessionUsage(db);
@@ -124,7 +125,7 @@ function tableIsEmpty(db: DatabaseSync, table: string): boolean {
124
125
  return row.n === 0;
125
126
  }
126
127
 
127
- /** 用量行:一条 `assistant/message` 事件行一行,物化本地日与两个归属标记。 */
128
+ // 用量行:一条 `assistant/message` 事件行一行,物化本地日与两个归属标记。
128
129
  function backfillEventUsage(db: DatabaseSync): void {
129
130
  if (!tableIsEmpty(db, "t_event_usage")) return;
130
131
  db.exec(`INSERT OR IGNORE INTO t_event_usage (
@@ -154,7 +155,7 @@ function backfillEventUsage(db: DatabaseSync): void {
154
155
  OR json_extract(e.f_data, '$.usage') IS NOT NULL)`);
155
156
  }
156
157
 
157
- /** 会话 × 本地日 × 模型的 token 汇总:从桥接行 × 用量行重算(含 fork 继承前缀)。 */
158
+ // 会话 × 本地日 × 模型的 token 汇总:从桥接行 × 用量行重算(含 fork 继承前缀)。
158
159
  function backfillSessionUsage(db: DatabaseSync): void {
159
160
  if (!tableIsEmpty(db, "t_session_usage")) return;
160
161
  db.exec(`INSERT OR IGNORE INTO t_session_usage (
@@ -168,7 +169,7 @@ function backfillSessionUsage(db: DatabaseSync): void {
168
169
  GROUP BY b.f_session_id, u.f_day, coalesce(u.f_provider, ''), coalesce(u.f_model, '')`);
169
170
  }
170
171
 
171
- /** 会话 × 本地日的活动计数:从桥接行 × 事件类型重算。 */
172
+ // 会话 × 本地日的活动计数:从桥接行 × 事件类型重算。
172
173
  function backfillSessionCounts(db: DatabaseSync): void {
173
174
  if (!tableIsEmpty(db, "t_session_counts")) return;
174
175
  db.exec(`INSERT OR IGNORE INTO t_session_counts (
@@ -200,7 +201,7 @@ function sessionListRowOf(row: Record<string, unknown>): SessionListRowRecord {
200
201
  };
201
202
  }
202
203
 
203
- /** SQL 的缺失求和是 NULL:没有 usage 字段的行使该字段计 0。 */
204
+ // SQL 的缺失求和是 NULL:没有 usage 字段的行使该字段计 0。
204
205
  function tokenTotalsOf(row: {
205
206
  input_tokens: number | null;
206
207
  output_tokens: number | null;
@@ -217,7 +218,7 @@ function tokenTotalsOf(row: {
217
218
  };
218
219
  }
219
220
 
220
- /** `t_session_counts` 的原始行(列名是 SQL 别名)折成四项活动计数。 */
221
+ // `t_session_counts` 的原始行(列名是 SQL 别名)折成四项活动计数。
221
222
  function activityTotalsOf(row: {
222
223
  turns: number | null;
223
224
  steps: number | null;
@@ -232,12 +233,12 @@ function activityTotalsOf(row: {
232
233
  };
233
234
  }
234
235
 
235
- /** 一行的整套指标:token 用量 + 活动计数(轮次 / 步骤 / 用户输入 / 工具调用)。 */
236
+ // 一行的整套指标:token 用量 + 活动计数(轮次 / 步骤 / 用户输入 / 工具调用)。
236
237
  function emptyActivity(): UsageActivityTotals {
237
238
  return { turns: 0, steps: 0, userInputs: 0, toolCalls: 0 };
238
239
  }
239
240
 
240
- /** 有 token 用量的会话:活动计数与列表都限定在这一批会话里,口径一致(读汇总表,几百行)。 */
241
+ // 有 token 用量的会话:活动计数与列表都限定在这一批会话里,口径一致(读汇总表,几百行)。
241
242
  function hasUsageSession(column: string): string {
242
243
  return `EXISTS (SELECT 1 FROM t_session_usage ub WHERE ub.f_session_id = ${column})`;
243
244
  }
@@ -694,11 +695,13 @@ export class SqliteBackend implements Backend {
694
695
  .run();
695
696
  }
696
697
 
697
- private async deleteBridgeTail(id: SessionId, fromSequence: number): Promise<void> {
698
- this.db
699
- .delete(tSessionEvents)
700
- .where(and(eq(tSessionEvents.fSessionId, id), gte(tSessionEvents.fSequence, fromSequence)))
701
- .run();
698
+ private async deleteBridgeTail(id: SessionId, fromSequence: number): Promise<readonly string[]> {
699
+ const rows = this.db.$client
700
+ .prepare(
701
+ "DELETE FROM t_session_events WHERE f_session_id = ? AND f_sequence >= ? RETURNING f_event_id",
702
+ )
703
+ .all(id, fromSequence) as Array<{ f_event_id: string }>;
704
+ return rows.map((row) => row.f_event_id);
702
705
  }
703
706
 
704
707
  private async getPrevBridge(
@@ -712,14 +715,17 @@ export class SqliteBackend implements Backend {
712
715
  .get() as { fEventId: string; fSequence: number } | undefined;
713
716
  }
714
717
 
715
- private async deleteSession(id: SessionId): Promise<void> {
716
- this.db.delete(tSessionEvents).where(eq(tSessionEvents.fSessionId, id)).run();
718
+ private async deleteSession(id: SessionId): Promise<readonly string[]> {
719
+ const rows = this.db.$client
720
+ .prepare("DELETE FROM t_session_events WHERE f_session_id = ? RETURNING f_event_id")
721
+ .all(id) as Array<{ f_event_id: string }>;
717
722
  this.db.delete(tWorkspaceSessions).where(eq(tWorkspaceSessions.fSessionId, id)).run();
718
723
  this.db.delete(tSessionProjcacheRows).where(eq(tSessionProjcacheRows.fSessionId, id)).run();
719
724
  this.db.delete(tSessions).where(eq(tSessions.fSessionId, id)).run();
725
+ return rows.map((row) => row.f_event_id);
720
726
  }
721
727
 
722
- /** 事件行可能被多个会话共享(fork 派生),所以孤儿只能在全库范围内判定。 */
728
+ // 事件行可能被多个会话共享(fork 派生),所以孤儿只能在全库范围内判定。
723
729
  async collectOrphans(): Promise<number> {
724
730
  const referenced = this.db.select({ fEventId: tSessionEvents.fEventId }).from(tSessionEvents);
725
731
  const info = this.db.delete(tEvents).where(notInArray(tEvents.fEventId, referenced)).run();
@@ -736,7 +742,7 @@ export class SqliteBackend implements Backend {
736
742
  return Number(info.changes);
737
743
  }
738
744
 
739
- /** 父会话被删后留下的 subagent 会话:父已不在表里,或本来就没有父。 */
745
+ // 父会话被删后留下的 subagent 会话:父已不在表里,或本来就没有父。
740
746
  async listOrphanSubagentSessions(): Promise<SessionId[]> {
741
747
  const rows = this.db.$client
742
748
  .prepare(
@@ -749,23 +755,28 @@ export class SqliteBackend implements Backend {
749
755
  return rows.map((row) => row.id as SessionId);
750
756
  }
751
757
 
752
- private async deleteSessions(ids: SessionId[]): Promise<number> {
753
- if (ids.length === 0) return 0;
754
- this.db.delete(tSessionEvents).where(inArray(tSessionEvents.fSessionId, ids)).run();
758
+ private async deleteSessions(
759
+ ids: SessionId[],
760
+ ): Promise<{ deleted: number; eventIds: readonly string[] }> {
761
+ if (ids.length === 0) return { deleted: 0, eventIds: [] };
762
+ const placeholders = ids.map(() => "?").join(", ");
763
+ const rows = this.db.$client
764
+ .prepare(
765
+ `DELETE FROM t_session_events WHERE f_session_id IN (${placeholders}) RETURNING f_event_id`,
766
+ )
767
+ .all(...ids) as Array<{ f_event_id: string }>;
755
768
  this.db.delete(tWorkspaceSessions).where(inArray(tWorkspaceSessions.fSessionId, ids)).run();
756
769
  this.db
757
770
  .delete(tSessionProjcacheRows)
758
771
  .where(inArray(tSessionProjcacheRows.fSessionId, ids))
759
772
  .run();
760
773
  const info = this.db.delete(tSessions).where(inArray(tSessions.fSessionId, ids)).run();
761
- return Number(info.changes);
774
+ return { deleted: Number(info.changes), eventIds: rows.map((row) => row.f_event_id) };
762
775
  }
763
776
 
764
- /**
765
- * 用量聚合:token 用量沿 `t_event_usage` 的用量行,活动计数(轮次 / 步骤 / 用户输入 / 工具调用)
766
- * 沿 `t_events` 的事件类型数——两者限定在**有 token 用量的会话**里,时间范围各自按自己的
767
- * `f_created_at`(同一时刻写入,口径一致)。
768
- */
777
+ // 用量聚合:token 用量沿 `t_event_usage` 的用量行,活动计数(轮次 / 步骤 / 用户输入 / 工具调用)
778
+ // 沿 `t_events` 的事件类型数——两者限定在**有 token 用量的会话**里,时间范围各自按自己的
779
+ // `f_created_at`(同一时刻写入,口径一致)。
769
780
  async listSessionRows(query: SessionListRowsQuery = {}): Promise<SessionListRowsPage> {
770
781
  const needle = query.query?.trim().toLowerCase() ?? "";
771
782
  const like = `%${needle}%`;
@@ -812,12 +823,9 @@ export class SqliteBackend implements Backend {
812
823
  };
813
824
  }
814
825
 
815
- /**
816
- * 用量聚合:只读三张派生统计表——事件级用量行给去重口径的总量与「天 × 模型」桶,
817
- * `t_session_usage` / `t_session_counts` 给按会话的行(含 fork 继承前缀)。
818
- * 归属、本地日与「是否被引用」都是物化列,这里不再现算、也不回连事件表。
819
- * 事件级按毫秒时间戳过滤、汇总表按本地日过滤——`sinceMs` 对齐到本地零点时两者等价。
820
- */
826
+ // 用量聚合:只读三张派生统计表——事件级用量行给去重口径的总量与「天 × 模型」桶,`t_session_usage` /
827
+ // `t_session_counts` 给按会话的行(含 fork 继承前缀);归属、本地日与「是否被引用」都是物化列,不再现算、也不
828
+ // 回连事件表。事件级按毫秒过滤、汇总表按本地日过滤,`sinceMs` 对齐本地零点时两者等价。
821
829
  async usageReport(sinceMs?: number): Promise<UsageAggregate> {
822
830
  const usageSince = sinceMs === undefined ? "" : " AND f_created_at >= ?";
823
831
  const params = sinceMs === undefined ? [] : [sinceMs];
@@ -932,7 +940,7 @@ export class SqliteBackend implements Backend {
932
940
  };
933
941
  }
934
942
 
935
- /** token 用量旁路累加:派生表 `t_session_usage`,不在写事务里(失败可丢,表可销毁重建)。 */
943
+ // token 用量旁路累加:派生表 `t_session_usage`,不在写事务里(失败可丢,表可销毁重建)。
936
944
  async incrementSessionUsage(
937
945
  id: SessionId,
938
946
  buckets: readonly SessionUsageBucket[],
@@ -964,7 +972,7 @@ export class SqliteBackend implements Backend {
964
972
  }
965
973
  }
966
974
 
967
- /** 活动计数旁路累加:派生表 `t_session_counts`,同上(四项计数一次落)。 */
975
+ // 活动计数旁路累加:派生表 `t_session_counts`,同上(四项计数一次落)。
968
976
  async incrementSessionCounts(
969
977
  id: SessionId,
970
978
  buckets: readonly SessionCountBucket[],
@@ -991,7 +999,7 @@ export class SqliteBackend implements Backend {
991
999
  }
992
1000
  }
993
1001
 
994
- /** 按会话重算两张汇总表(rewind / fork 之后):先删该会话的行,再从事件表重算。 */
1002
+ // 按会话重算两张汇总表(rewind / fork 之后):先删该会话的行,再从事件表重算。
995
1003
  async rebuildSessionStats(id: SessionId): Promise<void> {
996
1004
  this.db.$client.prepare("DELETE FROM t_session_usage WHERE f_session_id = ?").run(id);
997
1005
  this.db.$client
@@ -1027,17 +1035,21 @@ export class SqliteBackend implements Backend {
1027
1035
  .run(id);
1028
1036
  }
1029
1037
 
1030
- /**
1031
- * 全量重算用量行的引用标记:rewind 截断 / fork 复用 / 会话删除都会让「引用」跨会话变化,
1032
- * 只按本会话判定不够。低频路径,一次全表 UPDATE(3 万行量级)。
1033
- */
1034
- async refreshEventUsageFlags(): Promise<void> {
1035
- const eventId = "t_event_usage.f_event_id";
1036
- this.db.$client.exec(
1037
- `UPDATE t_event_usage SET
1038
- f_referenced = ${withEvent(EVENT_REFERENCED_SQL, eventId)},
1039
- f_subagent = ${withEvent(EVENT_SUBAGENT_SQL, eventId)}`,
1040
- );
1038
+ // 重算这些用量行的引用标记:引用只会在「这些事件行的桥接行集合变化」时才变,所以按事件 id 收口。
1039
+ // 全表 UPDATE(3 万行量级、秒级)没有调用点了:撤回与 fork 都不该动统计。
1040
+ async refreshEventUsageFlags(eventIds: readonly string[]): Promise<void> {
1041
+ for (let at = 0; at < eventIds.length; at += REFRESH_FLAG_CHUNK) {
1042
+ const chunk = eventIds.slice(at, at + REFRESH_FLAG_CHUNK);
1043
+ const placeholders = chunk.map(() => "?").join(", ");
1044
+ this.db.$client
1045
+ .prepare(
1046
+ `UPDATE t_event_usage SET
1047
+ f_referenced = ${withEvent(EVENT_REFERENCED_SQL, "f_event_id")},
1048
+ f_subagent = ${withEvent(EVENT_SUBAGENT_SQL, "f_event_id")}
1049
+ WHERE f_event_id IN (${placeholders})`,
1050
+ )
1051
+ .run(...chunk);
1052
+ }
1041
1053
  }
1042
1054
 
1043
1055
  async deleteSessionStats(id: SessionId): Promise<void> {
@@ -27,7 +27,7 @@ type CurrentCheckpointIdentity = CheckpointIdentity & {
27
27
  inheritedEventCount: number;
28
28
  };
29
29
 
30
- /** header 能证明的生命周期身份(上游 0.1.7 的 header-only 匹配口径)。 */
30
+ // header 能证明的生命周期身份(header-only 匹配口径)。
31
31
  type LifecycleIdentity = CheckpointIdentity & {
32
32
  formatVersion: number;
33
33
  isSeeded: boolean;
@@ -101,10 +101,8 @@ export class SessionProjectionCacheRdb extends Service {
101
101
  return identityMatches(record.identity, expected) ? record : undefined;
102
102
  }
103
103
 
104
- /**
105
- * header-only 读(会话列表这类):上游 0.1.7 起只给 header 与 keys——它要求的是**生命周期**身份,
106
- * 不再要求调用方提供 inherited cut(那个 cut 只有写路径与 hydration 用得上)。
107
- */
104
+ // header-only 读(会话列表这类):只给 header 与 keys,要求的是**生命周期**身份,不需要调用方提供
105
+ // inherited cut(那个只有写路径与 hydration 用得上)。
108
106
  cachedSnapshot(
109
107
  meta: SessionHeader,
110
108
  keys?: readonly Extract<keyof SessionProjectionMap, string>[],
@@ -336,7 +334,7 @@ function detachJson(
336
334
  return JSON.parse(text) as Record<string, ProjectionCheckpointRow>;
337
335
  }
338
336
 
339
- /** header 能证明的生命周期身份(不含 inherited cut):header-only 读只比对它。 */
337
+ // header 能证明的生命周期身份(不含 inherited cut):header-only 读只比对它。
340
338
  function lifecycleIdentityOf(header: SessionHeader): LifecycleIdentity {
341
339
  return {
342
340
  formatVersion: header.version,
@@ -382,8 +380,8 @@ function lifecycleIdentityMatches(
382
380
  stored: CheckpointIdentity,
383
381
  expected: LifecycleIdentity,
384
382
  ): boolean {
385
- // header-only 读的匹配口径(上游 0.1.7):只看 formatVersion 与生命周期字段,**不比对 inherited cut**
386
- // ——那个 cut 只有写路径与 hydration 用得上。
383
+ // header-only 读的匹配口径:只看 formatVersion 与生命周期字段,**不比对 inherited cut**(那个只有写路径与
384
+ // hydration 用得上)。
387
385
  return (
388
386
  stored.createdAt === expected.createdAt &&
389
387
  stored.cwd === expected.cwd &&
@@ -21,7 +21,7 @@ import type {
21
21
  export interface StorageRepositoryHost {
22
22
  db: () => Promise<unknown>;
23
23
 
24
- /** 不经 await、直接拿到的数据库句柄;未开放时只走异步的 `db`。 */
24
+ // 不经 await、直接拿到的数据库句柄;未开放时只走异步的 `db`。
25
25
  dbDirect?: () => unknown;
26
26
 
27
27
  tables: Record<string, unknown>;
@@ -273,7 +273,7 @@ export function createStorageRepository(host: StorageRepositoryHost): StorageRep
273
273
 
274
274
  // 钉住:未在集合里的清掉,集合里的按数组位置写序号(读回即按它排序)。
275
275
  // 找不到会话行的 id 跳过:pin 只作用于已存在的会话(与归档不同,不凭空造行)。
276
- // 旧 storages 文档(0.1.6 时代)没有这个集合,导入路径按空处理。
276
+ // 既有 storages 文档里可能没有这个集合,导入路径按空处理。
277
277
  const pinned = state.pinnedSessionIds ?? [];
278
278
  await runQuery(
279
279
  pinned.length === 0
@@ -37,7 +37,7 @@ export interface StorageRepository {
37
37
 
38
38
  loadProjcache(): Promise<StoredProjcacheEntry[]>;
39
39
 
40
- /** 不经 await、直接读的路径(供同步的 `cachedSnapshot` 消费);宿主提供不了则缺省。 */
40
+ // 不经 await、直接读的路径(供同步的 `cachedSnapshot` 消费);宿主提供不了则缺省。
41
41
  readProjcacheDirect?(sessionId: string): StoredProjcacheEntry | undefined;
42
42
 
43
43
  readSessionTitleDirect?(sessionId: string): { title: string; seq: number } | undefined;
package/src/usage.ts CHANGED
@@ -1,12 +1,11 @@
1
1
  import type { Context } from "@deepseek-ai/cordis";
2
+ import { SESSION_USAGE_PATH } from "./routes.ts";
2
3
  import type { SessionPersistenceRdb } from "./index.ts";
3
4
 
4
- export const SESSION_USAGE_PATH = "/api/session.usage";
5
+ export { SESSION_USAGE_PATH };
5
6
 
6
- /**
7
- * 时间范围的语义键:`all` 不限;`day` / `week` 是**本地自然日 / 自然周**(周一起算);
8
- * `7d` / `30d` / `90d` 是**最近 N 个自然日**(含今天)。
9
- */
7
+ // 时间范围的语义键:`all` 不限;`day` / `week` 是**本地自然日 / 自然周**(周一起算);
8
+ // `7d` / `30d` / `90d` 是**最近 N 个自然日**(含今天)。
10
9
  export type UsageRangeKey = "all" | "day" | "week" | "7d" | "30d" | "90d";
11
10
 
12
11
  const ROLLING_DAYS: Record<"7d" | "30d" | "90d", number> = { "7d": 7, "30d": 30, "90d": 90 };
@@ -17,14 +16,9 @@ function localDayStart(now: number): Date {
17
16
  return start;
18
17
  }
19
18
 
20
- /**
21
- * 范围起点(含),不限时为 undefined。**所有起点都对齐到 host 本地时区的零点**——`day` / `week` 是
22
- * 今天 / 本周一的零点,`7d` / `30d` / `90d` 是「今天零点往前 N-1 天」(含今天共 N 个自然日)。
23
- * 对齐的意义:事件级表按毫秒过滤、会话汇总表按本地日过滤,两者因此严格等价。
24
- * @param range - 语义键。
25
- * @param now - 当前时刻。
26
- * @returns 起点毫秒时间戳,或 undefined。
27
- */
19
+ // 范围起点(含),`all` 时 undefined。**所有起点都对齐到 host 本地时区的零点**——`day` / `week` 是今天 / 本周一
20
+ // 的零点,`7d` / `30d` / `90d` 是「今天零点往前 N-1 天」(含今天共 N 个自然日):事件级表按毫秒过滤、会话汇总表
21
+ // 按本地日过滤,两者因此严格等价。
28
22
  export function resolveUsageSince(range: UsageRangeKey, now: number): number | undefined {
29
23
  switch (range) {
30
24
  case "all":
@@ -52,7 +46,7 @@ function parseUsageRange(value: unknown): UsageRangeKey {
52
46
  : "all";
53
47
  }
54
48
 
55
- /** 一段 token 用量(字段直接取事件里模型报的 usage)。 */
49
+ // 一段 token 用量(字段直接取事件里模型报的 usage)。
56
50
  export interface UsageTokenTotals {
57
51
  inputTokens: number;
58
52
  outputTokens: number;
@@ -61,10 +55,8 @@ export interface UsageTokenTotals {
61
55
  totalTokens: number;
62
56
  }
63
57
 
64
- /**
65
- * 活动计数:与 token 用量同一时间范围、同一去重口径(只算被会话引用的事件行),
66
- * 按事件类型数出来——「轮次 / 步骤 / 用户输入 / 工具调用」比「事件行数」有信息量。
67
- */
58
+ // 活动计数:与 token 用量同一时间范围、同一去重口径(只算被会话引用的事件行),
59
+ // 按事件类型数出来——「轮次 / 步骤 / 用户输入 / 工具调用」比「事件行数」有信息量。
68
60
  export interface UsageActivityTotals {
69
61
  turns: number;
70
62
  steps: number;
@@ -72,13 +64,11 @@ export interface UsageActivityTotals {
72
64
  toolCalls: number;
73
65
  }
74
66
 
75
- /** 总量与按会话行用的一整套指标(token + 活动)。 */
67
+ // 总量与按会话行用的一整套指标(token + 活动)。
76
68
  export interface UsageTotals extends UsageTokenTotals, UsageActivityTotals {}
77
69
 
78
- /**
79
- * 一天 × 一个模型 × 是否子代理 的用量桶:总览、按天、按模型都由它折叠。
80
- * 只有 token 用量——活动计数没有模型归属(一个轮次可能跨模型),挂在总量与按会话行上。
81
- */
70
+ // 一天 × 一个模型 × 是否子代理 的用量桶:总览、按天、按模型都由它折叠。
71
+ // 只有 token 用量——活动计数没有模型归属(一个轮次可能跨模型),挂在总量与按会话行上。
82
72
  export interface UsageBucket extends UsageTokenTotals {
83
73
  day: string;
84
74
  provider: string | null;
@@ -86,7 +76,7 @@ export interface UsageBucket extends UsageTokenTotals {
86
76
  subagent: boolean;
87
77
  }
88
78
 
89
- /** 一条会话的用量行。 */
79
+ // 一条会话的用量行。
90
80
  export interface UsageSessionRow extends UsageTotals {
91
81
  sessionId: string;
92
82
  title: string | null;
@@ -94,7 +84,7 @@ export interface UsageSessionRow extends UsageTotals {
94
84
  archived: boolean;
95
85
  }
96
86
 
97
- /** 后端的原始聚合结果(总量 / 其中子代理 / 人类、按天×模型的桶、按会话的行)。 */
87
+ // 后端的原始聚合结果(总量 / 其中子代理 / 人类、按天×模型的桶、按会话的行)。
98
88
  export interface UsageAggregate {
99
89
  totals: UsageTotals;
100
90
  subagent: UsageTotals;
@@ -103,10 +93,10 @@ export interface UsageAggregate {
103
93
  sessions: UsageSessionRow[];
104
94
  }
105
95
 
106
- /** 一次统计请求的完整回报。 */
96
+ // 一次统计请求的完整回报。
107
97
  export type SessionUsageReport = UsageAggregate;
108
98
 
109
- /** 计入活动计数的事件类型(轮次 / 步骤 / 用户输入 / 工具调用)。 */
99
+ // 计入活动计数的事件类型(轮次 / 步骤 / 用户输入 / 工具调用)。
110
100
  export const COUNTED_EVENT_TYPES = [
111
101
  "turn/start",
112
102
  "step/start",
@@ -114,7 +104,7 @@ export const COUNTED_EVENT_TYPES = [
114
104
  "tool/call",
115
105
  ] as const;
116
106
 
117
- /** 按事件类型把计数累加到活动指标上(表里按类型存,读的时候折成四项)。 */
107
+ // 按事件类型把计数累加到活动指标上(表里按类型存,读的时候折成四项)。
118
108
  export function addActivityCount(
119
109
  target: UsageActivityTotals,
120
110
  type: string,
@@ -138,7 +128,7 @@ export function addActivityCount(
138
128
  }
139
129
  }
140
130
 
141
- /** 毫秒时间戳 → host 本地日(`YYYY-MM-DD`,与 SQL 的 localtime 口径一致)。 */
131
+ // 毫秒时间戳 → host 本地日(`YYYY-MM-DD`,与 SQL 的 localtime 口径一致)。
142
132
  export function localDayKey(ms: number): string {
143
133
  const date = new Date(ms);
144
134
  const pad = (value: number): string => String(value).padStart(2, "0");
@@ -179,17 +169,15 @@ export function addActivityTotals(
179
169
  return target;
180
170
  }
181
171
 
182
- /** 把一行的整套指标累加进目标(后端合并「是否子代理」两组时用)。 */
172
+ // 把一行的整套指标累加进目标(后端合并「是否子代理」两组时用)。
183
173
  export function addTotals(target: UsageTotals, row: UsageTotals): UsageTotals {
184
174
  addActivityTotals(target, row);
185
175
  addTokenTotals(target, row);
186
176
  return target;
187
177
  }
188
178
 
189
- /**
190
- * 用量统计通道:一次请求回报总览(含 subagent 拆分)、按天 × 模型的桶与按会话的行。
191
- * 聚合只算被会话引用的事件行——fork 共享行因此只计一次,已删会话留下的孤儿行不计。
192
- */
179
+ // 用量统计通道:一次请求回报总览(含 subagent 拆分)、按天 × 模型的桶与按会话的行。
180
+ // 聚合只算被会话引用的事件行——fork 共享行因此只计一次,已删会话留下的孤儿行不计。
193
181
  export function registerSessionUsage(ctx: Context, persistence: SessionPersistenceRdb): void {
194
182
  ctx.inject(["webServer", "connection"] as const, (webCtx) => {
195
183
  const webServer = webCtx.webServer as unknown as {
@@ -7,7 +7,7 @@ export class WriteGuard {
7
7
  this.headSeqs.set(id, head);
8
8
  }
9
9
 
10
- /** 本实例有没有这条会话的 head 记录(迁移重写用它决定是"登记基准"还是"严格比较")。 */
10
+ // 本实例有没有这条会话的 head 记录(迁移重写用它决定是"登记基准"还是"严格比较")。
11
11
  has(id: SessionId): boolean {
12
12
  return this.headSeqs.has(id);
13
13
  }