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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/README.md +17 -10
  2. package/dist/artifact.d.mts +2 -101
  3. package/dist/artifact.mjs +2 -2
  4. package/dist/{branch-BjUR6wfe.mjs → branch-7eOh_tbO.mjs} +3 -3
  5. package/dist/deletion.d.mts +1 -1
  6. package/dist/deletion.mjs +1 -1
  7. package/dist/import.d.mts +1 -1
  8. package/dist/import.mjs +1 -1
  9. package/dist/index.d.mts +1 -1
  10. package/dist/index.mjs +3 -3
  11. package/dist/{backend-E0bmTXpz.d.mts → log-BbleqD1M.d.mts} +171 -24
  12. package/dist/{log-qCNkDjh5.mjs → log-DTcvsMz7.mjs} +189 -12
  13. package/dist/{sqlite-zcXVyMxr.mjs → sqlite-ChM_sHAG.mjs} +320 -259
  14. package/dist/{src-B9X8BKyh.mjs → src-BWqePRA7.mjs} +290 -162
  15. package/dist/storage.d.mts +20 -7
  16. package/dist/storage.mjs +2 -2
  17. package/dist/testing.d.mts +1 -1
  18. package/dist/testing.mjs +1 -1
  19. package/drizzle/postgres/20260923120000_v3_usage_materialized/migration.sql +48 -0
  20. package/drizzle/sqlite/20260923120000_v3_usage_materialized/migration.sql +48 -0
  21. package/package.json +20 -20
  22. package/src/backend.ts +41 -17
  23. package/src/branch.ts +4 -4
  24. package/src/drizzle/postgres-v3.ts +2 -0
  25. package/src/drizzle/sqlite-v3.ts +2 -0
  26. package/src/entities/v3/event-usage.ts +8 -1
  27. package/src/entities/v3/index.ts +8 -0
  28. package/src/entities/v3/session-counts.ts +23 -0
  29. package/src/entities/v3/session-usage.ts +29 -0
  30. package/src/index.ts +109 -43
  31. package/src/log.ts +58 -17
  32. package/src/postgres.ts +248 -149
  33. package/src/rows.ts +2 -1
  34. package/src/schema.ts +4 -0
  35. package/src/sqlite.ts +263 -124
  36. package/src/storage-takeover/repository.ts +1 -4
  37. package/src/usage.ts +10 -6
@@ -1,4 +1,4 @@
1
- import { A as eventKind, B as tWorkspaceState, C as EVENT_ENCODING, D as SCHEMA_VERSION, E as JournalMode, F as tSessionEvents, H as WriteGuard, I as tSessionProjcacheRows, L as tSessions, M as tEvents, N as tPersistenceState, O as SESSION_PERSISTENCE_SQLITE_APPLICATION_ID, P as tSchemaMeta, R as tStorageUnits, S as DEFAULT_BUSY_TIMEOUT_MS, T as EventRole, U as StorageRepository, V as tWorkspaces, a as SessionListRowsPage, c as UsageAggregate, i as EventRow, j as tEventUsage, k as eventDimensions, n as BackendTx, o as SessionListRowsQuery, r as EventCountBucket, s as SessionRow, t as Backend, w as EventKind, z as tWorkspaceSessions } from "./backend-E0bmTXpz.mjs";
1
+ import { $ as eventDimensions, A as SessionRow, D as SessionCountBucket, E as EventRow, J as EventKind, K as DEFAULT_BUSY_TIMEOUT_MS, M as UsageAggregate, O as SessionListRowsPage, Q as SESSION_PERSISTENCE_SQLITE_APPLICATION_ID, T as BackendTx, X as JournalMode, Y as EventRole, Z as SCHEMA_VERSION, at as tSessionCounts, ct as tSessionUsage, dt as tWorkspaceSessions, et as eventKind, ft as tWorkspaceState, ht as StorageRepository, it as tSchemaMeta, j as SessionUsageBucket, k as SessionListRowsQuery, lt as tSessions, mt as WriteGuard, nt as tEvents, ot as tSessionEvents, pt as tWorkspaces, q as EVENT_ENCODING, rt as tPersistenceState, st as tSessionProjcacheRows, tt as tEventUsage, ut as tStorageUnits, w as Backend } from "./log-BbleqD1M.mjs";
2
2
  import { SessionId } from "@deepseek-ai/dsh-session";
3
3
  import { DatabaseSync } from "node:sqlite";
4
4
  //#region src/sqlite.d.ts
