@morlay/session-rdb 0.1.10-alpha.0 → 0.1.10-alpha.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.
- package/README.md +18 -3
- package/dist/artifact.d.mts +118 -2
- package/dist/artifact.mjs +5 -3
- package/dist/{log-TYgd8Xkr.d.mts → backend-BKtJ5sO5.d.mts} +183 -284
- package/dist/backend-rdb-BDCntVDy.mjs +1341 -0
- package/dist/branch-CG1PUlO6.mjs +833 -0
- package/dist/deletion-SSX7IqU8.mjs +35 -0
- package/dist/deletion.d.mts +1 -1
- package/dist/deletion.mjs +2 -2
- package/dist/export-CfJQjQfr.mjs +123 -0
- package/dist/export.d.mts +1 -1
- package/dist/export.mjs +31 -55
- package/dist/import.d.mts +1 -1
- package/dist/import.mjs +31 -76
- package/dist/index-CUD2ZpTA.d.mts +71 -0
- package/dist/index.d.mts +20 -2
- package/dist/index.mjs +1561 -4
- package/dist/open-YoyD_kah.mjs +561 -0
- package/dist/organize.d.mts +9 -0
- package/dist/organize.mjs +30 -0
- package/dist/route-DpfY0zq-.mjs +114 -0
- package/dist/storage.d.mts +41 -48
- package/dist/storage.mjs +3 -1
- package/dist/testing.d.mts +9 -1
- package/dist/testing.mjs +25 -4
- package/dist/usage-row-BQQM-_t6.mjs +36 -0
- package/dist/usage.d.mts +1 -1
- package/dist/usage.mjs +7 -40
- package/dist/write-guard-CYrY58GM.mjs +174 -0
- package/package.json +10 -14
- package/src/artifact.ts +4 -1
- package/src/backend-rdb.ts +331 -0
- package/src/backend.ts +32 -15
- package/src/{branch.ts → branch/index.ts} +109 -110
- package/src/branch/port.ts +62 -0
- package/src/branch/semantics.ts +69 -0
- package/src/db/open.ts +180 -0
- package/src/db/stats.ts +449 -0
- package/src/deletion-error.ts +17 -0
- package/src/deletion.ts +19 -72
- package/src/entities/index.ts +16 -1
- package/src/entities/v3/event-usage.ts +3 -3
- package/src/entities/v3/events.ts +3 -3
- package/src/entities/v3/index.ts +4 -16
- package/src/entities/v3/persistence-state.ts +3 -3
- package/src/entities/v3/schema-meta.ts +3 -3
- package/src/entities/v3/session-counts.ts +3 -3
- package/src/entities/v3/session-events.ts +3 -3
- package/src/entities/v3/session-projcache-rows.ts +3 -3
- package/src/entities/v3/session-usage.ts +3 -3
- package/src/entities/v3/sessions.ts +3 -3
- package/src/entities/v3/storage-records.ts +17 -0
- package/src/entities/v3/storage-units.ts +3 -3
- package/src/{schema.ts → event-shape.ts} +0 -46
- package/src/export.ts +32 -78
- package/src/handles.ts +349 -0
- package/src/http/route.ts +178 -0
- package/src/import-storages.ts +5 -2
- package/src/import.ts +63 -125
- package/src/index.ts +15 -1666
- package/src/legacy.ts +3 -1
- package/src/live-routing.ts +205 -0
- package/src/log/export.ts +84 -0
- package/src/{log.ts → log/repair.ts} +1 -268
- package/src/log/rows.ts +107 -0
- package/src/log/usage-row.ts +64 -0
- package/src/migrations.ts +196 -0
- package/src/organize.ts +38 -0
- package/src/read-path.ts +190 -0
- package/src/routes.ts +2 -1
- package/src/rows.ts +25 -77
- package/src/session-ops.ts +444 -0
- package/src/session-store.ts +675 -0
- package/src/stats-fold.ts +44 -0
- package/src/storage-takeover/index.ts +2 -5
- package/src/storage-takeover/legacy-domain-migration.ts +118 -0
- package/src/storage-takeover/projection-cache.ts +122 -71
- package/src/storage-takeover/read-model.ts +189 -0
- package/src/storage-takeover/repository.ts +236 -442
- package/src/storage-takeover/storage-backend.ts +44 -159
- package/src/storage-takeover/types.ts +45 -19
- package/src/storage-takeover/workspace-documents.ts +65 -0
- package/src/storage.ts +4 -2
- package/src/testing.ts +21 -1
- package/src/usage.ts +11 -65
- package/src/version.ts +13 -0
- package/src/write-path.ts +286 -0
- package/dist/branch-CoPCRKer.mjs +0 -401
- package/dist/deletion-CWl89TIR.mjs +0 -2249
- package/dist/gc.d.mts +0 -4
- package/dist/gc.mjs +0 -48
- package/dist/log-CcWy46VE.mjs +0 -558
- package/dist/routes-hZjTcX_R.mjs +0 -9
- package/dist/sqlite-BNRd1aqR.mjs +0 -1778
- package/drizzle/postgres/20260908072805_v2_initial/migration.sql +0 -58
- package/drizzle/postgres/20260908072805_v2_initial/snapshot.json +0 -686
- package/drizzle/postgres/20260908072806_v3_drop_original_seq/migration.sql +0 -1
- package/drizzle/postgres/20260908072806_v3_drop_original_seq/snapshot.json +0 -673
- package/drizzle/postgres/20260910120001_v3_storage_tables/migration.sql +0 -65
- package/drizzle/postgres/20260910120001_v3_storage_tables/snapshot.json +0 -1183
- package/drizzle/postgres/20260910130001_v3_session_title_backfill/migration.sql +0 -10
- package/drizzle/postgres/20260910130001_v3_session_title_backfill/snapshot.json +0 -1183
- package/drizzle/postgres/20260918120000_v3_event_usage/migration.sql +0 -14
- package/drizzle/postgres/20260918120000_v3_event_usage/snapshot.json +0 -1309
- package/drizzle/postgres/20260921075900_v3_agent_preset/migration.sql +0 -1
- package/drizzle/postgres/20260922090001_v3_pinned_seq/migration.sql +0 -2
- package/drizzle/postgres/20260922093001_v3_last_event_at/migration.sql +0 -10
- package/drizzle/postgres/20260923120000_v3_usage_materialized/migration.sql +0 -48
- package/drizzle/postgres/20261008103001_v3_schedule_tasks/migration.sql +0 -11
- package/drizzle/sqlite/20260908072751_v2_initial/migration.sql +0 -53
- package/drizzle/sqlite/20260908072751_v2_initial/snapshot.json +0 -492
- package/drizzle/sqlite/20260908072752_v3_drop_original_seq/migration.sql +0 -6
- package/drizzle/sqlite/20260908072752_v3_drop_original_seq/snapshot.json +0 -513
- package/drizzle/sqlite/20260910120000_v3_storage_tables/migration.sql +0 -66
- package/drizzle/sqlite/20260910120000_v3_storage_tables/snapshot.json +0 -905
- package/drizzle/sqlite/20260910130000_v3_session_title_backfill/migration.sql +0 -13
- package/drizzle/sqlite/20260910130000_v3_session_title_backfill/snapshot.json +0 -905
- package/drizzle/sqlite/20260918120000_v3_event_usage/migration.sql +0 -14
- package/drizzle/sqlite/20260918120000_v3_event_usage/snapshot.json +0 -1031
- package/drizzle/sqlite/20260921075855_v3_agent_preset/migration.sql +0 -7
- package/drizzle/sqlite/20260921075855_v3_agent_preset/snapshot.json +0 -1041
- package/drizzle/sqlite/20260922090000_v3_pinned_seq/migration.sql +0 -4
- package/drizzle/sqlite/20260922093000_v3_last_event_at/migration.sql +0 -13
- package/drizzle/sqlite/20260923120000_v3_usage_materialized/migration.sql +0 -48
- package/drizzle/sqlite/20261008103000_v3_schedule_tasks/migration.sql +0 -14
- package/src/adapters/index.ts +0 -12
- package/src/adapters/to-postgres.ts +0 -93
- package/src/adapters/to-sqlite.ts +0 -83
- package/src/adapters/types.ts +0 -44
- package/src/drizzle/postgres-v2.ts +0 -10
- package/src/drizzle/postgres-v3.ts +0 -18
- package/src/drizzle/sqlite-v2.ts +0 -9
- package/src/drizzle/sqlite-v3.ts +0 -18
- package/src/entities/v2/index.ts +0 -20
- package/src/entities/v2/session-events.ts +0 -10
- package/src/entities/v3/schedule-tasks.ts +0 -16
- package/src/entities/v3/workspace-sessions.ts +0 -19
- package/src/entities/v3/workspace-state.ts +0 -12
- package/src/entities/v3/workspaces.ts +0 -14
- package/src/gc.ts +0 -77
- package/src/postgres.ts +0 -1015
- package/src/sqlite.ts +0 -1096
package/src/sqlite.ts
DELETED
|
@@ -1,1096 +0,0 @@
|
|
|
1
|
-
import { randomUUID } from "node:crypto";
|
|
2
|
-
import { mkdir, open, readdir, stat } from "node:fs/promises";
|
|
3
|
-
import { dirname, resolve } from "node:path";
|
|
4
|
-
import { fileURLToPath } from "node:url";
|
|
5
|
-
import { DatabaseSync } from "node:sqlite";
|
|
6
|
-
import { and, desc, eq, inArray, lt, notInArray, sql } from "drizzle-orm";
|
|
7
|
-
import { drizzle, type NodeSQLiteDatabase } from "drizzle-orm/node-sqlite";
|
|
8
|
-
import { migrate } from "drizzle-orm/node-sqlite/migrator";
|
|
9
|
-
import type { SessionId } from "@deepseek-ai/dsh-session";
|
|
10
|
-
import type { SessionStorageMetadata } from "@deepseek-ai/dsh-session-persistence";
|
|
11
|
-
import {
|
|
12
|
-
type Backend,
|
|
13
|
-
type BackendTx,
|
|
14
|
-
type EventInsert,
|
|
15
|
-
type EventRow,
|
|
16
|
-
type SessionCountBucket,
|
|
17
|
-
type SessionListRowRecord,
|
|
18
|
-
type SessionListRowsPage,
|
|
19
|
-
type SessionListRowsQuery,
|
|
20
|
-
type SessionRow,
|
|
21
|
-
type SessionUsageBucket,
|
|
22
|
-
} from "./backend.ts";
|
|
23
|
-
import { sessionConflictRow, sessionInsertRow, titleOfEventData } from "./log.ts";
|
|
24
|
-
import type { EventUsageRow } from "./log.ts";
|
|
25
|
-
import {
|
|
26
|
-
COUNTED_EVENT_TYPES,
|
|
27
|
-
addActivityTotals,
|
|
28
|
-
addTokenTotals,
|
|
29
|
-
addTotals,
|
|
30
|
-
emptyTotals,
|
|
31
|
-
localDayKey,
|
|
32
|
-
} from "./usage.ts";
|
|
33
|
-
import type { UsageActivityTotals, UsageAggregate, UsageTokenTotals } from "./usage.ts";
|
|
34
|
-
import {
|
|
35
|
-
DEFAULT_BUSY_TIMEOUT_MS,
|
|
36
|
-
SCHEMA_VERSION,
|
|
37
|
-
SESSION_PERSISTENCE_SQLITE_APPLICATION_ID,
|
|
38
|
-
tEventUsage,
|
|
39
|
-
tEvents,
|
|
40
|
-
tPersistenceState,
|
|
41
|
-
tScheduleTasks,
|
|
42
|
-
tSessionEvents,
|
|
43
|
-
tSessionProjcacheRows,
|
|
44
|
-
tSessions,
|
|
45
|
-
tStorageUnits,
|
|
46
|
-
tWorkspaceSessions,
|
|
47
|
-
tWorkspaceState,
|
|
48
|
-
tWorkspaces,
|
|
49
|
-
type JournalMode,
|
|
50
|
-
} from "./schema.ts";
|
|
51
|
-
import { createStorageRepository } from "./storage-takeover/repository.ts";
|
|
52
|
-
import type { StorageRepository } from "./storage-takeover/types.ts";
|
|
53
|
-
|
|
54
|
-
type SqliteDb = NodeSQLiteDatabase & { $client: DatabaseSync };
|
|
55
|
-
|
|
56
|
-
// 用量聚合的原始行(列名是 SQL 别名)。
|
|
57
|
-
interface RawBucketRow {
|
|
58
|
-
day: string;
|
|
59
|
-
provider: string | null;
|
|
60
|
-
model: string | null;
|
|
61
|
-
subagent: number;
|
|
62
|
-
input_tokens: number | null;
|
|
63
|
-
output_tokens: number | null;
|
|
64
|
-
cache_read_tokens: number | null;
|
|
65
|
-
reasoning_tokens: number | null;
|
|
66
|
-
total_tokens: number | null;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
// 「是否子代理」两组的 token 合计行。
|
|
70
|
-
interface RawScopeRow extends RawBucketRow {}
|
|
71
|
-
|
|
72
|
-
// 按会话的行:token 一条查询、活动一条查询,按 session_id 合并。
|
|
73
|
-
interface RawSessionTokenRow extends RawBucketRow {
|
|
74
|
-
session_id: string;
|
|
75
|
-
title: string | null;
|
|
76
|
-
archived: number;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
// `t_session_counts` 的原始行(四项计数,列名是 SQL 别名)。
|
|
80
|
-
interface RawCountRow {
|
|
81
|
-
turns: number | null;
|
|
82
|
-
steps: number | null;
|
|
83
|
-
user_inputs: number | null;
|
|
84
|
-
tool_calls: number | null;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
// 按会话的活动计数行。
|
|
88
|
-
interface RawSessionCountRow extends RawCountRow {
|
|
89
|
-
session_id: string;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
// 活动总览行:是否子代理 + 四项计数。
|
|
93
|
-
interface RawActivityScopeRow extends RawCountRow {
|
|
94
|
-
subagent: number | null;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
// 用量行的「被引用」物化表达式(`%EVENT%` 换成事件 id 列)。
|
|
98
|
-
const EVENT_REFERENCED_SQL = `EXISTS (SELECT 1 FROM t_session_events rb
|
|
99
|
-
WHERE rb.f_event_id = %EVENT%)`;
|
|
100
|
-
|
|
101
|
-
// 用量行的「被 subagent 会话引用」物化表达式(`%EVENT%` 换成事件 id 列)。
|
|
102
|
-
const EVENT_SUBAGENT_SQL = `EXISTS (SELECT 1 FROM t_session_events sb
|
|
103
|
-
JOIN t_sessions ss ON ss.f_session_id = sb.f_session_id
|
|
104
|
-
WHERE sb.f_event_id = %EVENT% AND ss.f_origin = 'subagent')`;
|
|
105
|
-
|
|
106
|
-
// `IN (...)` 用的类型字面量(与 `COUNTED_EVENT_TYPES` 同源)。
|
|
107
|
-
const COUNTED_EVENT_TYPE_SQL = COUNTED_EVENT_TYPES.map((type) => `'${type}'`).join(", ");
|
|
108
|
-
|
|
109
|
-
// 引用标记的分批重算:一次绑定的参数有上限,按块走。
|
|
110
|
-
const REFRESH_FLAG_CHUNK = 400;
|
|
111
|
-
|
|
112
|
-
function withEvent(expression: string, column: string): string {
|
|
113
|
-
return expression.replace("%EVENT%", column);
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
// 派生统计表的回填:任一表为空即按事件表全量重算(幂等;迁移删表重建后、手动清空后都靠它自愈)。
|
|
117
|
-
// 顺序有依赖:用量行(带物化列)先落,两张会话汇总表再从它 / 事件表重算。
|
|
118
|
-
function backfillUsageTables(db: DatabaseSync): void {
|
|
119
|
-
backfillEventUsage(db);
|
|
120
|
-
backfillSessionUsage(db);
|
|
121
|
-
backfillSessionCounts(db);
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
function tableIsEmpty(db: DatabaseSync, table: string): boolean {
|
|
125
|
-
const row = db.prepare(`SELECT count(*) AS n FROM ${table}`).get() as { n: number };
|
|
126
|
-
return row.n === 0;
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
// 用量行:一条 `assistant/message` 事件行一行,物化本地日与两个归属标记。
|
|
130
|
-
function backfillEventUsage(db: DatabaseSync): void {
|
|
131
|
-
if (!tableIsEmpty(db, "t_event_usage")) return;
|
|
132
|
-
db.exec(`INSERT OR IGNORE INTO t_event_usage (
|
|
133
|
-
f_event_id, f_created_at, f_day, f_provider, f_model, f_referenced, f_subagent,
|
|
134
|
-
f_input_tokens, f_output_tokens, f_cache_read_tokens, f_reasoning_tokens, f_total_tokens)
|
|
135
|
-
SELECT e.f_event_id, e.f_created_at,
|
|
136
|
-
date(e.f_created_at / 1000, 'unixepoch', 'localtime'),
|
|
137
|
-
coalesce(json_extract(e.f_data, '$.data.message.source.provider'),
|
|
138
|
-
json_extract(e.f_data, '$.message.source.provider')),
|
|
139
|
-
coalesce(json_extract(e.f_data, '$.data.message.source.model'),
|
|
140
|
-
json_extract(e.f_data, '$.message.source.model')),
|
|
141
|
-
${withEvent(EVENT_REFERENCED_SQL, "e.f_event_id")},
|
|
142
|
-
${withEvent(EVENT_SUBAGENT_SQL, "e.f_event_id")},
|
|
143
|
-
coalesce(json_extract(e.f_data, '$.data.usage.inputTokens'),
|
|
144
|
-
json_extract(e.f_data, '$.usage.inputTokens'), 0),
|
|
145
|
-
coalesce(json_extract(e.f_data, '$.data.usage.outputTokens'),
|
|
146
|
-
json_extract(e.f_data, '$.usage.outputTokens'), 0),
|
|
147
|
-
coalesce(json_extract(e.f_data, '$.data.usage.cacheReadTokens'),
|
|
148
|
-
json_extract(e.f_data, '$.usage.cacheReadTokens'), 0),
|
|
149
|
-
coalesce(json_extract(e.f_data, '$.data.usage.reasoningTokens'),
|
|
150
|
-
json_extract(e.f_data, '$.usage.reasoningTokens'), 0),
|
|
151
|
-
coalesce(json_extract(e.f_data, '$.data.usage.totalTokens'),
|
|
152
|
-
json_extract(e.f_data, '$.usage.totalTokens'), 0)
|
|
153
|
-
FROM t_events e
|
|
154
|
-
WHERE e.f_type = 'assistant/message'
|
|
155
|
-
AND (json_extract(e.f_data, '$.data.usage') IS NOT NULL
|
|
156
|
-
OR json_extract(e.f_data, '$.usage') IS NOT NULL)`);
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
// 会话 × 本地日 × 模型的 token 汇总:从桥接行 × 用量行重算(含 fork 继承前缀)。
|
|
160
|
-
function backfillSessionUsage(db: DatabaseSync): void {
|
|
161
|
-
if (!tableIsEmpty(db, "t_session_usage")) return;
|
|
162
|
-
db.exec(`INSERT OR IGNORE INTO t_session_usage (
|
|
163
|
-
f_session_id, f_day, f_provider, f_model,
|
|
164
|
-
f_input_tokens, f_output_tokens, f_cache_read_tokens, f_reasoning_tokens, f_total_tokens)
|
|
165
|
-
SELECT b.f_session_id, u.f_day, coalesce(u.f_provider, ''), coalesce(u.f_model, ''),
|
|
166
|
-
sum(u.f_input_tokens), sum(u.f_output_tokens), sum(u.f_cache_read_tokens),
|
|
167
|
-
sum(u.f_reasoning_tokens), sum(u.f_total_tokens)
|
|
168
|
-
FROM t_session_events b
|
|
169
|
-
JOIN t_event_usage u ON u.f_event_id = b.f_event_id
|
|
170
|
-
GROUP BY b.f_session_id, u.f_day, coalesce(u.f_provider, ''), coalesce(u.f_model, '')`);
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
// 会话 × 本地日的活动计数:从桥接行 × 事件类型重算。
|
|
174
|
-
function backfillSessionCounts(db: DatabaseSync): void {
|
|
175
|
-
if (!tableIsEmpty(db, "t_session_counts")) return;
|
|
176
|
-
db.exec(`INSERT OR IGNORE INTO t_session_counts (
|
|
177
|
-
f_session_id, f_day, f_turns, f_steps, f_user_inputs, f_tool_calls)
|
|
178
|
-
SELECT b.f_session_id,
|
|
179
|
-
date(e.f_created_at / 1000, 'unixepoch', 'localtime'),
|
|
180
|
-
sum(CASE WHEN e.f_type = 'turn/start' THEN 1 ELSE 0 END),
|
|
181
|
-
sum(CASE WHEN e.f_type = 'step/start' THEN 1 ELSE 0 END),
|
|
182
|
-
sum(CASE WHEN e.f_type = 'user/message' THEN 1 ELSE 0 END),
|
|
183
|
-
sum(CASE WHEN e.f_type = 'tool/call' THEN 1 ELSE 0 END)
|
|
184
|
-
FROM t_session_events b
|
|
185
|
-
JOIN t_events e ON e.f_event_id = b.f_event_id
|
|
186
|
-
WHERE e.f_type IN (${COUNTED_EVENT_TYPE_SQL})
|
|
187
|
-
GROUP BY b.f_session_id, 2`);
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
function sessionListRowOf(row: Record<string, unknown>): SessionListRowRecord {
|
|
191
|
-
return {
|
|
192
|
-
sessionId:
|
|
193
|
-
typeof row["session_id"] === "string" ? row["session_id"] : String(row["session_id"]),
|
|
194
|
-
title: typeof row["title"] === "string" ? row["title"] : null,
|
|
195
|
-
origin: typeof row["origin"] === "string" ? row["origin"] : null,
|
|
196
|
-
cwd: typeof row["cwd"] === "string" ? row["cwd"] : null,
|
|
197
|
-
createdAt: Number(row["created_at"]),
|
|
198
|
-
updatedAt: Number(row["updated_at"]),
|
|
199
|
-
archived: row["archived"] === 1 || row["archived"] === true,
|
|
200
|
-
subagent: row["origin"] === "subagent",
|
|
201
|
-
workspace: typeof row["workspace"] === "string" ? row["workspace"] : null,
|
|
202
|
-
};
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
// SQL 的缺失求和是 NULL:没有 usage 字段的行使该字段计 0。
|
|
206
|
-
function tokenTotalsOf(row: {
|
|
207
|
-
input_tokens: number | null;
|
|
208
|
-
output_tokens: number | null;
|
|
209
|
-
cache_read_tokens: number | null;
|
|
210
|
-
reasoning_tokens: number | null;
|
|
211
|
-
total_tokens: number | null;
|
|
212
|
-
}): UsageTokenTotals {
|
|
213
|
-
return {
|
|
214
|
-
inputTokens: row.input_tokens ?? 0,
|
|
215
|
-
outputTokens: row.output_tokens ?? 0,
|
|
216
|
-
cacheReadTokens: row.cache_read_tokens ?? 0,
|
|
217
|
-
reasoningTokens: row.reasoning_tokens ?? 0,
|
|
218
|
-
totalTokens: row.total_tokens ?? 0,
|
|
219
|
-
};
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
// `t_session_counts` 的原始行(列名是 SQL 别名)折成四项活动计数。
|
|
223
|
-
function activityTotalsOf(row: {
|
|
224
|
-
turns: number | null;
|
|
225
|
-
steps: number | null;
|
|
226
|
-
user_inputs: number | null;
|
|
227
|
-
tool_calls: number | null;
|
|
228
|
-
}): UsageActivityTotals {
|
|
229
|
-
return {
|
|
230
|
-
turns: row.turns ?? 0,
|
|
231
|
-
steps: row.steps ?? 0,
|
|
232
|
-
userInputs: row.user_inputs ?? 0,
|
|
233
|
-
toolCalls: row.tool_calls ?? 0,
|
|
234
|
-
};
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
// 一行的整套指标:token 用量 + 活动计数(轮次 / 步骤 / 用户输入 / 工具调用)。
|
|
238
|
-
function emptyActivity(): UsageActivityTotals {
|
|
239
|
-
return { turns: 0, steps: 0, userInputs: 0, toolCalls: 0 };
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
// 有 token 用量的会话:活动计数与列表都限定在这一批会话里,口径一致(读汇总表,几百行)。
|
|
243
|
-
function hasUsageSession(column: string): string {
|
|
244
|
-
return `EXISTS (SELECT 1 FROM t_session_usage ub WHERE ub.f_session_id = ${column})`;
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
const sqliteMigrationsDir = fileURLToPath(new URL("../drizzle/sqlite/", import.meta.url));
|
|
248
|
-
|
|
249
|
-
const sqliteTxQueues = new Map<string, Promise<void>>();
|
|
250
|
-
|
|
251
|
-
function enqueueSqliteTx<T>(path: string, fn: () => Promise<T>): Promise<T> {
|
|
252
|
-
const tail = sqliteTxQueues.get(path) ?? Promise.resolve();
|
|
253
|
-
const run = tail.then(fn);
|
|
254
|
-
|
|
255
|
-
sqliteTxQueues.set(
|
|
256
|
-
path,
|
|
257
|
-
run.then(
|
|
258
|
-
() => undefined,
|
|
259
|
-
() => undefined,
|
|
260
|
-
),
|
|
261
|
-
);
|
|
262
|
-
return run;
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
async function createDatabaseFile(path: string): Promise<void> {
|
|
266
|
-
try {
|
|
267
|
-
const handle = await open(path, "wx", 0o600);
|
|
268
|
-
await handle.close();
|
|
269
|
-
} catch (error) {
|
|
270
|
-
if ((error as NodeJS.ErrnoException).code !== "EEXIST") throw error;
|
|
271
|
-
}
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
export async function openDatabase(
|
|
275
|
-
path: string,
|
|
276
|
-
journalMode: JournalMode,
|
|
277
|
-
busyTimeout = DEFAULT_BUSY_TIMEOUT_MS,
|
|
278
|
-
): Promise<DatabaseSync> {
|
|
279
|
-
const db = new DatabaseSync(path);
|
|
280
|
-
try {
|
|
281
|
-
configureDatabase(db, path, journalMode, busyTimeout);
|
|
282
|
-
const dbx = drizzle({ client: db });
|
|
283
|
-
const { user_version: onDisk } = db.prepare("PRAGMA user_version").get() as {
|
|
284
|
-
user_version: number;
|
|
285
|
-
};
|
|
286
|
-
|
|
287
|
-
if (onDisk === 2 && !hasMigrationsTable(db)) {
|
|
288
|
-
await baselineV2(db);
|
|
289
|
-
}
|
|
290
|
-
migrate(dbx, { migrationsFolder: sqliteMigrationsDir });
|
|
291
|
-
backfillUsageTables(db);
|
|
292
|
-
|
|
293
|
-
dbx
|
|
294
|
-
.insert(tPersistenceState)
|
|
295
|
-
.values({ fSingleton: 1, fStoreId: randomUUID() })
|
|
296
|
-
.onConflictDoNothing()
|
|
297
|
-
.run();
|
|
298
|
-
if (onDisk === 0 || onDisk === 2) {
|
|
299
|
-
db.exec(`PRAGMA user_version = ${SCHEMA_VERSION}`);
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
db.exec(`PRAGMA journal_mode = ${journalMode.toUpperCase()}`);
|
|
303
|
-
return db;
|
|
304
|
-
} catch (error: unknown) {
|
|
305
|
-
db.close();
|
|
306
|
-
throw error;
|
|
307
|
-
}
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
function hasMigrationsTable(db: DatabaseSync): boolean {
|
|
311
|
-
return (
|
|
312
|
-
db
|
|
313
|
-
.prepare("SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = '__drizzle_migrations'")
|
|
314
|
-
.get() !== undefined
|
|
315
|
-
);
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
async function baselineV2(db: DatabaseSync): Promise<void> {
|
|
319
|
-
const dir = (await readdir(sqliteMigrationsDir)).find((name) => name.endsWith("_v2_initial"));
|
|
320
|
-
if (dir === undefined) throw new Error("missing v2 baseline migration in drizzle/sqlite");
|
|
321
|
-
db.exec(`
|
|
322
|
-
CREATE TABLE __drizzle_migrations (
|
|
323
|
-
id INTEGER PRIMARY KEY,
|
|
324
|
-
hash text NOT NULL,
|
|
325
|
-
created_at numeric,
|
|
326
|
-
name text,
|
|
327
|
-
applied_at TEXT
|
|
328
|
-
)
|
|
329
|
-
`);
|
|
330
|
-
db.prepare(
|
|
331
|
-
"INSERT INTO __drizzle_migrations (hash, created_at, name, applied_at) VALUES (?, ?, ?, ?)",
|
|
332
|
-
).run("baseline", 0, dir, new Date().toISOString());
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
function configureDatabase(
|
|
336
|
-
db: DatabaseSync,
|
|
337
|
-
path: string,
|
|
338
|
-
journalMode: JournalMode,
|
|
339
|
-
busyTimeout: number,
|
|
340
|
-
): void {
|
|
341
|
-
db.exec("PRAGMA foreign_keys = ON");
|
|
342
|
-
|
|
343
|
-
db.exec(`PRAGMA busy_timeout = ${busyTimeout}`);
|
|
344
|
-
// 冷读一次要顺序扫整份日志(大会话 30k+ 事件、几十 MiB 的 `f_data`):把页缓存与 mmap 窗口调大,
|
|
345
|
-
// 省掉的是 SQLite 与内核页缓存之间的往返。`:memory:` 上这两个 PRAGMA 无害。
|
|
346
|
-
db.exec("PRAGMA cache_size = -65536");
|
|
347
|
-
db.exec("PRAGMA mmap_size = 268435456");
|
|
348
|
-
const dbx = drizzle({ client: db });
|
|
349
|
-
|
|
350
|
-
dbx.transaction(
|
|
351
|
-
(tx) => {
|
|
352
|
-
const { user_version: onDisk } = tx.get(sql`PRAGMA user_version`) as {
|
|
353
|
-
user_version: number;
|
|
354
|
-
};
|
|
355
|
-
const { application_id: applicationId } = tx.get(sql`PRAGMA application_id`) as {
|
|
356
|
-
application_id: number;
|
|
357
|
-
};
|
|
358
|
-
const { count: userObjectCount } = tx.get(
|
|
359
|
-
sql`SELECT COUNT(*) AS count FROM sqlite_schema WHERE name NOT GLOB 'sqlite_*'`,
|
|
360
|
-
) as { count: number };
|
|
361
|
-
if (onDisk === 0 && (applicationId !== 0 || userObjectCount > 0)) {
|
|
362
|
-
throw new Error(
|
|
363
|
-
`session database at "${path}" has an unversioned schema or application identity`,
|
|
364
|
-
);
|
|
365
|
-
}
|
|
366
|
-
if (onDisk !== 0 && onDisk !== 2 && onDisk !== SCHEMA_VERSION) {
|
|
367
|
-
throw new Error(
|
|
368
|
-
`session database at "${path}" has schema version ${onDisk}, incompatible with this build (${SCHEMA_VERSION})`,
|
|
369
|
-
);
|
|
370
|
-
}
|
|
371
|
-
if (onDisk >= 1 && applicationId !== SESSION_PERSISTENCE_SQLITE_APPLICATION_ID) {
|
|
372
|
-
throw new Error(
|
|
373
|
-
`session database at "${path}" has application id ${applicationId}, expected ${SESSION_PERSISTENCE_SQLITE_APPLICATION_ID}`,
|
|
374
|
-
);
|
|
375
|
-
}
|
|
376
|
-
if (onDisk === 0) {
|
|
377
|
-
tx.run(sql.raw(`PRAGMA application_id = ${SESSION_PERSISTENCE_SQLITE_APPLICATION_ID}`));
|
|
378
|
-
tx.run(sql.raw(`PRAGMA user_version = ${SCHEMA_VERSION}`));
|
|
379
|
-
}
|
|
380
|
-
},
|
|
381
|
-
{ behavior: "immediate" },
|
|
382
|
-
);
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
export interface SqliteBackendOptions {
|
|
386
|
-
path: string;
|
|
387
|
-
journalMode: JournalMode;
|
|
388
|
-
busyTimeout: number;
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
export class SqliteBackend implements Backend {
|
|
392
|
-
readonly kind = "sqlite" as const;
|
|
393
|
-
storeIdentity!: string;
|
|
394
|
-
|
|
395
|
-
readonly storage: StorageRepository;
|
|
396
|
-
|
|
397
|
-
private dbPath = "";
|
|
398
|
-
private db!: SqliteDb;
|
|
399
|
-
private readonly dbReady: Promise<SqliteDb>;
|
|
400
|
-
private resolveDb!: (db: SqliteDb) => void;
|
|
401
|
-
private rejectDb!: (error: unknown) => void;
|
|
402
|
-
|
|
403
|
-
constructor(private readonly options: SqliteBackendOptions) {
|
|
404
|
-
this.dbReady = new Promise<SqliteDb>((resolve, reject) => {
|
|
405
|
-
this.resolveDb = resolve;
|
|
406
|
-
this.rejectDb = reject;
|
|
407
|
-
});
|
|
408
|
-
|
|
409
|
-
this.dbReady.catch(() => {});
|
|
410
|
-
this.storage = createStorageRepository({
|
|
411
|
-
db: () => this.dbReady,
|
|
412
|
-
|
|
413
|
-
dbDirect: () => {
|
|
414
|
-
if (this.db === undefined) throw new Error("sqlite session database is not open");
|
|
415
|
-
return this.db;
|
|
416
|
-
},
|
|
417
|
-
writeAtomically: (fn) =>
|
|
418
|
-
enqueueSqliteTx(this.dbPath, async () => {
|
|
419
|
-
const db = await this.dbReady;
|
|
420
|
-
db.$client.exec("BEGIN IMMEDIATE");
|
|
421
|
-
try {
|
|
422
|
-
const result = await fn();
|
|
423
|
-
db.$client.exec("COMMIT");
|
|
424
|
-
return result;
|
|
425
|
-
} catch (error: unknown) {
|
|
426
|
-
try {
|
|
427
|
-
db.$client.exec("ROLLBACK");
|
|
428
|
-
} catch {}
|
|
429
|
-
throw error;
|
|
430
|
-
}
|
|
431
|
-
}),
|
|
432
|
-
tables: {
|
|
433
|
-
t_sessions: tSessions,
|
|
434
|
-
t_storage_units: tStorageUnits,
|
|
435
|
-
t_workspaces: tWorkspaces,
|
|
436
|
-
t_workspace_sessions: tWorkspaceSessions,
|
|
437
|
-
t_workspace_state: tWorkspaceState,
|
|
438
|
-
t_schedule_tasks: tScheduleTasks,
|
|
439
|
-
t_session_projcache_row: tSessionProjcacheRows,
|
|
440
|
-
},
|
|
441
|
-
});
|
|
442
|
-
}
|
|
443
|
-
|
|
444
|
-
async open(): Promise<void> {
|
|
445
|
-
try {
|
|
446
|
-
await this.doOpen();
|
|
447
|
-
this.resolveDb(this.db);
|
|
448
|
-
} catch (error: unknown) {
|
|
449
|
-
this.rejectDb(error);
|
|
450
|
-
throw error;
|
|
451
|
-
}
|
|
452
|
-
}
|
|
453
|
-
|
|
454
|
-
private async doOpen(): Promise<void> {
|
|
455
|
-
const actual =
|
|
456
|
-
this.options.path === ":memory:" ? this.options.path : resolve(this.options.path);
|
|
457
|
-
this.dbPath = actual;
|
|
458
|
-
if (actual !== ":memory:") {
|
|
459
|
-
await mkdir(dirname(actual), { recursive: true, mode: 0o700 });
|
|
460
|
-
await createDatabaseFile(actual);
|
|
461
|
-
}
|
|
462
|
-
|
|
463
|
-
await enqueueSqliteTx(actual, async () => {
|
|
464
|
-
this.db = drizzle({
|
|
465
|
-
client: await openDatabase(actual, this.options.journalMode, this.options.busyTimeout),
|
|
466
|
-
});
|
|
467
|
-
|
|
468
|
-
migrate(this.db, { migrationsFolder: sqliteMigrationsDir });
|
|
469
|
-
});
|
|
470
|
-
try {
|
|
471
|
-
const row = this.db
|
|
472
|
-
.select({ fStoreId: tPersistenceState.fStoreId })
|
|
473
|
-
.from(tPersistenceState)
|
|
474
|
-
.where(eq(tPersistenceState.fSingleton, 1))
|
|
475
|
-
.get() as { fStoreId: string } | undefined;
|
|
476
|
-
|
|
477
|
-
if (row === undefined) {
|
|
478
|
-
throw new Error(`session database at "${actual}" has no store identity`);
|
|
479
|
-
}
|
|
480
|
-
if (row.fStoreId.length === 0) {
|
|
481
|
-
throw new Error(`session database at "${actual}" has no valid store identity`);
|
|
482
|
-
}
|
|
483
|
-
if (actual !== ":memory:") {
|
|
484
|
-
const identity = await stat(actual, { bigint: true });
|
|
485
|
-
this.storeIdentity = `file:${identity.dev}:${identity.ino}:${identity.birthtimeNs}:store:${row.fStoreId}`;
|
|
486
|
-
} else {
|
|
487
|
-
this.storeIdentity = `memory:store:${row.fStoreId}`;
|
|
488
|
-
}
|
|
489
|
-
} catch (error: unknown) {
|
|
490
|
-
this.db.$client.close();
|
|
491
|
-
throw error;
|
|
492
|
-
}
|
|
493
|
-
}
|
|
494
|
-
|
|
495
|
-
async close(): Promise<void> {
|
|
496
|
-
if (this.db === undefined) return;
|
|
497
|
-
this.db.$client.close();
|
|
498
|
-
}
|
|
499
|
-
|
|
500
|
-
async getSession(id: SessionId): Promise<SessionRow | undefined> {
|
|
501
|
-
return this.db.select().from(tSessions).where(eq(tSessions.fSessionId, id)).get() as
|
|
502
|
-
| SessionRow
|
|
503
|
-
| undefined;
|
|
504
|
-
}
|
|
505
|
-
|
|
506
|
-
async getEventRows(id: SessionId, fromSequence?: number): Promise<EventRow[]> {
|
|
507
|
-
// 裸 prepared 语句而不是 drizzle 的 `.select().all()`:冷读要一次拉整份日志(大会话 30k+ 行、几十 MiB 的
|
|
508
|
-
// `f_data`),drizzle 的逐行对象映射在这条路径上是纯开销。列集合与 `eventRows()` 完全一致。
|
|
509
|
-
const sql =
|
|
510
|
-
`select se.f_event_id as fEventId, se.f_sequence as fSequence, e.f_type as fType,
|
|
511
|
-
e.f_kind as fKind, e.f_role as fRole, e.f_name as fName, e.f_action_id as fActionId,
|
|
512
|
-
e.f_created_at as fCreatedAt, e.f_data as fData, se.f_surface_op as fSurfaceOp
|
|
513
|
-
from t_session_events se join t_events e on se.f_event_id = e.f_event_id
|
|
514
|
-
where se.f_session_id = ?${fromSequence === undefined ? "" : " and se.f_sequence >= ?"}
|
|
515
|
-
order by se.f_sequence`;
|
|
516
|
-
const statement = this.db.$client.prepare(sql);
|
|
517
|
-
const rows =
|
|
518
|
-
fromSequence === undefined ? statement.all(id) : statement.all(id, fromSequence);
|
|
519
|
-
return rows as unknown as EventRow[];
|
|
520
|
-
}
|
|
521
|
-
|
|
522
|
-
// 只取类型:rewind 的边界 / 窗口探测不该把整个事件 JSON(f_data)拖回来。
|
|
523
|
-
async getEventTypeAt(id: SessionId, sequence: number): Promise<string | undefined> {
|
|
524
|
-
const row = this.db
|
|
525
|
-
.select({ fType: tEvents.fType })
|
|
526
|
-
.from(tSessionEvents)
|
|
527
|
-
.innerJoin(tEvents, eq(tSessionEvents.fEventId, tEvents.fEventId))
|
|
528
|
-
.where(and(eq(tSessionEvents.fSessionId, id), eq(tSessionEvents.fSequence, sequence)))
|
|
529
|
-
.get() as { fType?: string } | undefined;
|
|
530
|
-
return row?.fType;
|
|
531
|
-
}
|
|
532
|
-
|
|
533
|
-
async getEventTypesBefore(
|
|
534
|
-
id: SessionId,
|
|
535
|
-
beforeSequence: number,
|
|
536
|
-
limit: number,
|
|
537
|
-
): Promise<Array<Pick<EventRow, "fSequence" | "fType">>> {
|
|
538
|
-
return this.db
|
|
539
|
-
.select({ fSequence: tSessionEvents.fSequence, fType: tEvents.fType })
|
|
540
|
-
.from(tSessionEvents)
|
|
541
|
-
.innerJoin(tEvents, eq(tSessionEvents.fEventId, tEvents.fEventId))
|
|
542
|
-
.where(and(eq(tSessionEvents.fSessionId, id), lt(tSessionEvents.fSequence, beforeSequence)))
|
|
543
|
-
.orderBy(desc(tSessionEvents.fSequence))
|
|
544
|
-
.limit(limit)
|
|
545
|
-
.all() as unknown as Array<Pick<EventRow, "fSequence" | "fType">>;
|
|
546
|
-
}
|
|
547
|
-
|
|
548
|
-
async listSessions(): Promise<SessionRow[]> {
|
|
549
|
-
return this.db.select().from(tSessions).all() as SessionRow[];
|
|
550
|
-
}
|
|
551
|
-
|
|
552
|
-
async transaction<T>(fn: (tx: BackendTx) => Promise<T>): Promise<T> {
|
|
553
|
-
return enqueueSqliteTx(this.dbPath, async () => {
|
|
554
|
-
this.db.$client.exec("BEGIN IMMEDIATE");
|
|
555
|
-
try {
|
|
556
|
-
const result = await fn(this.tx);
|
|
557
|
-
this.db.$client.exec("COMMIT");
|
|
558
|
-
return result;
|
|
559
|
-
} catch (error: unknown) {
|
|
560
|
-
try {
|
|
561
|
-
this.db.$client.exec("ROLLBACK");
|
|
562
|
-
} catch {}
|
|
563
|
-
throw error;
|
|
564
|
-
}
|
|
565
|
-
});
|
|
566
|
-
}
|
|
567
|
-
|
|
568
|
-
private readonly tx: BackendTx = {
|
|
569
|
-
upsertSession: (storage, incarnation) => this.upsertSession(storage, incarnation),
|
|
570
|
-
getHead: (id) => this.getHead(id),
|
|
571
|
-
getSeedLength: (id) => this.getSeedLength(id),
|
|
572
|
-
updateSeedLength: (id, seedLength) => this.updateSeedLength(id, seedLength),
|
|
573
|
-
insertEvents: (events, usageRows) => this.insertEvents(events, usageRows),
|
|
574
|
-
insertBridges: (rows) => this.insertBridges(rows),
|
|
575
|
-
updateHead: (id, headEventId, headSequence) => this.updateHead(id, headEventId, headSequence),
|
|
576
|
-
bumpRevision: (id, lastEventAt) => this.bumpRevision(id, lastEventAt),
|
|
577
|
-
refreshTitle: (id) => this.refreshTitle(id),
|
|
578
|
-
deleteBridgeTail: (id, fromSequence) => this.deleteBridgeTail(id, fromSequence),
|
|
579
|
-
getPrevBridge: (id, sequence) => this.getPrevBridge(id, sequence),
|
|
580
|
-
deleteSession: (id) => this.deleteSession(id),
|
|
581
|
-
deleteSessions: (ids) => this.deleteSessions(ids),
|
|
582
|
-
};
|
|
583
|
-
|
|
584
|
-
private async upsertSession(storage: SessionStorageMetadata, incarnation: string): Promise<void> {
|
|
585
|
-
this.db
|
|
586
|
-
.insert(tSessions)
|
|
587
|
-
.values(sessionInsertRow(storage, incarnation))
|
|
588
|
-
.onConflictDoUpdate({
|
|
589
|
-
target: tSessions.fSessionId,
|
|
590
|
-
set: sessionConflictRow(storage),
|
|
591
|
-
})
|
|
592
|
-
.run();
|
|
593
|
-
}
|
|
594
|
-
|
|
595
|
-
private async getHead(
|
|
596
|
-
id: SessionId,
|
|
597
|
-
): Promise<Pick<SessionRow, "fHeadEventId" | "fHeadSequence">> {
|
|
598
|
-
const head = this.db
|
|
599
|
-
.select({ fHeadEventId: tSessions.fHeadEventId, fHeadSequence: tSessions.fHeadSequence })
|
|
600
|
-
.from(tSessions)
|
|
601
|
-
.where(eq(tSessions.fSessionId, id))
|
|
602
|
-
.get() as Pick<SessionRow, "fHeadEventId" | "fHeadSequence"> | undefined;
|
|
603
|
-
|
|
604
|
-
if (head === undefined) throw new Error(`session "${id}" has no materialized row`);
|
|
605
|
-
return head;
|
|
606
|
-
}
|
|
607
|
-
|
|
608
|
-
private async getSeedLength(id: SessionId): Promise<number | null> {
|
|
609
|
-
const row = this.db
|
|
610
|
-
.select({ fSeedLength: tSessions.fSeedLength })
|
|
611
|
-
.from(tSessions)
|
|
612
|
-
.where(eq(tSessions.fSessionId, id))
|
|
613
|
-
.get() as { fSeedLength: number | null } | undefined;
|
|
614
|
-
|
|
615
|
-
if (row === undefined) throw new Error(`session "${id}" has no materialized row`);
|
|
616
|
-
return row.fSeedLength;
|
|
617
|
-
}
|
|
618
|
-
|
|
619
|
-
private async updateSeedLength(id: SessionId, seedLength: number): Promise<void> {
|
|
620
|
-
this.db
|
|
621
|
-
.update(tSessions)
|
|
622
|
-
.set({ fSeedLength: seedLength })
|
|
623
|
-
.where(eq(tSessions.fSessionId, id))
|
|
624
|
-
.run();
|
|
625
|
-
}
|
|
626
|
-
|
|
627
|
-
private static readonly INSERT_BATCH_ROWS = 1000;
|
|
628
|
-
|
|
629
|
-
private async insertEvents(
|
|
630
|
-
events: EventInsert[],
|
|
631
|
-
usageRows: readonly EventUsageRow[],
|
|
632
|
-
): Promise<void> {
|
|
633
|
-
if (events.length === 0) return;
|
|
634
|
-
for (let i = 0; i < events.length; i += SqliteBackend.INSERT_BATCH_ROWS) {
|
|
635
|
-
this.db
|
|
636
|
-
.insert(tEvents)
|
|
637
|
-
.values(events.slice(i, i + SqliteBackend.INSERT_BATCH_ROWS).map((event) => ({ ...event })))
|
|
638
|
-
.run();
|
|
639
|
-
}
|
|
640
|
-
// 用量行顺带落 t_event_usage(本地日与引用标记已由写路径物化):统计不再解析事件 JSON、
|
|
641
|
-
// 也不回连事件表(重复事件行忽略,fork 复用不重复记)。
|
|
642
|
-
if (usageRows.length > 0) {
|
|
643
|
-
this.db
|
|
644
|
-
.insert(tEventUsage)
|
|
645
|
-
.values(usageRows.map((row) => ({ ...row })))
|
|
646
|
-
.onConflictDoNothing()
|
|
647
|
-
.run();
|
|
648
|
-
}
|
|
649
|
-
}
|
|
650
|
-
|
|
651
|
-
private async insertBridges(
|
|
652
|
-
rows: Array<{
|
|
653
|
-
fSessionId: SessionId;
|
|
654
|
-
fEventId: string;
|
|
655
|
-
fSequence: number;
|
|
656
|
-
fSurfaceOp: string | null;
|
|
657
|
-
}>,
|
|
658
|
-
): Promise<void> {
|
|
659
|
-
if (rows.length === 0) return;
|
|
660
|
-
for (let i = 0; i < rows.length; i += SqliteBackend.INSERT_BATCH_ROWS) {
|
|
661
|
-
this.db
|
|
662
|
-
.insert(tSessionEvents)
|
|
663
|
-
.values(rows.slice(i, i + SqliteBackend.INSERT_BATCH_ROWS).map((row) => ({ ...row })))
|
|
664
|
-
.run();
|
|
665
|
-
}
|
|
666
|
-
}
|
|
667
|
-
|
|
668
|
-
private async updateHead(
|
|
669
|
-
id: SessionId,
|
|
670
|
-
headEventId: string,
|
|
671
|
-
headSequence: number,
|
|
672
|
-
): Promise<void> {
|
|
673
|
-
this.db
|
|
674
|
-
.update(tSessions)
|
|
675
|
-
.set({ fHeadEventId: headEventId, fHeadSequence: headSequence })
|
|
676
|
-
.where(eq(tSessions.fSessionId, id))
|
|
677
|
-
.run();
|
|
678
|
-
}
|
|
679
|
-
|
|
680
|
-
private async bumpRevision(id: SessionId, lastEventAt?: number): Promise<void> {
|
|
681
|
-
this.db
|
|
682
|
-
.update(tSessions)
|
|
683
|
-
.set({
|
|
684
|
-
fRevision: sql`${tSessions.fRevision} + 1`,
|
|
685
|
-
...(lastEventAt === undefined
|
|
686
|
-
? {}
|
|
687
|
-
: { fLastEventAt: sql`MAX(COALESCE(${tSessions.fLastEventAt}, 0), ${lastEventAt})` }),
|
|
688
|
-
})
|
|
689
|
-
.where(eq(tSessions.fSessionId, id))
|
|
690
|
-
.run();
|
|
691
|
-
}
|
|
692
|
-
|
|
693
|
-
private async refreshTitle(id: SessionId): Promise<void> {
|
|
694
|
-
const row = this.db
|
|
695
|
-
.select({ fSequence: tSessionEvents.fSequence, fData: tEvents.fData })
|
|
696
|
-
.from(tSessionEvents)
|
|
697
|
-
.innerJoin(tEvents, eq(tEvents.fEventId, tSessionEvents.fEventId))
|
|
698
|
-
.where(and(eq(tSessionEvents.fSessionId, id), eq(tEvents.fType, "session/title")))
|
|
699
|
-
.orderBy(desc(tSessionEvents.fSequence))
|
|
700
|
-
.limit(1)
|
|
701
|
-
.get() as { fSequence: number; fData: string } | undefined;
|
|
702
|
-
const title = row === undefined ? undefined : titleOfEventData(row.fData);
|
|
703
|
-
this.db
|
|
704
|
-
.update(tSessions)
|
|
705
|
-
.set({ fTitle: title ?? null, fTitleSeq: title === undefined ? null : row!.fSequence })
|
|
706
|
-
.where(eq(tSessions.fSessionId, id))
|
|
707
|
-
.run();
|
|
708
|
-
}
|
|
709
|
-
|
|
710
|
-
private async deleteBridgeTail(id: SessionId, fromSequence: number): Promise<readonly string[]> {
|
|
711
|
-
const rows = this.db.$client
|
|
712
|
-
.prepare(
|
|
713
|
-
"DELETE FROM t_session_events WHERE f_session_id = ? AND f_sequence >= ? RETURNING f_event_id",
|
|
714
|
-
)
|
|
715
|
-
.all(id, fromSequence) as Array<{ f_event_id: string }>;
|
|
716
|
-
return rows.map((row) => row.f_event_id);
|
|
717
|
-
}
|
|
718
|
-
|
|
719
|
-
private async getPrevBridge(
|
|
720
|
-
id: SessionId,
|
|
721
|
-
sequence: number,
|
|
722
|
-
): Promise<{ fEventId: string; fSequence: number } | undefined> {
|
|
723
|
-
return this.db
|
|
724
|
-
.select({ fEventId: tSessionEvents.fEventId, fSequence: tSessionEvents.fSequence })
|
|
725
|
-
.from(tSessionEvents)
|
|
726
|
-
.where(and(eq(tSessionEvents.fSessionId, id), eq(tSessionEvents.fSequence, sequence)))
|
|
727
|
-
.get() as { fEventId: string; fSequence: number } | undefined;
|
|
728
|
-
}
|
|
729
|
-
|
|
730
|
-
private async deleteSession(id: SessionId): Promise<readonly string[]> {
|
|
731
|
-
const rows = this.db.$client
|
|
732
|
-
.prepare("DELETE FROM t_session_events WHERE f_session_id = ? RETURNING f_event_id")
|
|
733
|
-
.all(id) as Array<{ f_event_id: string }>;
|
|
734
|
-
this.db.delete(tWorkspaceSessions).where(eq(tWorkspaceSessions.fSessionId, id)).run();
|
|
735
|
-
this.db.delete(tSessionProjcacheRows).where(eq(tSessionProjcacheRows.fSessionId, id)).run();
|
|
736
|
-
this.db.delete(tSessions).where(eq(tSessions.fSessionId, id)).run();
|
|
737
|
-
return rows.map((row) => row.f_event_id);
|
|
738
|
-
}
|
|
739
|
-
|
|
740
|
-
// 事件行可能被多个会话共享(fork 派生),所以孤儿只能在全库范围内判定。
|
|
741
|
-
async collectOrphans(): Promise<number> {
|
|
742
|
-
const referenced = this.db.select({ fEventId: tSessionEvents.fEventId }).from(tSessionEvents);
|
|
743
|
-
const info = this.db.delete(tEvents).where(notInArray(tEvents.fEventId, referenced)).run();
|
|
744
|
-
// 用量行跟着事件行走:没有事件行的用量不再计入统计。
|
|
745
|
-
this.db
|
|
746
|
-
.delete(tEventUsage)
|
|
747
|
-
.where(
|
|
748
|
-
notInArray(
|
|
749
|
-
tEventUsage.fEventId,
|
|
750
|
-
this.db.select({ fEventId: tEvents.fEventId }).from(tEvents),
|
|
751
|
-
),
|
|
752
|
-
)
|
|
753
|
-
.run();
|
|
754
|
-
return Number(info.changes);
|
|
755
|
-
}
|
|
756
|
-
|
|
757
|
-
// 父会话被删后留下的 subagent 会话:父已不在表里,或本来就没有父。
|
|
758
|
-
async listOrphanSubagentSessions(): Promise<SessionId[]> {
|
|
759
|
-
const rows = this.db.$client
|
|
760
|
-
.prepare(
|
|
761
|
-
`SELECT f_session_id AS id FROM t_sessions
|
|
762
|
-
WHERE f_origin = 'subagent'
|
|
763
|
-
AND (f_parent_session IS NULL
|
|
764
|
-
OR f_parent_session NOT IN (SELECT f_session_id FROM t_sessions))`,
|
|
765
|
-
)
|
|
766
|
-
.all() as Array<{ id: string }>;
|
|
767
|
-
return rows.map((row) => row.id as SessionId);
|
|
768
|
-
}
|
|
769
|
-
|
|
770
|
-
private async deleteSessions(
|
|
771
|
-
ids: SessionId[],
|
|
772
|
-
): Promise<{ deleted: number; eventIds: readonly string[] }> {
|
|
773
|
-
if (ids.length === 0) return { deleted: 0, eventIds: [] };
|
|
774
|
-
const placeholders = ids.map(() => "?").join(", ");
|
|
775
|
-
const rows = this.db.$client
|
|
776
|
-
.prepare(
|
|
777
|
-
`DELETE FROM t_session_events WHERE f_session_id IN (${placeholders}) RETURNING f_event_id`,
|
|
778
|
-
)
|
|
779
|
-
.all(...ids) as Array<{ f_event_id: string }>;
|
|
780
|
-
this.db.delete(tWorkspaceSessions).where(inArray(tWorkspaceSessions.fSessionId, ids)).run();
|
|
781
|
-
this.db
|
|
782
|
-
.delete(tSessionProjcacheRows)
|
|
783
|
-
.where(inArray(tSessionProjcacheRows.fSessionId, ids))
|
|
784
|
-
.run();
|
|
785
|
-
const info = this.db.delete(tSessions).where(inArray(tSessions.fSessionId, ids)).run();
|
|
786
|
-
return { deleted: Number(info.changes), eventIds: rows.map((row) => row.f_event_id) };
|
|
787
|
-
}
|
|
788
|
-
|
|
789
|
-
// 用量聚合:token 用量沿 `t_event_usage` 的用量行,活动计数(轮次 / 步骤 / 用户输入 / 工具调用)
|
|
790
|
-
// 沿 `t_events` 的事件类型数——两者限定在**有 token 用量的会话**里,时间范围各自按自己的
|
|
791
|
-
// `f_created_at`(同一时刻写入,口径一致)。
|
|
792
|
-
async listSessionRows(query: SessionListRowsQuery = {}): Promise<SessionListRowsPage> {
|
|
793
|
-
const needle = query.query?.trim().toLowerCase() ?? "";
|
|
794
|
-
const like = `%${needle}%`;
|
|
795
|
-
// 排序键用物化列:`f_last_event_at` 由写路径维护、老数据由迁移回填,没有事件的会话回落 createdAt。
|
|
796
|
-
// 早先这里对每行算一次「该会话最后事件时间」的相关子查询——243 行要 4.2s,分页也救不了(排序要求
|
|
797
|
-
// 全表先算出来)。
|
|
798
|
-
const where = `WHERE (? = '' OR lower(COALESCE(s.f_title, '')) LIKE ? OR lower(COALESCE(w.f_title, '')) LIKE ?)
|
|
799
|
-
AND (? = 1 OR s.f_origin IS NOT 'subagent')`;
|
|
800
|
-
const filterParams = [needle, like, like, query.includeSubagents === true ? 1 : 0];
|
|
801
|
-
const limit = query.limit ?? 20;
|
|
802
|
-
const offset = query.offset ?? 0;
|
|
803
|
-
const rows = this.db.$client
|
|
804
|
-
.prepare(
|
|
805
|
-
`SELECT s.f_session_id AS session_id,
|
|
806
|
-
s.f_title AS title,
|
|
807
|
-
s.f_origin AS origin,
|
|
808
|
-
s.f_cwd AS cwd,
|
|
809
|
-
s.f_created_at AS created_at,
|
|
810
|
-
(s.f_archived_at IS NOT NULL) AS archived,
|
|
811
|
-
COALESCE(s.f_last_event_at, s.f_created_at) AS updated_at,
|
|
812
|
-
w.f_title AS workspace
|
|
813
|
-
FROM t_sessions s
|
|
814
|
-
LEFT JOIN t_workspaces w ON w.f_workspace_id = (
|
|
815
|
-
SELECT b.f_workspace_id FROM t_workspace_sessions b
|
|
816
|
-
WHERE b.f_session_id = s.f_session_id ORDER BY b.f_position LIMIT 1)
|
|
817
|
-
${where}
|
|
818
|
-
ORDER BY updated_at DESC, s.f_session_id
|
|
819
|
-
LIMIT ? OFFSET ?`,
|
|
820
|
-
)
|
|
821
|
-
.all(...filterParams, limit, offset) as unknown as Array<Record<string, unknown>>;
|
|
822
|
-
const counted = this.db.$client
|
|
823
|
-
.prepare(
|
|
824
|
-
`SELECT count(*) AS n
|
|
825
|
-
FROM t_sessions s
|
|
826
|
-
LEFT JOIN t_workspaces w ON w.f_workspace_id = (
|
|
827
|
-
SELECT b.f_workspace_id FROM t_workspace_sessions b
|
|
828
|
-
WHERE b.f_session_id = s.f_session_id ORDER BY b.f_position LIMIT 1)
|
|
829
|
-
${where}`,
|
|
830
|
-
)
|
|
831
|
-
.get(...filterParams) as { n: number };
|
|
832
|
-
return {
|
|
833
|
-
items: rows.map((row) => sessionListRowOf(row)),
|
|
834
|
-
total: Number(counted.n),
|
|
835
|
-
};
|
|
836
|
-
}
|
|
837
|
-
|
|
838
|
-
// 用量聚合:只读三张派生统计表——事件级用量行给去重口径的总量与「天 × 模型」桶,`t_session_usage` /
|
|
839
|
-
// `t_session_counts` 给按会话的行(**含 fork 继承前缀**,见 ADR-fork不继承统计);归属、本地日与「是否被引用」
|
|
840
|
-
// 都是物化列,不再现算、也不
|
|
841
|
-
// 回连事件表。事件级按毫秒过滤、汇总表按本地日过滤,`sinceMs` 对齐本地零点时两者等价。
|
|
842
|
-
async usageReport(sinceMs?: number): Promise<UsageAggregate> {
|
|
843
|
-
const usageSince = sinceMs === undefined ? "" : " AND f_created_at >= ?";
|
|
844
|
-
const params = sinceMs === undefined ? [] : [sinceMs];
|
|
845
|
-
const daySince = sinceMs === undefined ? "" : " AND %COL% >= ?";
|
|
846
|
-
const dayParams = sinceMs === undefined ? [] : [localDayKey(sinceMs)];
|
|
847
|
-
const dayFilter = (column: string): string => daySince.replace("%COL%", column);
|
|
848
|
-
|
|
849
|
-
const buckets = this.db.$client
|
|
850
|
-
.prepare(
|
|
851
|
-
`SELECT f_day AS day,
|
|
852
|
-
f_provider AS provider,
|
|
853
|
-
f_model AS model,
|
|
854
|
-
f_subagent AS subagent,
|
|
855
|
-
sum(f_input_tokens) AS input_tokens,
|
|
856
|
-
sum(f_output_tokens) AS output_tokens,
|
|
857
|
-
sum(f_cache_read_tokens) AS cache_read_tokens,
|
|
858
|
-
sum(f_reasoning_tokens) AS reasoning_tokens,
|
|
859
|
-
sum(f_total_tokens) AS total_tokens
|
|
860
|
-
FROM t_event_usage
|
|
861
|
-
WHERE f_referenced = 1${usageSince}
|
|
862
|
-
GROUP BY f_day, f_provider, f_model, f_subagent`,
|
|
863
|
-
)
|
|
864
|
-
.all(...params) as unknown as RawBucketRow[];
|
|
865
|
-
const tokenScope = this.db.$client
|
|
866
|
-
.prepare(
|
|
867
|
-
`SELECT f_subagent AS subagent,
|
|
868
|
-
sum(f_input_tokens) AS input_tokens,
|
|
869
|
-
sum(f_output_tokens) AS output_tokens,
|
|
870
|
-
sum(f_cache_read_tokens) AS cache_read_tokens,
|
|
871
|
-
sum(f_reasoning_tokens) AS reasoning_tokens,
|
|
872
|
-
sum(f_total_tokens) AS total_tokens
|
|
873
|
-
FROM t_event_usage
|
|
874
|
-
WHERE f_referenced = 1${usageSince}
|
|
875
|
-
GROUP BY f_subagent`,
|
|
876
|
-
)
|
|
877
|
-
.all(...params) as unknown as RawScopeRow[];
|
|
878
|
-
const activityScope = this.db.$client
|
|
879
|
-
.prepare(
|
|
880
|
-
`SELECT (s.f_origin = 'subagent') AS subagent,
|
|
881
|
-
sum(c.f_turns) AS turns,
|
|
882
|
-
sum(c.f_steps) AS steps,
|
|
883
|
-
sum(c.f_user_inputs) AS user_inputs,
|
|
884
|
-
sum(c.f_tool_calls) AS tool_calls
|
|
885
|
-
FROM t_session_counts c
|
|
886
|
-
JOIN t_sessions s ON s.f_session_id = c.f_session_id
|
|
887
|
-
WHERE ${hasUsageSession("c.f_session_id")}${dayFilter("c.f_day")}
|
|
888
|
-
GROUP BY subagent`,
|
|
889
|
-
)
|
|
890
|
-
.all(...dayParams) as unknown as RawActivityScopeRow[];
|
|
891
|
-
const tokenSessions = this.db.$client
|
|
892
|
-
.prepare(
|
|
893
|
-
`SELECT u.f_session_id AS session_id,
|
|
894
|
-
s.f_title AS title,
|
|
895
|
-
(s.f_origin = 'subagent') AS subagent,
|
|
896
|
-
(s.f_archived_at IS NOT NULL) AS archived,
|
|
897
|
-
sum(u.f_input_tokens) AS input_tokens,
|
|
898
|
-
sum(u.f_output_tokens) AS output_tokens,
|
|
899
|
-
sum(u.f_cache_read_tokens) AS cache_read_tokens,
|
|
900
|
-
sum(u.f_reasoning_tokens) AS reasoning_tokens,
|
|
901
|
-
sum(u.f_total_tokens) AS total_tokens
|
|
902
|
-
FROM t_session_usage u
|
|
903
|
-
JOIN t_sessions s ON s.f_session_id = u.f_session_id
|
|
904
|
-
WHERE 1 = 1${dayFilter("u.f_day")}
|
|
905
|
-
GROUP BY u.f_session_id, s.f_title, s.f_origin, s.f_archived_at`,
|
|
906
|
-
)
|
|
907
|
-
.all(...dayParams) as unknown as RawSessionTokenRow[];
|
|
908
|
-
const activitySessions = this.db.$client
|
|
909
|
-
.prepare(
|
|
910
|
-
`SELECT f_session_id AS session_id,
|
|
911
|
-
sum(f_turns) AS turns,
|
|
912
|
-
sum(f_steps) AS steps,
|
|
913
|
-
sum(f_user_inputs) AS user_inputs,
|
|
914
|
-
sum(f_tool_calls) AS tool_calls
|
|
915
|
-
FROM t_session_counts
|
|
916
|
-
WHERE 1 = 1${dayFilter("f_day")}
|
|
917
|
-
GROUP BY f_session_id`,
|
|
918
|
-
)
|
|
919
|
-
.all(...dayParams) as unknown as RawSessionCountRow[];
|
|
920
|
-
const human = emptyTotals();
|
|
921
|
-
const subagent = emptyTotals();
|
|
922
|
-
for (const row of tokenScope) {
|
|
923
|
-
addTokenTotals(row.subagent === 1 ? subagent : human, tokenTotalsOf(row));
|
|
924
|
-
}
|
|
925
|
-
for (const row of activityScope) {
|
|
926
|
-
addActivityTotals(row.subagent === 1 ? subagent : human, activityTotalsOf(row));
|
|
927
|
-
}
|
|
928
|
-
const totals = addTotals(addTotals(emptyTotals(), human), subagent);
|
|
929
|
-
|
|
930
|
-
const activityBySession = new Map<string, UsageActivityTotals>();
|
|
931
|
-
for (const row of activitySessions) {
|
|
932
|
-
activityBySession.set(row.session_id, activityTotalsOf(row));
|
|
933
|
-
}
|
|
934
|
-
return {
|
|
935
|
-
totals,
|
|
936
|
-
subagent,
|
|
937
|
-
human,
|
|
938
|
-
buckets: buckets.map((row) => ({
|
|
939
|
-
day: row.day,
|
|
940
|
-
provider: row.provider,
|
|
941
|
-
model: row.model,
|
|
942
|
-
subagent: row.subagent === 1,
|
|
943
|
-
...tokenTotalsOf(row),
|
|
944
|
-
})),
|
|
945
|
-
sessions: tokenSessions.map((row) => ({
|
|
946
|
-
sessionId: row.session_id,
|
|
947
|
-
title: row.title,
|
|
948
|
-
subagent: row.subagent === 1,
|
|
949
|
-
archived: row.archived === 1,
|
|
950
|
-
...tokenTotalsOf(row),
|
|
951
|
-
...(activityBySession.get(row.session_id) ?? emptyActivity()),
|
|
952
|
-
})),
|
|
953
|
-
};
|
|
954
|
-
}
|
|
955
|
-
|
|
956
|
-
// token 用量旁路累加:派生表 `t_session_usage`,不在写事务里(失败可丢,表可销毁重建)。
|
|
957
|
-
async incrementSessionUsage(
|
|
958
|
-
id: SessionId,
|
|
959
|
-
buckets: readonly SessionUsageBucket[],
|
|
960
|
-
): Promise<void> {
|
|
961
|
-
if (buckets.length === 0) return;
|
|
962
|
-
const statement = this.db.$client.prepare(
|
|
963
|
-
`INSERT INTO t_session_usage (f_session_id, f_day, f_provider, f_model,
|
|
964
|
-
f_input_tokens, f_output_tokens, f_cache_read_tokens, f_reasoning_tokens, f_total_tokens)
|
|
965
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
966
|
-
ON CONFLICT(f_session_id, f_day, f_provider, f_model) DO UPDATE SET
|
|
967
|
-
f_input_tokens = f_input_tokens + excluded.f_input_tokens,
|
|
968
|
-
f_output_tokens = f_output_tokens + excluded.f_output_tokens,
|
|
969
|
-
f_cache_read_tokens = f_cache_read_tokens + excluded.f_cache_read_tokens,
|
|
970
|
-
f_reasoning_tokens = f_reasoning_tokens + excluded.f_reasoning_tokens,
|
|
971
|
-
f_total_tokens = f_total_tokens + excluded.f_total_tokens`,
|
|
972
|
-
);
|
|
973
|
-
for (const bucket of buckets) {
|
|
974
|
-
statement.run(
|
|
975
|
-
id,
|
|
976
|
-
bucket.day,
|
|
977
|
-
bucket.provider ?? "",
|
|
978
|
-
bucket.model ?? "",
|
|
979
|
-
bucket.inputTokens,
|
|
980
|
-
bucket.outputTokens,
|
|
981
|
-
bucket.cacheReadTokens,
|
|
982
|
-
bucket.reasoningTokens,
|
|
983
|
-
bucket.totalTokens,
|
|
984
|
-
);
|
|
985
|
-
}
|
|
986
|
-
}
|
|
987
|
-
|
|
988
|
-
// 活动计数旁路累加:派生表 `t_session_counts`,同上(四项计数一次落)。
|
|
989
|
-
async incrementSessionCounts(
|
|
990
|
-
id: SessionId,
|
|
991
|
-
buckets: readonly SessionCountBucket[],
|
|
992
|
-
): Promise<void> {
|
|
993
|
-
if (buckets.length === 0) return;
|
|
994
|
-
const statement = this.db.$client.prepare(
|
|
995
|
-
`INSERT INTO t_session_counts (f_session_id, f_day, f_turns, f_steps, f_user_inputs, f_tool_calls)
|
|
996
|
-
VALUES (?, ?, ?, ?, ?, ?)
|
|
997
|
-
ON CONFLICT(f_session_id, f_day) DO UPDATE SET
|
|
998
|
-
f_turns = f_turns + excluded.f_turns,
|
|
999
|
-
f_steps = f_steps + excluded.f_steps,
|
|
1000
|
-
f_user_inputs = f_user_inputs + excluded.f_user_inputs,
|
|
1001
|
-
f_tool_calls = f_tool_calls + excluded.f_tool_calls`,
|
|
1002
|
-
);
|
|
1003
|
-
for (const bucket of buckets) {
|
|
1004
|
-
statement.run(
|
|
1005
|
-
id,
|
|
1006
|
-
bucket.day,
|
|
1007
|
-
bucket.turns,
|
|
1008
|
-
bucket.steps,
|
|
1009
|
-
bucket.userInputs,
|
|
1010
|
-
bucket.toolCalls,
|
|
1011
|
-
);
|
|
1012
|
-
}
|
|
1013
|
-
}
|
|
1014
|
-
|
|
1015
|
-
// 按会话重算两张汇总表(旧日志重写后):先删该会话的行,再从事件表重算——rewind 与 fork 不走这里。
|
|
1016
|
-
async rebuildSessionStats(id: SessionId): Promise<void> {
|
|
1017
|
-
this.db.$client.prepare("DELETE FROM t_session_usage WHERE f_session_id = ?").run(id);
|
|
1018
|
-
this.db.$client
|
|
1019
|
-
.prepare(
|
|
1020
|
-
`INSERT INTO t_session_usage (
|
|
1021
|
-
f_session_id, f_day, f_provider, f_model,
|
|
1022
|
-
f_input_tokens, f_output_tokens, f_cache_read_tokens, f_reasoning_tokens, f_total_tokens)
|
|
1023
|
-
SELECT b.f_session_id, u.f_day, coalesce(u.f_provider, ''), coalesce(u.f_model, ''),
|
|
1024
|
-
sum(u.f_input_tokens), sum(u.f_output_tokens), sum(u.f_cache_read_tokens),
|
|
1025
|
-
sum(u.f_reasoning_tokens), sum(u.f_total_tokens)
|
|
1026
|
-
FROM t_session_events b
|
|
1027
|
-
JOIN t_event_usage u ON u.f_event_id = b.f_event_id
|
|
1028
|
-
WHERE b.f_session_id = ?
|
|
1029
|
-
GROUP BY b.f_session_id, u.f_day, coalesce(u.f_provider, ''), coalesce(u.f_model, '')`,
|
|
1030
|
-
)
|
|
1031
|
-
.run(id);
|
|
1032
|
-
this.db.$client.prepare("DELETE FROM t_session_counts WHERE f_session_id = ?").run(id);
|
|
1033
|
-
this.db.$client
|
|
1034
|
-
.prepare(
|
|
1035
|
-
`INSERT INTO t_session_counts (
|
|
1036
|
-
f_session_id, f_day, f_turns, f_steps, f_user_inputs, f_tool_calls)
|
|
1037
|
-
SELECT b.f_session_id,
|
|
1038
|
-
date(e.f_created_at / 1000, 'unixepoch', 'localtime'),
|
|
1039
|
-
sum(CASE WHEN e.f_type = 'turn/start' THEN 1 ELSE 0 END),
|
|
1040
|
-
sum(CASE WHEN e.f_type = 'step/start' THEN 1 ELSE 0 END),
|
|
1041
|
-
sum(CASE WHEN e.f_type = 'user/message' THEN 1 ELSE 0 END),
|
|
1042
|
-
sum(CASE WHEN e.f_type = 'tool/call' THEN 1 ELSE 0 END)
|
|
1043
|
-
FROM t_session_events b
|
|
1044
|
-
JOIN t_events e ON e.f_event_id = b.f_event_id
|
|
1045
|
-
WHERE b.f_session_id = ? AND e.f_type IN (${COUNTED_EVENT_TYPE_SQL})
|
|
1046
|
-
GROUP BY b.f_session_id, 2`,
|
|
1047
|
-
)
|
|
1048
|
-
.run(id);
|
|
1049
|
-
}
|
|
1050
|
-
|
|
1051
|
-
// 重算这些用量行的引用标记:引用只会在「这些事件行的桥接行集合变化」时才变,所以按事件 id 收口。
|
|
1052
|
-
// 全表 UPDATE(3 万行量级、秒级)没有调用点了:撤回与 fork 都不该动统计。
|
|
1053
|
-
async refreshEventUsageFlags(eventIds: readonly string[]): Promise<void> {
|
|
1054
|
-
for (let at = 0; at < eventIds.length; at += REFRESH_FLAG_CHUNK) {
|
|
1055
|
-
const chunk = eventIds.slice(at, at + REFRESH_FLAG_CHUNK);
|
|
1056
|
-
const placeholders = chunk.map(() => "?").join(", ");
|
|
1057
|
-
this.db.$client
|
|
1058
|
-
.prepare(
|
|
1059
|
-
`UPDATE t_event_usage SET
|
|
1060
|
-
f_referenced = ${withEvent(EVENT_REFERENCED_SQL, "f_event_id")},
|
|
1061
|
-
f_subagent = ${withEvent(EVENT_SUBAGENT_SQL, "f_event_id")}
|
|
1062
|
-
WHERE f_event_id IN (${placeholders})`,
|
|
1063
|
-
)
|
|
1064
|
-
.run(...chunk);
|
|
1065
|
-
}
|
|
1066
|
-
}
|
|
1067
|
-
|
|
1068
|
-
async deleteSessionStats(id: SessionId): Promise<void> {
|
|
1069
|
-
this.db.$client.prepare("DELETE FROM t_session_usage WHERE f_session_id = ?").run(id);
|
|
1070
|
-
this.db.$client.prepare("DELETE FROM t_session_counts WHERE f_session_id = ?").run(id);
|
|
1071
|
-
}
|
|
1072
|
-
|
|
1073
|
-
async vacuum(): Promise<void> {
|
|
1074
|
-
await enqueueSqliteTx(this.dbPath, async () => {
|
|
1075
|
-
this.db.$client.exec("VACUUM");
|
|
1076
|
-
});
|
|
1077
|
-
}
|
|
1078
|
-
|
|
1079
|
-
private eventRows() {
|
|
1080
|
-
return this.db
|
|
1081
|
-
.select({
|
|
1082
|
-
fEventId: tSessionEvents.fEventId,
|
|
1083
|
-
fSequence: tSessionEvents.fSequence,
|
|
1084
|
-
fType: tEvents.fType,
|
|
1085
|
-
fKind: tEvents.fKind,
|
|
1086
|
-
fRole: tEvents.fRole,
|
|
1087
|
-
fName: tEvents.fName,
|
|
1088
|
-
fActionId: tEvents.fActionId,
|
|
1089
|
-
fCreatedAt: tEvents.fCreatedAt,
|
|
1090
|
-
fData: tEvents.fData,
|
|
1091
|
-
fSurfaceOp: tSessionEvents.fSurfaceOp,
|
|
1092
|
-
})
|
|
1093
|
-
.from(tSessionEvents)
|
|
1094
|
-
.innerJoin(tEvents, eq(tSessionEvents.fEventId, tEvents.fEventId));
|
|
1095
|
-
}
|
|
1096
|
-
}
|