@@ -53,14 +53,27 @@ declare class SqliteBackend implements Backend {
53
53
  * `f_created_at`(同一时刻写入,口径一致)。
54
54
  */
55
55
  listSessionRows(query?: SessionListRowsQuery): Promise<SessionListRowsPage>;
56
+ /**
57
+ * 用量聚合:只读三张派生统计表——事件级用量行给去重口径的总量与「天 × 模型」桶,
58
+ * `t_session_usage` / `t_session_counts` 给按会话的行(含 fork 继承前缀)。
59
+ * 归属、本地日与「是否被引用」都是物化列,这里不再现算、也不回连事件表。
60
+ * 事件级按毫秒时间戳过滤、汇总表按本地日过滤——`sinceMs` 对齐到本地零点时两者等价。
61
+ */
56
62
  usageReport(sinceMs?: number): Promise<UsageAggregate>;
57
- /** 活动计数旁路累加:派生表 `t_event_counts`,不在写事务里(失败可丢,表可销毁重建)。 */
58
- incrementEventCounts(id: SessionId, buckets: readonly EventCountBucket[]): Promise<void>;
59
- /** 按会话重算活动计数(rewind / fork 之后):先删该会话的行,再从事件表重算。 */
60
- rebuildEventCounts(id: SessionId): Promise<void>;
61
- deleteEventCounts(id: SessionId): Promise<void>;
63
+ /** token 用量旁路累加:派生表 `t_session_usage`,不在写事务里(失败可丢,表可销毁重建)。 */
64
+ incrementSessionUsage(id: SessionId, buckets: readonly SessionUsageBucket[]): Promise<void>;
65
+ /** 活动计数旁路累加:派生表 `t_session_counts`,同上(四项计数一次落)。 */
66
+ incrementSessionCounts(id: SessionId, buckets: readonly SessionCountBucket[]): Promise<void>;
67
+ /** 按会话重算两张汇总表(rewind / fork 之后):先删该会话的行,再从事件表重算。 */
68
+ rebuildSessionStats(id: SessionId): Promise<void>;
69
+ /**
70
+ * 全量重算用量行的引用标记:rewind 截断 / fork 复用 / 会话删除都会让「引用」跨会话变化,
71
+ * 只按本会话判定不够。低频路径,一次全表 UPDATE(3 万行量级)。
72
+ */
73
+ refreshEventUsageFlags(): Promise<void>;
74
+ deleteSessionStats(id: SessionId): Promise<void>;
62
75
  vacuum(): Promise<void>;
63
76
  private eventRows;
64
77
  }
65
78
  //#endregion
66
- export { DEFAULT_BUSY_TIMEOUT_MS, EVENT_ENCODING, EventKind, EventRole, type EventRow, JournalMode, SCHEMA_VERSION, SESSION_PERSISTENCE_SQLITE_APPLICATION_ID, type SessionRow, SqliteBackend, SqliteBackendOptions, WriteGuard, eventDimensions, eventKind, openDatabase, tEventUsage, tEvents, tPersistenceState, tSchemaMeta, tSessionEvents, tSessionProjcacheRows, tSessions, tStorageUnits, tWorkspaceSessions, tWorkspaceState, tWorkspaces };
79
+ export { DEFAULT_BUSY_TIMEOUT_MS, EVENT_ENCODING, EventKind, EventRole, type EventRow, JournalMode, SCHEMA_VERSION, SESSION_PERSISTENCE_SQLITE_APPLICATION_ID, type SessionRow, SqliteBackend, SqliteBackendOptions, WriteGuard, eventDimensions, eventKind, openDatabase, tEventUsage, tEvents, tPersistenceState, tSchemaMeta, tSessionCounts, tSessionEvents, tSessionProjcacheRows, tSessionUsage, tSessions, tStorageUnits, tWorkspaceSessions, tWorkspaceState, tWorkspaces };
package/dist/storage.mjs CHANGED
@@ -1,2 +1,2 @@
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-zcXVyMxr.mjs";
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
+ import { S as tWorkspaces, T as WriteGuard, _ as tSessionUsage, a as EVENT_ENCODING, b as tWorkspaceSessions, c as eventDimensions, d as tEvents, f as tPersistenceState, g as tSessionProjcacheRows, h as tSessionEvents, i as DEFAULT_BUSY_TIMEOUT_MS, l as eventKind, m as tSessionCounts, 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 tSessions, x as tWorkspaceState, y as tStorageUnits } from "./sqlite-ChM_sHAG.mjs";
2
+ export { DEFAULT_BUSY_TIMEOUT_MS, EVENT_ENCODING, SCHEMA_VERSION, SESSION_PERSISTENCE_SQLITE_APPLICATION_ID, SqliteBackend, WriteGuard, eventDimensions, eventKind, openDatabase, tEventUsage, tEvents, tPersistenceState, tSchemaMeta, tSessionCounts, tSessionEvents, tSessionProjcacheRows, tSessionUsage, tSessions, tStorageUnits, tWorkspaceSessions, tWorkspaceState, tWorkspaces };
@@ -1,4 +1,4 @@
1
- import { v as truncateLiveSession } from "./backend-E0bmTXpz.mjs";
1
+ import { H as truncateLiveSession } from "./log-BbleqD1M.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";
package/dist/testing.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { a as truncateLiveSession } from "./branch-BjUR6wfe.mjs";
1
+ import { a as truncateLiveSession } from "./branch-7eOh_tbO.mjs";
2
2
  import { SessionAlreadyExistsError, SessionAlreadyOwnedError, SessionFormatUnsupportedError, SessionHandleClosedError, SessionPersistenceNotFoundError, SessionReadOnlyError } from "@deepseek-ai/dsh-session-persistence";
3
3
  import SessionStore, { SESSION_FORMAT_VERSION, SessionId, SessionLogOffset, SessionSeq } from "@deepseek-ai/dsh-session";
4
4
  import { Context } from "@deepseek-ai/cordis";
@@ -0,0 +1,48 @@
1
+ -- 统计衍生表:纯派生数据(打开库时按事件表全量回填),迁移直接先删后建,
2
+ -- 不做 ALTER / 数据搬迁。结构与取舍见 ADR-统计衍生表物化归属与汇总。
3
+ DROP TABLE IF EXISTS "t_event_usage";--> statement-breakpoint
4
+ DROP TABLE IF EXISTS "t_event_counts";--> statement-breakpoint
5
+ DROP TABLE IF EXISTS "t_session_usage";--> statement-breakpoint
6
+ DROP TABLE IF EXISTS "t_session_counts";--> statement-breakpoint
7
+ CREATE TABLE "t_event_usage" (
8
+ "f_event_id" text PRIMARY KEY NOT NULL,
9
+ "f_created_at" bigint NOT NULL,
10
+ "f_day" text NOT NULL,
11
+ "f_provider" text,
12
+ "f_model" text,
13
+ "f_referenced" integer DEFAULT 0 NOT NULL,
14
+ "f_subagent" integer DEFAULT 0 NOT NULL,
15
+ "f_input_tokens" integer DEFAULT 0 NOT NULL,
16
+ "f_output_tokens" integer DEFAULT 0 NOT NULL,
17
+ "f_cache_read_tokens" integer DEFAULT 0 NOT NULL,
18
+ "f_reasoning_tokens" integer DEFAULT 0 NOT NULL,
19
+ "f_total_tokens" integer DEFAULT 0 NOT NULL,
20
+ CONSTRAINT "fk_t_event_usage_f_event_id_t_events_f_event_id_fk" FOREIGN KEY ("f_event_id") REFERENCES "t_events"("f_event_id") ON DELETE CASCADE
21
+ );
22
+ --> statement-breakpoint
23
+ CREATE INDEX "idx_event_usage_created_at" ON "t_event_usage" USING btree ("f_created_at");--> statement-breakpoint
24
+ CREATE TABLE "t_session_usage" (
25
+ "f_session_id" text NOT NULL,
26
+ "f_day" text NOT NULL,
27
+ "f_provider" text DEFAULT '' NOT NULL,
28
+ "f_model" text DEFAULT '' NOT NULL,
29
+ "f_input_tokens" integer DEFAULT 0 NOT NULL,
30
+ "f_output_tokens" integer DEFAULT 0 NOT NULL,
31
+ "f_cache_read_tokens" integer DEFAULT 0 NOT NULL,
32
+ "f_reasoning_tokens" integer DEFAULT 0 NOT NULL,
33
+ "f_total_tokens" integer DEFAULT 0 NOT NULL,
34
+ CONSTRAINT "fk_t_session_usage_f_session_id_t_sessions_f_session_id_fk" FOREIGN KEY ("f_session_id") REFERENCES "t_sessions"("f_session_id") ON DELETE CASCADE
35
+ );
36
+ --> statement-breakpoint
37
+ CREATE UNIQUE INDEX "uq_session_usage_key" ON "t_session_usage" ("f_session_id","f_day","f_provider","f_model");--> statement-breakpoint
38
+ CREATE TABLE "t_session_counts" (
39
+ "f_session_id" text NOT NULL,
40
+ "f_day" text NOT NULL,
41
+ "f_turns" integer DEFAULT 0 NOT NULL,
42
+ "f_steps" integer DEFAULT 0 NOT NULL,
43
+ "f_user_inputs" integer DEFAULT 0 NOT NULL,
44
+ "f_tool_calls" integer DEFAULT 0 NOT NULL,
45
+ CONSTRAINT "fk_t_session_counts_f_session_id_t_sessions_f_session_id_fk" FOREIGN KEY ("f_session_id") REFERENCES "t_sessions"("f_session_id") ON DELETE CASCADE
46
+ );
47
+ --> statement-breakpoint
48
+ CREATE UNIQUE INDEX "uq_session_counts_key" ON "t_session_counts" ("f_session_id","f_day");
@@ -0,0 +1,48 @@
1
+ -- 统计衍生表:纯派生数据(打开库时按事件表全量回填),迁移直接先删后建,
2
+ -- 不做 ALTER / 数据搬迁。结构与取舍见 ADR-统计衍生表物化归属与汇总。
3
+ DROP TABLE IF EXISTS `t_event_usage`;--> statement-breakpoint
4
+ DROP TABLE IF EXISTS `t_event_counts`;--> statement-breakpoint
5
+ DROP TABLE IF EXISTS `t_session_usage`;--> statement-breakpoint
6
+ DROP TABLE IF EXISTS `t_session_counts`;--> statement-breakpoint
7
+ CREATE TABLE `t_event_usage` (
8
+ `f_event_id` text PRIMARY KEY NOT NULL,
9
+ `f_created_at` bigint NOT NULL,
10
+ `f_day` text NOT NULL,
11
+ `f_provider` text,
12
+ `f_model` text,
13
+ `f_referenced` integer DEFAULT 0 NOT NULL,
14
+ `f_subagent` integer DEFAULT 0 NOT NULL,
15
+ `f_input_tokens` integer DEFAULT 0 NOT NULL,
16
+ `f_output_tokens` integer DEFAULT 0 NOT NULL,
17
+ `f_cache_read_tokens` integer DEFAULT 0 NOT NULL,
18
+ `f_reasoning_tokens` integer DEFAULT 0 NOT NULL,
19
+ `f_total_tokens` integer DEFAULT 0 NOT NULL,
20
+ CONSTRAINT `fk_t_event_usage_f_event_id_t_events_f_event_id_fk` FOREIGN KEY (`f_event_id`) REFERENCES `t_events`(`f_event_id`) ON DELETE CASCADE
21
+ );
22
+ --> statement-breakpoint
23
+ CREATE INDEX `idx_event_usage_created_at` ON `t_event_usage` (`f_created_at`);--> statement-breakpoint
24
+ CREATE TABLE `t_session_usage` (
25
+ `f_session_id` text NOT NULL,
26
+ `f_day` text NOT NULL,
27
+ `f_provider` text DEFAULT '' NOT NULL,
28
+ `f_model` text DEFAULT '' NOT NULL,
29
+ `f_input_tokens` integer DEFAULT 0 NOT NULL,
30
+ `f_output_tokens` integer DEFAULT 0 NOT NULL,
31
+ `f_cache_read_tokens` integer DEFAULT 0 NOT NULL,
32
+ `f_reasoning_tokens` integer DEFAULT 0 NOT NULL,
33
+ `f_total_tokens` integer DEFAULT 0 NOT NULL,
34
+ CONSTRAINT `fk_t_session_usage_f_session_id_t_sessions_f_session_id_fk` FOREIGN KEY (`f_session_id`) REFERENCES `t_sessions`(`f_session_id`) ON DELETE CASCADE
35
+ );
36
+ --> statement-breakpoint
37
+ CREATE UNIQUE INDEX `uq_session_usage_key` ON `t_session_usage` (`f_session_id`,`f_day`,`f_provider`,`f_model`);--> statement-breakpoint
38
+ CREATE TABLE `t_session_counts` (
39
+ `f_session_id` text NOT NULL,
40
+ `f_day` text NOT NULL,
41
+ `f_turns` integer DEFAULT 0 NOT NULL,
42
+ `f_steps` integer DEFAULT 0 NOT NULL,
43
+ `f_user_inputs` integer DEFAULT 0 NOT NULL,
44
+ `f_tool_calls` integer DEFAULT 0 NOT NULL,
45
+ CONSTRAINT `fk_t_session_counts_f_session_id_t_sessions_f_session_id_fk` FOREIGN KEY (`f_session_id`) REFERENCES `t_sessions`(`f_session_id`) ON DELETE CASCADE
46
+ );
47
+ --> statement-breakpoint
48
+ CREATE UNIQUE INDEX `uq_session_counts_key` ON `t_session_counts` (`f_session_id`,`f_day`);
package/package.json CHANGED
@@ -1,14 +1,14 @@
1
1
  {
2
2
  "name": "@morlay/session-rdb",
3
- "version": "0.0.21-alpha.7",
3
+ "version": "0.0.21-alpha.8",
4
4
  "description": "RDB durable session backend for DeepSeek Harness: implements both session-persistence and session-branch (rewind / retry / fork) providers.",
5
5
  "keywords": [
6
6
  "branch",
7
7
  "dsh",
8
8
  "dsh-plugin",
9
- "dsh-session-rdb",
10
9
  "rdb",
11
10
  "rewind",
11
+ "session-persistence",
12
12
  "session-rdb"
13
13
  ],
14
14
  "license": "MIT",
@@ -41,34 +41,34 @@
41
41
  "./locale/*.json": "./locale/*.json"
42
42
  },
43
43
  "dependencies": {
44
- "@morlay/session-branch": "0.0.10-alpha.7",
44
+ "@morlay/session-branch": "0.0.10-alpha.8",
45
45
  "drizzle-orm": "^1.0.0-rc.5-ab785fc",
46
46
  "fflate": "^0.8.2",
47
47
  "pg": "^8.23.0"
48
48
  },
49
49
  "devDependencies": {
50
- "@deepseek-ai/dsh-compaction": "0.1.7-alpha.2",
51
- "@deepseek-ai/dsh-session-projection": "0.1.7-alpha.2",
52
- "@deepseek-ai/dsh-session-projection-cache": "0.1.7-alpha.2",
53
- "@deepseek-ai/dsh-session-turn-outline": "0.1.7-alpha.2",
54
- "@deepseek-ai/dsh-storage": "0.1.7-alpha.2",
55
- "@deepseek-ai/dsh-storage-domain": "0.1.7-alpha.2",
56
- "@deepseek-ai/dsh-token-meter": "0.1.7-alpha.2",
57
- "@deepseek-ai/dsh-workspace": "0.1.7-alpha.2",
50
+ "@deepseek-ai/dsh-compaction": "0.1.7-rc.1",
51
+ "@deepseek-ai/dsh-session-projection": "0.1.7-rc.1",
52
+ "@deepseek-ai/dsh-session-projection-cache": "0.1.7-rc.1",
53
+ "@deepseek-ai/dsh-session-turn-outline": "0.1.7-rc.1",
54
+ "@deepseek-ai/dsh-storage": "0.1.7-rc.1",
55
+ "@deepseek-ai/dsh-storage-domain": "0.1.7-rc.1",
56
+ "@deepseek-ai/dsh-token-meter": "0.1.7-rc.1",
57
+ "@deepseek-ai/dsh-workspace": "0.1.7-rc.1",
58
58
  "@types/pg": "^8.23.1",
59
59
  "drizzle-kit": "1.0.0-rc.5-ab785fc"
60
60
  },
61
61
  "peerDependencies": {
62
62
  "@deepseek-ai/cordis": "4.0.4",
63
- "@deepseek-ai/dsh-invariants": "0.1.7-alpha.2",
64
- "@deepseek-ai/dsh-llm": "0.1.7-alpha.2",
65
- "@deepseek-ai/dsh-scope": "0.1.7-alpha.2",
66
- "@deepseek-ai/dsh-session": "0.1.7-alpha.2",
67
- "@deepseek-ai/dsh-session-format": "0.1.7-alpha.2",
68
- "@deepseek-ai/dsh-session-format-catalog": "0.1.7-alpha.2",
69
- "@deepseek-ai/dsh-session-persistence": "0.1.7-alpha.2",
70
- "@deepseek-ai/dsh-session-query": "0.1.7-alpha.2",
71
- "@deepseek-ai/dsh-storage": "0.1.7-alpha.2",
63
+ "@deepseek-ai/dsh-invariants": "0.1.7-rc.1",
64
+ "@deepseek-ai/dsh-llm": "0.1.7-rc.1",
65
+ "@deepseek-ai/dsh-scope": "0.1.7-rc.1",
66
+ "@deepseek-ai/dsh-session": "0.1.7-rc.1",
67
+ "@deepseek-ai/dsh-session-format": "0.1.7-rc.1",
68
+ "@deepseek-ai/dsh-session-format-catalog": "0.1.7-rc.1",
69
+ "@deepseek-ai/dsh-session-persistence": "0.1.7-rc.1",
70
+ "@deepseek-ai/dsh-session-query": "0.1.7-rc.1",
71
+ "@deepseek-ai/dsh-storage": "0.1.7-rc.1",
72
72
  "@deepseek-ai/schemastery": "3.18.4"
73
73
  },
74
74
  "dsh": {
package/src/backend.ts CHANGED
@@ -1,7 +1,8 @@
1
1
  import type { SessionId } from "@deepseek-ai/dsh-session";
2
2
  import type { SessionStorageMetadata } from "@deepseek-ai/dsh-session-persistence";
3
+ import type { EventUsageRow } from "./log.ts";
3
4
  import type { StorageRepository } from "./storage-takeover/types.ts";
4
- import type { UsageAggregate } from "./usage.ts";
5
+ import type { UsageActivityTotals, UsageAggregate, UsageTokenTotals } from "./usage.ts";
5
6
 
6
7
  export interface SessionRow {
7
8
  fSessionId: string;
@@ -74,7 +75,11 @@ export interface BackendTx {
74
75
 
75
76
  refreshTitle(id: SessionId): Promise<void>;
76
77
 
77
- insertEvents(events: EventInsert[]): Promise<void>;
78
+ /**
79
+ * 插入事件行与它们的用量行(`usageRows` 由写路径一次折好,读侧维度已物化在行上)。
80
+ * 这一批事件与桥接行同事务,提交即「被引用」。
81
+ */
82
+ insertEvents(events: EventInsert[], usageRows: readonly EventUsageRow[]): Promise<void>;
78
83
 
79
84
  insertBridges(
80
85
  rows: Array<{
@@ -139,11 +144,16 @@ export interface SessionListRowsPage {
139
144
  total: number;
140
145
  }
141
146
 
142
- /** 活动计数的一个桶:本地日 + 事件类型 + 计数。 */
143
- export interface EventCountBucket {
147
+ /** 活动计数的一个桶:本地日 + 四项计数(轮次 / 步骤 / 用户输入 / 工具调用)。 */
148
+ export interface SessionCountBucket extends UsageActivityTotals {
149
+ day: string;
150
+ }
151
+
152
+ /** 用量累加的一个桶:本地日 + 模型归属 + 五个 token 列。 */
153
+ export interface SessionUsageBucket extends UsageTokenTotals {
144
154
  day: string;
145
- type: string;
146
- count: number;
155
+ provider: string | null;
156
+ model: string | null;
147
157
  }
148
158
 
149
159
  export interface Backend {
@@ -178,9 +188,10 @@ export interface Backend {
178
188
  listOrphanSubagentSessions(): Promise<SessionId[]>;
179
189
 
180
190
  /**
181
- * 用量统计的原始聚合:token 用量读 `t_event_usage`、活动计数读派生表 `t_event_counts`,
182
- * 加按天×模型的桶与按会话的行(事件行去重、排除孤儿)。
183
- * @param sinceMs - 只算该时刻(含)之后的事件行;省略即全量。
191
+ * 用量统计的原始聚合:读三张派生统计表(`t_event_usage` + 两张会话汇总表),
192
+ * 给出总量 / subagent 拆分、按天×模型的桶与按会话的行(事件行去重、排除孤儿)。
193
+ * @param sinceMs - 只算该时刻(含)之后的用量;省略即全量。会话汇总表按本地日过滤,
194
+ * 与毫秒过滤等价的前提是调用方把起点对齐到本地零点(见 `resolveUsageSince`)。
184
195
  */
185
196
  usageReport(sinceMs?: number): Promise<UsageAggregate>;
186
197
 
@@ -188,21 +199,34 @@ export interface Backend {
188
199
  listSessionRows(query?: SessionListRowsQuery): Promise<SessionListRowsPage>;
189
200
 
190
201
  /**
191
- * 活动计数**旁路累加**(派生表 `t_event_counts`,不参与写事务、失败可丢——表可销毁重建):
192
- * 每个 (会话, 本地日, 事件类型) 记一行计数。
202
+ * token 用量**旁路累加**(派生表 `t_session_usage`,不参与写事务、失败可丢——表可销毁重建):
203
+ * 每个 (会话, 本地日, 模型) 记一行累加。
204
+ * @param id - 会话 id。
205
+ * @param buckets - 本批要累加的 (day, provider, model, tokens)。
206
+ */
207
+ incrementSessionUsage(id: SessionId, buckets: readonly SessionUsageBucket[]): Promise<void>;
208
+
209
+ /**
210
+ * 活动计数**旁路累加**(派生表 `t_session_counts`,同上):每个 (会话, 本地日) 累加四项计数。
193
211
  * @param id - 会话 id。
194
- * @param buckets - 本批要累加的 (day, type, count)。
212
+ * @param buckets - 本批要累加的 (day, turns, steps, userInputs, toolCalls)。
195
213
  */
196
- incrementEventCounts(id: SessionId, buckets: readonly EventCountBucket[]): Promise<void>;
214
+ incrementSessionCounts(id: SessionId, buckets: readonly SessionCountBucket[]): Promise<void>;
197
215
 
198
216
  /**
199
- * 按会话从事件表重算活动计数(rewind / fork 之后调用):先删该会话的行再重算。
217
+ * 按会话从事件表重算两张会话汇总表(rewind / fork 之后调用):先删该会话的行再重算。
200
218
  * @param id - 会话 id。
201
219
  */
202
- rebuildEventCounts(id: SessionId): Promise<void>;
220
+ rebuildSessionStats(id: SessionId): Promise<void>;
221
+
222
+ /**
223
+ * 全量重算 `t_event_usage` 的引用标记(`f_referenced` / `f_subagent`):rewind / fork / 会话删除之后调用。
224
+ * 引用可能跨会话消失(截断、删除),只按本会话判定不够,故这里是全量。
225
+ */
226
+ refreshEventUsageFlags(): Promise<void>;
203
227
 
204
- /** 删掉一个会话的活动计数行(会话删除时)。 */
205
- deleteEventCounts(id: SessionId): Promise<void>;
228
+ /** 删掉一个会话的汇总行(会话删除时;外键 CASCADE 之外再显式清一次)。 */
229
+ deleteSessionStats(id: SessionId): Promise<void>;
206
230
 
207
231
  /** 回收空间与统计(含 VACUUM);不得在事务内执行。 */
208
232
  vacuum(): Promise<void>;
package/src/branch.ts CHANGED
@@ -311,8 +311,8 @@ export class SessionBranchRdbProvider implements SessionBranchProvider {
311
311
  internals.dropReuseEventIds(childId);
312
312
  throw error;
313
313
  }
314
- // 子会话继承了前缀事件:活动计数按子会话重算(派生表,best-effort)。
315
- await this.persistence.rebuildEventCounts(childId);
314
+ // 子会话继承了前缀事件:统计按子会话重算(派生表,best-effort)。
315
+ await this.persistence.rebuildSessionStats(childId);
316
316
  return childId;
317
317
  }
318
318
 
@@ -443,8 +443,8 @@ export class SessionBranchRdbProvider implements SessionBranchProvider {
443
443
  });
444
444
  }
445
445
 
446
- // 截断后该会话的事件集合变了:活动计数跟着重算(派生表,best-effort)。
447
- await this.persistence.rebuildEventCounts(id);
446
+ // 截断后该会话的事件集合变了:统计跟着重算(派生表,best-effort)。
447
+ await this.persistence.rebuildSessionStats(id);
448
448
 
449
449
  return { header: rowToMeta(row), revision: (await internals.readStoredRevision(id))! };
450
450
  }
@@ -14,3 +14,5 @@ export const tWorkspaceSessions = tables["t_workspace_sessions"]!;
14
14
  export const tWorkspaceState = tables["t_workspace_state"]!;
15
15
  export const tSessionProjcacheRows = tables["t_session_projcache_row"]!;
16
16
  export const tEventUsage = tables["t_event_usage"]!;
17
+ export const tSessionUsage = tables["t_session_usage"]!;
18
+ export const tSessionCounts = tables["t_session_counts"]!;
@@ -14,3 +14,5 @@ export const tWorkspaceSessions = tables["t_workspace_sessions"]!;
14
14
  export const tWorkspaceState = tables["t_workspace_state"]!;
15
15
  export const tSessionProjcacheRows = tables["t_session_projcache_row"]!;
16
16
  export const tEventUsage = tables["t_event_usage"]!;
17
+ export const tSessionUsage = tables["t_session_usage"]!;
18
+ export const tSessionCounts = tables["t_session_counts"]!;
@@ -2,7 +2,9 @@ import type { TableDef } from "../../adapters/types.ts";
2
2
 
3
3
  /**
4
4
  * 用量日志:一条 `assistant/message` 事件一行(按事件行唯一,fork 共享行不重复)。
5
- * 统计只读这张表,不再逐行解析事件的 JSON;事件行本身仍留在 `t_events`。
5
+ * 读侧维度全部物化在行上——本地日、是否被会话引用、是否被子代理会话引用——统计因此不做
6
+ * `EXISTS` 判定、也不回连事件表;事件行本身仍留在 `t_events`。
7
+ * 取舍见 ADR-统计衍生表物化归属与汇总。
6
8
  */
7
9
  export const eventUsage: TableDef = {
8
10
  name: "t_event_usage",
@@ -14,8 +16,13 @@ export const eventUsage: TableDef = {
14
16
  },
15
17
  // 毫秒时间戳:SQLite 的 integer 是 64 位,PG 的 integer 只有 int4(21 亿上限)——必须是 bigint。
16
18
  f_created_at: { type: "bigint", notNull: true },
19
+ // host 本地日 `YYYY-MM-DD`(与统计桶的 localtime 口径一致)。
20
+ f_day: { type: "text", notNull: true },
17
21
  f_provider: { type: "text" },
18
22
  f_model: { type: "text" },
23
+ // 被任何会话桥接行引用(孤儿 0,不计入统计)/ 被任何 subagent 会话引用(拆「人类 / 子代理」)。
24
+ f_referenced: { type: "integer", notNull: true, default: 0 },
25
+ f_subagent: { type: "integer", notNull: true, default: 0 },
19
26
  f_input_tokens: { type: "integer", notNull: true, default: 0 },
20
27
  f_output_tokens: { type: "integer", notNull: true, default: 0 },
21
28
  f_cache_read_tokens: { type: "integer", notNull: true, default: 0 },
@@ -9,6 +9,8 @@ import { workspaceSessions } from "./workspace-sessions.ts";
9
9
  import { workspaceState } from "./workspace-state.ts";
10
10
  import { sessionProjcacheRows } from "./session-projcache-rows.ts";
11
11
  import { eventUsage } from "./event-usage.ts";
12
+ import { sessionUsage } from "./session-usage.ts";
13
+ import { sessionCounts } from "./session-counts.ts";
12
14
 
13
15
  export { persistenceState };
14
16
  export { schemaMeta };
@@ -21,6 +23,8 @@ export { workspaceSessions };
21
23
  export { workspaceState };
22
24
  export { sessionProjcacheRows };
23
25
  export { eventUsage };
26
+ export { sessionUsage };
27
+ export { sessionCounts };
24
28
 
25
29
  export const sqliteTableDefs = [
26
30
  persistenceState,
@@ -34,6 +38,8 @@ export const sqliteTableDefs = [
34
38
  workspaceState,
35
39
  sessionProjcacheRows,
36
40
  eventUsage,
41
+ sessionUsage,
42
+ sessionCounts,
37
43
  ] as const;
38
44
 
39
45
  export const postgresTableDefs = [
@@ -48,4 +54,6 @@ export const postgresTableDefs = [
48
54
  workspaceState,
49
55
  sessionProjcacheRows,
50
56
  eventUsage,
57
+ sessionUsage,
58
+ sessionCounts,
51
59
  ] as const;
@@ -0,0 +1,23 @@
1
+ import type { TableDef } from "../../adapters/types.ts";
2
+
3
+ /**
4
+ * 会话 × 本地日的活动计数(派生统计表):轮次 / 步骤 / 用户输入 / 工具调用四列直接读,
5
+ * 不再存事件类型、也不在读取时折。旁路累加、可按会话重建,读侧只读;
6
+ * 取舍见 ADR-统计衍生表物化归属与汇总。
7
+ */
8
+ export const sessionCounts: TableDef = {
9
+ name: "t_session_counts",
10
+ columns: {
11
+ f_session_id: {
12
+ type: "text",
13
+ notNull: true,
14
+ references: { table: "t_sessions", column: "f_session_id", onDelete: "cascade" },
15
+ },
16
+ f_day: { type: "text", notNull: true },
17
+ f_turns: { type: "integer", notNull: true, default: 0 },
18
+ f_steps: { type: "integer", notNull: true, default: 0 },
19
+ f_user_inputs: { type: "integer", notNull: true, default: 0 },
20
+ f_tool_calls: { type: "integer", notNull: true, default: 0 },
21
+ },
22
+ uniques: { uq_session_counts_key: { columns: ["f_session_id", "f_day"] } },
23
+ };
@@ -0,0 +1,29 @@
1
+ import type { TableDef } from "../../adapters/types.ts";
2
+
3
+ /**
4
+ * 会话 × 本地日 × 模型的 token 汇总(派生统计表):**按会话的行**读它,因此保住
5
+ * 「fork 子会话含继承前缀」的口径(各会话行之和 ≥ 总量)而不回连事件表。
6
+ * 旁路累加、可按会话重建,读侧只读;取舍见 ADR-统计衍生表物化归属与汇总。
7
+ */
8
+ export const sessionUsage: TableDef = {
9
+ name: "t_session_usage",
10
+ columns: {
11
+ f_session_id: {
12
+ type: "text",
13
+ notNull: true,
14
+ references: { table: "t_sessions", column: "f_session_id", onDelete: "cascade" },
15
+ },
16
+ f_day: { type: "text", notNull: true },
17
+ // 模型未知时落空串(不是 NULL):唯一键里的 NULL 在 SQLite 下互不冲突,空串才保证一行。
18
+ f_provider: { type: "text", notNull: true, default: "" },
19
+ f_model: { type: "text", notNull: true, default: "" },
20
+ f_input_tokens: { type: "integer", notNull: true, default: 0 },
21
+ f_output_tokens: { type: "integer", notNull: true, default: 0 },
22
+ f_cache_read_tokens: { type: "integer", notNull: true, default: 0 },
23
+ f_reasoning_tokens: { type: "integer", notNull: true, default: 0 },
24
+ f_total_tokens: { type: "integer", notNull: true, default: 0 },
25
+ },
26
+ uniques: {
27
+ uq_session_usage_key: { columns: ["f_session_id", "f_day", "f_provider", "f_model"] },
28
+ },
29
+ };