@morlay/ui-conversation-message-actions 0.0.19-alpha.7 → 0.0.19-alpha.9
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 +5 -5
- package/dist/client.cjs +1 -1
- package/dist/testing.d.cts +6 -7
- package/dist/testing.d.mts +6 -7
- package/dist/testing.mjs +906 -508
- package/locale/en.json +1 -1
- package/locale/zh.json +1 -1
- package/package.json +26 -31
- package/src/client/chat-node/MessageItem.styles.ts +2 -1
- package/src/client/composer-stats/StatsPills.tsx +4 -1
- package/cordis.patch.yml +0 -9
package/dist/testing.mjs
CHANGED
|
@@ -20514,6 +20514,157 @@ createSessionFormatCatalog({
|
|
|
20514
20514
|
}
|
|
20515
20515
|
});
|
|
20516
20516
|
//#endregion
|
|
20517
|
+
//#region ../session-rdb/src/usage.ts
|
|
20518
|
+
const SESSION_USAGE_PATH = "/api/session.usage";
|
|
20519
|
+
const ROLLING_DAYS = {
|
|
20520
|
+
"7d": 7,
|
|
20521
|
+
"30d": 30,
|
|
20522
|
+
"90d": 90
|
|
20523
|
+
};
|
|
20524
|
+
function localDayStart(now) {
|
|
20525
|
+
const start = new Date(now);
|
|
20526
|
+
start.setHours(0, 0, 0, 0);
|
|
20527
|
+
return start;
|
|
20528
|
+
}
|
|
20529
|
+
/**
|
|
20530
|
+
* 范围起点(含),不限时为 undefined。**所有起点都对齐到 host 本地时区的零点**——`day` / `week` 是
|
|
20531
|
+
* 今天 / 本周一的零点,`7d` / `30d` / `90d` 是「今天零点往前 N-1 天」(含今天共 N 个自然日)。
|
|
20532
|
+
* 对齐的意义:事件级表按毫秒过滤、会话汇总表按本地日过滤,两者因此严格等价。
|
|
20533
|
+
* @param range - 语义键。
|
|
20534
|
+
* @param now - 当前时刻。
|
|
20535
|
+
* @returns 起点毫秒时间戳,或 undefined。
|
|
20536
|
+
*/
|
|
20537
|
+
function resolveUsageSince(range, now) {
|
|
20538
|
+
switch (range) {
|
|
20539
|
+
case "all": return;
|
|
20540
|
+
case "day": return localDayStart(now).getTime();
|
|
20541
|
+
case "week": {
|
|
20542
|
+
const start = localDayStart(now);
|
|
20543
|
+
start.setDate(start.getDate() - (start.getDay() + 6) % 7);
|
|
20544
|
+
return start.getTime();
|
|
20545
|
+
}
|
|
20546
|
+
default: {
|
|
20547
|
+
const start = localDayStart(now);
|
|
20548
|
+
start.setDate(start.getDate() - (ROLLING_DAYS[range] - 1));
|
|
20549
|
+
return start.getTime();
|
|
20550
|
+
}
|
|
20551
|
+
}
|
|
20552
|
+
}
|
|
20553
|
+
function parseUsageRange(value) {
|
|
20554
|
+
return value === "day" || value === "week" || value === "7d" || value === "30d" || value === "90d" ? value : "all";
|
|
20555
|
+
}
|
|
20556
|
+
/** 计入活动计数的事件类型(轮次 / 步骤 / 用户输入 / 工具调用)。 */
|
|
20557
|
+
const COUNTED_EVENT_TYPES = [
|
|
20558
|
+
"turn/start",
|
|
20559
|
+
"step/start",
|
|
20560
|
+
"user/message",
|
|
20561
|
+
"tool/call"
|
|
20562
|
+
];
|
|
20563
|
+
/** 按事件类型把计数累加到活动指标上(表里按类型存,读的时候折成四项)。 */
|
|
20564
|
+
function addActivityCount(target, type, count) {
|
|
20565
|
+
switch (type) {
|
|
20566
|
+
case "turn/start":
|
|
20567
|
+
target.turns += count;
|
|
20568
|
+
return target;
|
|
20569
|
+
case "step/start":
|
|
20570
|
+
target.steps += count;
|
|
20571
|
+
return target;
|
|
20572
|
+
case "user/message":
|
|
20573
|
+
target.userInputs += count;
|
|
20574
|
+
return target;
|
|
20575
|
+
case "tool/call":
|
|
20576
|
+
target.toolCalls += count;
|
|
20577
|
+
return target;
|
|
20578
|
+
default: return target;
|
|
20579
|
+
}
|
|
20580
|
+
}
|
|
20581
|
+
/** 毫秒时间戳 → host 本地日(`YYYY-MM-DD`,与 SQL 的 localtime 口径一致)。 */
|
|
20582
|
+
function localDayKey(ms) {
|
|
20583
|
+
const date = new Date(ms);
|
|
20584
|
+
const pad = (value) => String(value).padStart(2, "0");
|
|
20585
|
+
return `${String(date.getFullYear())}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`;
|
|
20586
|
+
}
|
|
20587
|
+
function emptyTotals() {
|
|
20588
|
+
return {
|
|
20589
|
+
turns: 0,
|
|
20590
|
+
steps: 0,
|
|
20591
|
+
userInputs: 0,
|
|
20592
|
+
toolCalls: 0,
|
|
20593
|
+
inputTokens: 0,
|
|
20594
|
+
outputTokens: 0,
|
|
20595
|
+
cacheReadTokens: 0,
|
|
20596
|
+
reasoningTokens: 0,
|
|
20597
|
+
totalTokens: 0
|
|
20598
|
+
};
|
|
20599
|
+
}
|
|
20600
|
+
function addTokenTotals(target, row) {
|
|
20601
|
+
target.inputTokens += row.inputTokens;
|
|
20602
|
+
target.outputTokens += row.outputTokens;
|
|
20603
|
+
target.cacheReadTokens += row.cacheReadTokens;
|
|
20604
|
+
target.reasoningTokens += row.reasoningTokens;
|
|
20605
|
+
target.totalTokens += row.totalTokens;
|
|
20606
|
+
return target;
|
|
20607
|
+
}
|
|
20608
|
+
function addActivityTotals(target, row) {
|
|
20609
|
+
target.turns += row.turns;
|
|
20610
|
+
target.steps += row.steps;
|
|
20611
|
+
target.userInputs += row.userInputs;
|
|
20612
|
+
target.toolCalls += row.toolCalls;
|
|
20613
|
+
return target;
|
|
20614
|
+
}
|
|
20615
|
+
/** 把一行的整套指标累加进目标(后端合并「是否子代理」两组时用)。 */
|
|
20616
|
+
function addTotals(target, row) {
|
|
20617
|
+
addActivityTotals(target, row);
|
|
20618
|
+
addTokenTotals(target, row);
|
|
20619
|
+
return target;
|
|
20620
|
+
}
|
|
20621
|
+
/**
|
|
20622
|
+
* 用量统计通道:一次请求回报总览(含 subagent 拆分)、按天 × 模型的桶与按会话的行。
|
|
20623
|
+
* 聚合只算被会话引用的事件行——fork 共享行因此只计一次,已删会话留下的孤儿行不计。
|
|
20624
|
+
*/
|
|
20625
|
+
function registerSessionUsage(ctx, persistence) {
|
|
20626
|
+
ctx.inject(["webServer", "connection"], (webCtx) => {
|
|
20627
|
+
const webServer = webCtx.webServer;
|
|
20628
|
+
const connection = webCtx.get("connection");
|
|
20629
|
+
return webCtx.effect(() => webServer.register({
|
|
20630
|
+
kind: "exact",
|
|
20631
|
+
path: SESSION_USAGE_PATH,
|
|
20632
|
+
handler: async (req, res) => {
|
|
20633
|
+
const rejection = connection.requestRejection(req);
|
|
20634
|
+
if (rejection !== void 0) {
|
|
20635
|
+
res.writeHead(rejection);
|
|
20636
|
+
res.end(rejection === 401 ? "unauthorized" : "forbidden");
|
|
20637
|
+
return;
|
|
20638
|
+
}
|
|
20639
|
+
if (req.method !== "POST") {
|
|
20640
|
+
res.writeHead(405, { "content-type": "application/json" });
|
|
20641
|
+
res.end(JSON.stringify({ error: "method not allowed" }));
|
|
20642
|
+
return;
|
|
20643
|
+
}
|
|
20644
|
+
const chunks = [];
|
|
20645
|
+
for await (const chunk of req) chunks.push(chunk);
|
|
20646
|
+
const raw = Buffer.concat(chunks).toString("utf8");
|
|
20647
|
+
let envelope = {};
|
|
20648
|
+
if (raw !== "") try {
|
|
20649
|
+
envelope = JSON.parse(raw);
|
|
20650
|
+
} catch {
|
|
20651
|
+
res.writeHead(400, { "content-type": "application/json" });
|
|
20652
|
+
res.end(JSON.stringify({ error: "request body is not JSON" }));
|
|
20653
|
+
return;
|
|
20654
|
+
}
|
|
20655
|
+
try {
|
|
20656
|
+
const report = await persistence.usageReport(resolveUsageSince(parseUsageRange(envelope.range), Date.now()));
|
|
20657
|
+
res.writeHead(200, { "content-type": "application/json" });
|
|
20658
|
+
res.end(JSON.stringify(report));
|
|
20659
|
+
} catch (error) {
|
|
20660
|
+
res.writeHead(500, { "content-type": "application/json" });
|
|
20661
|
+
res.end(JSON.stringify({ error: error instanceof Error ? error.message : "usage report failed" }));
|
|
20662
|
+
}
|
|
20663
|
+
}
|
|
20664
|
+
}), `session-rdb: ${SESSION_USAGE_PATH} route`);
|
|
20665
|
+
});
|
|
20666
|
+
}
|
|
20667
|
+
//#endregion
|
|
20517
20668
|
//#region ../session-rdb/src/log.ts
|
|
20518
20669
|
function normalizeSurfaceOp(value) {
|
|
20519
20670
|
if (typeof value !== "object" || value === null || Array.isArray(value)) return value;
|
|
@@ -20868,15 +21019,15 @@ function renameLegacyPtcEvents(events) {
|
|
|
20868
21019
|
* (v4 起 system 消息只认这个 kind);
|
|
20869
21020
|
* - `tool/result` 的消息:v3 是 user 角色、结果块包在 `content[0]`;v4 是 `role: 'tool'` + 顶层
|
|
20870
21021
|
* `toolCallId` + `content` 就是结果块;
|
|
20871
|
-
* -
|
|
20872
|
-
*
|
|
21022
|
+
* - **本仓库自造、上游不认识的事件类型**(见 `OWN_EVENT_TYPES`:历史遗留的与我们当前在写的)标
|
|
21023
|
+
* `ignorable: true`——v4 的校验对未知类型只接受可忽略信封。
|
|
20873
21024
|
*
|
|
20874
21025
|
* 只认这张白名单:**其它未知类型保持 fail loud**(那是「数据来自更新的 harness」的信号,不能吞掉)。
|
|
20875
21026
|
*/
|
|
20876
21027
|
function normalizeToCurrentShape(events) {
|
|
20877
21028
|
for (const event of events) {
|
|
20878
21029
|
const type = event.type;
|
|
20879
|
-
if (
|
|
21030
|
+
if (OWN_EVENT_TYPES.has(type)) {
|
|
20880
21031
|
event.ignorable = true;
|
|
20881
21032
|
continue;
|
|
20882
21033
|
}
|
|
@@ -20919,16 +21070,36 @@ function liftToolResultMessage(message) {
|
|
|
20919
21070
|
};
|
|
20920
21071
|
}
|
|
20921
21072
|
/**
|
|
20922
|
-
*
|
|
20923
|
-
*
|
|
21073
|
+
* 落库前给**本仓库自造的事件类型**补上 `ignorable: true` 信封。
|
|
21074
|
+
*
|
|
21075
|
+
* 上游的持久化校验(`validateStoredEvents`)只放行「已知类型」或「带可忽略信封的未知类型」,我们的类型
|
|
21076
|
+
* 按构造就在已知表之外——不补的话连**写入**都会被拒(`appendBatch` 里那道校验)。上游 `Session.append`
|
|
21077
|
+
* 没有承载这个信封的口子(第三个参数只服务 surface 事件),所以这一笔由持久化层补:canonical log 里的
|
|
21078
|
+
* 形状不变,落库的那一份带上信封。
|
|
21079
|
+
* @param events - 即将落库的批次(`materializeAppendBatch` 的 JSON 快照副本,可写)。
|
|
20924
21080
|
*/
|
|
20925
|
-
|
|
20926
|
-
|
|
20927
|
-
|
|
20928
|
-
|
|
21081
|
+
function sealOwnEvents(events) {
|
|
21082
|
+
for (const event of events) if (OWN_EVENT_TYPES.has(event.type)) event.ignorable = true;
|
|
21083
|
+
}
|
|
21084
|
+
/**
|
|
21085
|
+
* 本仓库自造、上游不认识的事件类型:读的时候标 `ignorable: true`,v4 的持久化校验才肯跳过它们
|
|
21086
|
+
* (`validateStoredEvents` 只放行「已知类型」或「带可忽略信封的未知类型」)。
|
|
21087
|
+
*
|
|
21088
|
+
* 两类都在这张表里:
|
|
21089
|
+
* - **历史遗留**:写过、后来连同机制一起删除的(`session-branch/version`);
|
|
21090
|
+
* - **当前在写**:本仓库的插件自己声明的事件类型(`session-mode/selected`)——上游的
|
|
21091
|
+
* `KNOWN_SESSION_EVENT_TYPES` 由上游仓库的声明生成,外部插件的事件按构造就在它之外。
|
|
21092
|
+
*
|
|
21093
|
+
* 新增自造事件类型时**必须**登记到这里,否则读路径 fail loud(真回归:`session-mode/selected` 漏登记,
|
|
21094
|
+
* 会话打不开)。**不要**用「所有未知类型」代替(那会掩盖更新版本的未知事件——那种必须拒读)。
|
|
21095
|
+
*/
|
|
21096
|
+
const OWN_EVENT_TYPES = /* @__PURE__ */ new Set(["session-branch/version", "session-mode/selected"]);
|
|
21097
|
+
/** 这批事件是否需要按当前形状归一(旧代消息形状,或尚未标 `ignorable` 的自造事件类型)——写路径曾把回退
|
|
21098
|
+
* 视图的结果以当前版本号落库,所以**版本号不可信**,读之前要按内容判一次。 */
|
|
21099
|
+
function needsShapeAdoption(events) {
|
|
20929
21100
|
for (const event of events) {
|
|
20930
21101
|
const type = event.type;
|
|
20931
|
-
if (
|
|
21102
|
+
if (OWN_EVENT_TYPES.has(type)) return true;
|
|
20932
21103
|
if (type === "system/message") {
|
|
20933
21104
|
if ((event.data["message"]?.["source"])?.["kind"] === "plugin") return true;
|
|
20934
21105
|
continue;
|
|
@@ -21069,10 +21240,13 @@ function textField(value) {
|
|
|
21069
21240
|
/**
|
|
21070
21241
|
* 一条事件行的用量(只有带 `usage` 的 `assistant/message` 才有):写路径据此记录
|
|
21071
21242
|
* `t_event_usage`,统计因此不必逐行解析 JSON。两种 `f_data` 结构(带信封 / 老格式)都认。
|
|
21243
|
+
* 读侧维度一起物化:本地日由事件时间算,`f_referenced` 直接 1(这一批事件行与桥接行同事务,
|
|
21244
|
+
* 提交即被引用),`f_subagent` 取写入它的会话是否子代理。
|
|
21072
21245
|
* @param event - 待写入的事件行。
|
|
21246
|
+
* @param subagent - 写入它的会话是否 `origin === 'subagent'`。
|
|
21073
21247
|
* @returns 用量行,或该事件没有用量时的 undefined。
|
|
21074
21248
|
*/
|
|
21075
|
-
function usageRowOf(event) {
|
|
21249
|
+
function usageRowOf(event, subagent) {
|
|
21076
21250
|
if (event.fType !== "assistant/message") return void 0;
|
|
21077
21251
|
let parsed;
|
|
21078
21252
|
try {
|
|
@@ -21087,8 +21261,11 @@ function usageRowOf(event) {
|
|
|
21087
21261
|
return {
|
|
21088
21262
|
fEventId: event.fEventId,
|
|
21089
21263
|
fCreatedAt: event.fCreatedAt,
|
|
21264
|
+
fDay: localDayKey(event.fCreatedAt),
|
|
21090
21265
|
fProvider: textField(source?.["provider"]),
|
|
21091
21266
|
fModel: textField(source?.["model"]),
|
|
21267
|
+
fReferenced: 1,
|
|
21268
|
+
fSubagent: subagent ? 1 : 0,
|
|
21092
21269
|
fInputTokens: numericField(usage["inputTokens"]),
|
|
21093
21270
|
fOutputTokens: numericField(usage["outputTokens"]),
|
|
21094
21271
|
fCacheReadTokens: numericField(usage["cacheReadTokens"]),
|
|
@@ -25025,7 +25202,9 @@ const sessionProjcacheRows = {
|
|
|
25025
25202
|
//#region ../session-rdb/src/entities/v3/event-usage.ts
|
|
25026
25203
|
/**
|
|
25027
25204
|
* 用量日志:一条 `assistant/message` 事件一行(按事件行唯一,fork 共享行不重复)。
|
|
25028
|
-
*
|
|
25205
|
+
* 读侧维度全部物化在行上——本地日、是否被会话引用、是否被子代理会话引用——统计因此不做
|
|
25206
|
+
* `EXISTS` 判定、也不回连事件表;事件行本身仍留在 `t_events`。
|
|
25207
|
+
* 取舍见 ADR-统计衍生表物化归属与汇总。
|
|
25029
25208
|
*/
|
|
25030
25209
|
const eventUsage = {
|
|
25031
25210
|
name: "t_event_usage",
|
|
@@ -25043,8 +25222,22 @@ const eventUsage = {
|
|
|
25043
25222
|
type: "bigint",
|
|
25044
25223
|
notNull: true
|
|
25045
25224
|
},
|
|
25225
|
+
f_day: {
|
|
25226
|
+
type: "text",
|
|
25227
|
+
notNull: true
|
|
25228
|
+
},
|
|
25046
25229
|
f_provider: { type: "text" },
|
|
25047
25230
|
f_model: { type: "text" },
|
|
25231
|
+
f_referenced: {
|
|
25232
|
+
type: "integer",
|
|
25233
|
+
notNull: true,
|
|
25234
|
+
default: 0
|
|
25235
|
+
},
|
|
25236
|
+
f_subagent: {
|
|
25237
|
+
type: "integer",
|
|
25238
|
+
notNull: true,
|
|
25239
|
+
default: 0
|
|
25240
|
+
},
|
|
25048
25241
|
f_input_tokens: {
|
|
25049
25242
|
type: "integer",
|
|
25050
25243
|
notNull: true,
|
|
@@ -25074,6 +25267,118 @@ const eventUsage = {
|
|
|
25074
25267
|
indexes: { idx_event_usage_created_at: { columns: ["f_created_at"] } }
|
|
25075
25268
|
};
|
|
25076
25269
|
//#endregion
|
|
25270
|
+
//#region ../session-rdb/src/entities/v3/session-usage.ts
|
|
25271
|
+
/**
|
|
25272
|
+
* 会话 × 本地日 × 模型的 token 汇总(派生统计表):**按会话的行**读它,因此保住
|
|
25273
|
+
* 「fork 子会话含继承前缀」的口径(各会话行之和 ≥ 总量)而不回连事件表。
|
|
25274
|
+
* 旁路累加、可按会话重建,读侧只读;取舍见 ADR-统计衍生表物化归属与汇总。
|
|
25275
|
+
*/
|
|
25276
|
+
const sessionUsage = {
|
|
25277
|
+
name: "t_session_usage",
|
|
25278
|
+
columns: {
|
|
25279
|
+
f_session_id: {
|
|
25280
|
+
type: "text",
|
|
25281
|
+
notNull: true,
|
|
25282
|
+
references: {
|
|
25283
|
+
table: "t_sessions",
|
|
25284
|
+
column: "f_session_id",
|
|
25285
|
+
onDelete: "cascade"
|
|
25286
|
+
}
|
|
25287
|
+
},
|
|
25288
|
+
f_day: {
|
|
25289
|
+
type: "text",
|
|
25290
|
+
notNull: true
|
|
25291
|
+
},
|
|
25292
|
+
f_provider: {
|
|
25293
|
+
type: "text",
|
|
25294
|
+
notNull: true,
|
|
25295
|
+
default: ""
|
|
25296
|
+
},
|
|
25297
|
+
f_model: {
|
|
25298
|
+
type: "text",
|
|
25299
|
+
notNull: true,
|
|
25300
|
+
default: ""
|
|
25301
|
+
},
|
|
25302
|
+
f_input_tokens: {
|
|
25303
|
+
type: "integer",
|
|
25304
|
+
notNull: true,
|
|
25305
|
+
default: 0
|
|
25306
|
+
},
|
|
25307
|
+
f_output_tokens: {
|
|
25308
|
+
type: "integer",
|
|
25309
|
+
notNull: true,
|
|
25310
|
+
default: 0
|
|
25311
|
+
},
|
|
25312
|
+
f_cache_read_tokens: {
|
|
25313
|
+
type: "integer",
|
|
25314
|
+
notNull: true,
|
|
25315
|
+
default: 0
|
|
25316
|
+
},
|
|
25317
|
+
f_reasoning_tokens: {
|
|
25318
|
+
type: "integer",
|
|
25319
|
+
notNull: true,
|
|
25320
|
+
default: 0
|
|
25321
|
+
},
|
|
25322
|
+
f_total_tokens: {
|
|
25323
|
+
type: "integer",
|
|
25324
|
+
notNull: true,
|
|
25325
|
+
default: 0
|
|
25326
|
+
}
|
|
25327
|
+
},
|
|
25328
|
+
uniques: { uq_session_usage_key: { columns: [
|
|
25329
|
+
"f_session_id",
|
|
25330
|
+
"f_day",
|
|
25331
|
+
"f_provider",
|
|
25332
|
+
"f_model"
|
|
25333
|
+
] } }
|
|
25334
|
+
};
|
|
25335
|
+
//#endregion
|
|
25336
|
+
//#region ../session-rdb/src/entities/v3/session-counts.ts
|
|
25337
|
+
/**
|
|
25338
|
+
* 会话 × 本地日的活动计数(派生统计表):轮次 / 步骤 / 用户输入 / 工具调用四列直接读,
|
|
25339
|
+
* 不再存事件类型、也不在读取时折。旁路累加、可按会话重建,读侧只读;
|
|
25340
|
+
* 取舍见 ADR-统计衍生表物化归属与汇总。
|
|
25341
|
+
*/
|
|
25342
|
+
const sessionCounts = {
|
|
25343
|
+
name: "t_session_counts",
|
|
25344
|
+
columns: {
|
|
25345
|
+
f_session_id: {
|
|
25346
|
+
type: "text",
|
|
25347
|
+
notNull: true,
|
|
25348
|
+
references: {
|
|
25349
|
+
table: "t_sessions",
|
|
25350
|
+
column: "f_session_id",
|
|
25351
|
+
onDelete: "cascade"
|
|
25352
|
+
}
|
|
25353
|
+
},
|
|
25354
|
+
f_day: {
|
|
25355
|
+
type: "text",
|
|
25356
|
+
notNull: true
|
|
25357
|
+
},
|
|
25358
|
+
f_turns: {
|
|
25359
|
+
type: "integer",
|
|
25360
|
+
notNull: true,
|
|
25361
|
+
default: 0
|
|
25362
|
+
},
|
|
25363
|
+
f_steps: {
|
|
25364
|
+
type: "integer",
|
|
25365
|
+
notNull: true,
|
|
25366
|
+
default: 0
|
|
25367
|
+
},
|
|
25368
|
+
f_user_inputs: {
|
|
25369
|
+
type: "integer",
|
|
25370
|
+
notNull: true,
|
|
25371
|
+
default: 0
|
|
25372
|
+
},
|
|
25373
|
+
f_tool_calls: {
|
|
25374
|
+
type: "integer",
|
|
25375
|
+
notNull: true,
|
|
25376
|
+
default: 0
|
|
25377
|
+
}
|
|
25378
|
+
},
|
|
25379
|
+
uniques: { uq_session_counts_key: { columns: ["f_session_id", "f_day"] } }
|
|
25380
|
+
};
|
|
25381
|
+
//#endregion
|
|
25077
25382
|
//#region ../session-rdb/src/entities/v3/index.ts
|
|
25078
25383
|
const sqliteTableDefs = [
|
|
25079
25384
|
persistenceState,
|
|
@@ -25086,7 +25391,9 @@ const sqliteTableDefs = [
|
|
|
25086
25391
|
workspaceSessions,
|
|
25087
25392
|
workspaceState,
|
|
25088
25393
|
sessionProjcacheRows,
|
|
25089
|
-
eventUsage
|
|
25394
|
+
eventUsage,
|
|
25395
|
+
sessionUsage,
|
|
25396
|
+
sessionCounts
|
|
25090
25397
|
];
|
|
25091
25398
|
const postgresTableDefs = [
|
|
25092
25399
|
persistenceState,
|
|
@@ -25099,7 +25406,9 @@ const postgresTableDefs = [
|
|
|
25099
25406
|
workspaceSessions,
|
|
25100
25407
|
workspaceState,
|
|
25101
25408
|
sessionProjcacheRows,
|
|
25102
|
-
eventUsage
|
|
25409
|
+
eventUsage,
|
|
25410
|
+
sessionUsage,
|
|
25411
|
+
sessionCounts
|
|
25103
25412
|
];
|
|
25104
25413
|
({ ...sessionEvents$1 }), { ...sessionEvents$1.columns };
|
|
25105
25414
|
//#endregion
|
|
@@ -25118,6 +25427,8 @@ const tWorkspaceSessions = sqliteTables["t_workspace_sessions"];
|
|
|
25118
25427
|
const tWorkspaceState = sqliteTables["t_workspace_state"];
|
|
25119
25428
|
const tSessionProjcacheRows = sqliteTables["t_session_projcache_row"];
|
|
25120
25429
|
const tEventUsage = sqliteTables["t_event_usage"];
|
|
25430
|
+
sqliteTables["t_session_usage"];
|
|
25431
|
+
sqliteTables["t_session_counts"];
|
|
25121
25432
|
const DEFAULT_BUSY_TIMEOUT_MS = 5e3;
|
|
25122
25433
|
function eventKind(event) {
|
|
25123
25434
|
switch (event.type) {
|
|
@@ -25352,209 +25663,63 @@ var NodeSQLiteDatabase = class extends SQLiteAsyncDatabase {
|
|
|
25352
25663
|
};
|
|
25353
25664
|
function construct(client, config = {}) {
|
|
25354
25665
|
const dialect = new SQLiteDialect({ useJitMappers: jitCompatCheck(config.jit) });
|
|
25355
|
-
let logger;
|
|
25356
|
-
if (config.logger === true) logger = new DefaultLogger();
|
|
25357
|
-
else if (config.logger !== false) logger = config.logger;
|
|
25358
|
-
const relations = config.relations ?? {};
|
|
25359
|
-
const db = new NodeSQLiteDatabase("sync", dialect, new NodeSQLiteSession(client, dialect, relations, { logger }), relations);
|
|
25360
|
-
db.$client = client;
|
|
25361
|
-
return db;
|
|
25362
|
-
}
|
|
25363
|
-
function drizzle(...params) {
|
|
25364
|
-
if (params[0] === void 0 || typeof params[0] === "string") return construct(params[0] === void 0 ? new DatabaseSync(":memory:") : new DatabaseSync(params[0]), params[1]);
|
|
25365
|
-
const { connection, client, ...config } = params[0];
|
|
25366
|
-
if (client) return construct(client, config);
|
|
25367
|
-
if (typeof connection === "object") {
|
|
25368
|
-
const { path, ...options } = connection;
|
|
25369
|
-
return construct(new DatabaseSync(path ?? ":memory:", options), config);
|
|
25370
|
-
}
|
|
25371
|
-
return construct(new DatabaseSync(connection ?? ":memory:"), config);
|
|
25372
|
-
}
|
|
25373
|
-
(function(_drizzle) {
|
|
25374
|
-
function mock(config) {
|
|
25375
|
-
return construct({}, config);
|
|
25376
|
-
}
|
|
25377
|
-
_drizzle.mock = mock;
|
|
25378
|
-
})(drizzle || (drizzle = {}));
|
|
25379
|
-
//#endregion
|
|
25380
|
-
//#region ../../../node_modules/.pnpm/drizzle-orm@1.0.0-rc.5-ab785fc_@opentelemetry+api@1.9.1_@types+pg@8.23.1_pg@8.23.0_typebox@1.3.7_zod@4.6.5/node_modules/drizzle-orm/migrator.js
|
|
25381
|
-
function readMigrationFiles(config) {
|
|
25382
|
-
if (fs.existsSync(`${config.migrationsFolder}/meta/_journal.json`)) throw Error("We detected that you have old drizzle-kit migration folders. You must upgrade drizzle-kit and run \"drizzle-kit up\"");
|
|
25383
|
-
const migrationFolderTo = config.migrationsFolder;
|
|
25384
|
-
const migrationQueries = [];
|
|
25385
|
-
const migrations = readdirSync(migrationFolderTo).map((subdir) => ({
|
|
25386
|
-
path: join(migrationFolderTo, subdir, "migration.sql"),
|
|
25387
|
-
name: subdir
|
|
25388
|
-
})).filter((it) => existsSync(it.path));
|
|
25389
|
-
migrations.sort((a, b) => a.name.localeCompare(b.name));
|
|
25390
|
-
for (const migration of migrations) {
|
|
25391
|
-
const migrationPath = migration.path;
|
|
25392
|
-
const migrationDate = migration.name.slice(0, 14);
|
|
25393
|
-
const query = fs.readFileSync(migrationPath).toString();
|
|
25394
|
-
const result = query.split("--> statement-breakpoint").map((it) => {
|
|
25395
|
-
return it;
|
|
25396
|
-
});
|
|
25397
|
-
const millis = formatToMillis(migrationDate);
|
|
25398
|
-
migrationQueries.push({
|
|
25399
|
-
sql: result,
|
|
25400
|
-
bps: true,
|
|
25401
|
-
folderMillis: millis,
|
|
25402
|
-
hash: crypto.createHash("sha256").update(query).digest("hex"),
|
|
25403
|
-
name: migration.name
|
|
25404
|
-
});
|
|
25405
|
-
}
|
|
25406
|
-
return migrationQueries;
|
|
25407
|
-
}
|
|
25408
|
-
//#endregion
|
|
25409
|
-
//#region ../../../node_modules/.pnpm/drizzle-orm@1.0.0-rc.5-ab785fc_@opentelemetry+api@1.9.1_@types+pg@8.23.1_pg@8.23.0_typebox@1.3.7_zod@4.6.5/node_modules/drizzle-orm/node-sqlite/migrator.js
|
|
25410
|
-
function migrate$1(db, config) {
|
|
25411
|
-
return migrateSync(readMigrationFiles(config), db.session, config);
|
|
25412
|
-
}
|
|
25413
|
-
//#endregion
|
|
25414
|
-
//#region ../session-rdb/src/usage.ts
|
|
25415
|
-
const SESSION_USAGE_PATH = "/api/session.usage";
|
|
25416
|
-
const DAY_MS = 864e5;
|
|
25417
|
-
const ROLLING_DAYS = {
|
|
25418
|
-
"7d": 7,
|
|
25419
|
-
"30d": 30,
|
|
25420
|
-
"90d": 90
|
|
25421
|
-
};
|
|
25422
|
-
function localDayStart(now) {
|
|
25423
|
-
const start = new Date(now);
|
|
25424
|
-
start.setHours(0, 0, 0, 0);
|
|
25425
|
-
return start;
|
|
25426
|
-
}
|
|
25427
|
-
/**
|
|
25428
|
-
* 范围起点(含),不限时为 undefined。自然日 / 自然周按 host 的本地时区算,周边界是周一 00:00。
|
|
25429
|
-
* @param range - 语义键。
|
|
25430
|
-
* @param now - 当前时刻。
|
|
25431
|
-
* @returns 起点毫秒时间戳,或 undefined。
|
|
25432
|
-
*/
|
|
25433
|
-
function resolveUsageSince(range, now) {
|
|
25434
|
-
switch (range) {
|
|
25435
|
-
case "all": return;
|
|
25436
|
-
case "day": return localDayStart(now).getTime();
|
|
25437
|
-
case "week": {
|
|
25438
|
-
const start = localDayStart(now);
|
|
25439
|
-
start.setDate(start.getDate() - (start.getDay() + 6) % 7);
|
|
25440
|
-
return start.getTime();
|
|
25441
|
-
}
|
|
25442
|
-
default: return now - ROLLING_DAYS[range] * DAY_MS;
|
|
25443
|
-
}
|
|
25444
|
-
}
|
|
25445
|
-
function parseUsageRange(value) {
|
|
25446
|
-
return value === "day" || value === "week" || value === "7d" || value === "30d" || value === "90d" ? value : "all";
|
|
25447
|
-
}
|
|
25448
|
-
/** 计入活动计数的事件类型(轮次 / 步骤 / 用户输入 / 工具调用)。 */
|
|
25449
|
-
const COUNTED_EVENT_TYPES = [
|
|
25450
|
-
"turn/start",
|
|
25451
|
-
"step/start",
|
|
25452
|
-
"user/message",
|
|
25453
|
-
"tool/call"
|
|
25454
|
-
];
|
|
25455
|
-
/** 按事件类型把计数累加到活动指标上(表里按类型存,读的时候折成四项)。 */
|
|
25456
|
-
function addActivityCount(target, type, count) {
|
|
25457
|
-
switch (type) {
|
|
25458
|
-
case "turn/start":
|
|
25459
|
-
target.turns += count;
|
|
25460
|
-
return target;
|
|
25461
|
-
case "step/start":
|
|
25462
|
-
target.steps += count;
|
|
25463
|
-
return target;
|
|
25464
|
-
case "user/message":
|
|
25465
|
-
target.userInputs += count;
|
|
25466
|
-
return target;
|
|
25467
|
-
case "tool/call":
|
|
25468
|
-
target.toolCalls += count;
|
|
25469
|
-
return target;
|
|
25470
|
-
default: return target;
|
|
25471
|
-
}
|
|
25472
|
-
}
|
|
25473
|
-
/** 毫秒时间戳 → host 本地日(`YYYY-MM-DD`,与 SQL 的 localtime 口径一致)。 */
|
|
25474
|
-
function localDayKey(ms) {
|
|
25475
|
-
const date = new Date(ms);
|
|
25476
|
-
const pad = (value) => String(value).padStart(2, "0");
|
|
25477
|
-
return `${String(date.getFullYear())}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`;
|
|
25478
|
-
}
|
|
25479
|
-
function emptyTotals() {
|
|
25480
|
-
return {
|
|
25481
|
-
turns: 0,
|
|
25482
|
-
steps: 0,
|
|
25483
|
-
userInputs: 0,
|
|
25484
|
-
toolCalls: 0,
|
|
25485
|
-
inputTokens: 0,
|
|
25486
|
-
outputTokens: 0,
|
|
25487
|
-
cacheReadTokens: 0,
|
|
25488
|
-
reasoningTokens: 0,
|
|
25489
|
-
totalTokens: 0
|
|
25490
|
-
};
|
|
25491
|
-
}
|
|
25492
|
-
function addTokenTotals(target, row) {
|
|
25493
|
-
target.inputTokens += row.inputTokens;
|
|
25494
|
-
target.outputTokens += row.outputTokens;
|
|
25495
|
-
target.cacheReadTokens += row.cacheReadTokens;
|
|
25496
|
-
target.reasoningTokens += row.reasoningTokens;
|
|
25497
|
-
target.totalTokens += row.totalTokens;
|
|
25498
|
-
return target;
|
|
25666
|
+
let logger;
|
|
25667
|
+
if (config.logger === true) logger = new DefaultLogger();
|
|
25668
|
+
else if (config.logger !== false) logger = config.logger;
|
|
25669
|
+
const relations = config.relations ?? {};
|
|
25670
|
+
const db = new NodeSQLiteDatabase("sync", dialect, new NodeSQLiteSession(client, dialect, relations, { logger }), relations);
|
|
25671
|
+
db.$client = client;
|
|
25672
|
+
return db;
|
|
25499
25673
|
}
|
|
25500
|
-
function
|
|
25501
|
-
|
|
25502
|
-
|
|
25503
|
-
|
|
25504
|
-
|
|
25505
|
-
|
|
25674
|
+
function drizzle(...params) {
|
|
25675
|
+
if (params[0] === void 0 || typeof params[0] === "string") return construct(params[0] === void 0 ? new DatabaseSync(":memory:") : new DatabaseSync(params[0]), params[1]);
|
|
25676
|
+
const { connection, client, ...config } = params[0];
|
|
25677
|
+
if (client) return construct(client, config);
|
|
25678
|
+
if (typeof connection === "object") {
|
|
25679
|
+
const { path, ...options } = connection;
|
|
25680
|
+
return construct(new DatabaseSync(path ?? ":memory:", options), config);
|
|
25681
|
+
}
|
|
25682
|
+
return construct(new DatabaseSync(connection ?? ":memory:"), config);
|
|
25506
25683
|
}
|
|
25507
|
-
|
|
25508
|
-
function
|
|
25509
|
-
|
|
25510
|
-
|
|
25511
|
-
|
|
25684
|
+
(function(_drizzle) {
|
|
25685
|
+
function mock(config) {
|
|
25686
|
+
return construct({}, config);
|
|
25687
|
+
}
|
|
25688
|
+
_drizzle.mock = mock;
|
|
25689
|
+
})(drizzle || (drizzle = {}));
|
|
25690
|
+
//#endregion
|
|
25691
|
+
//#region ../../../node_modules/.pnpm/drizzle-orm@1.0.0-rc.5-ab785fc_@opentelemetry+api@1.9.1_@types+pg@8.23.1_pg@8.23.0_typebox@1.3.7_zod@4.6.5/node_modules/drizzle-orm/migrator.js
|
|
25692
|
+
function readMigrationFiles(config) {
|
|
25693
|
+
if (fs.existsSync(`${config.migrationsFolder}/meta/_journal.json`)) throw Error("We detected that you have old drizzle-kit migration folders. You must upgrade drizzle-kit and run \"drizzle-kit up\"");
|
|
25694
|
+
const migrationFolderTo = config.migrationsFolder;
|
|
25695
|
+
const migrationQueries = [];
|
|
25696
|
+
const migrations = readdirSync(migrationFolderTo).map((subdir) => ({
|
|
25697
|
+
path: join(migrationFolderTo, subdir, "migration.sql"),
|
|
25698
|
+
name: subdir
|
|
25699
|
+
})).filter((it) => existsSync(it.path));
|
|
25700
|
+
migrations.sort((a, b) => a.name.localeCompare(b.name));
|
|
25701
|
+
for (const migration of migrations) {
|
|
25702
|
+
const migrationPath = migration.path;
|
|
25703
|
+
const migrationDate = migration.name.slice(0, 14);
|
|
25704
|
+
const query = fs.readFileSync(migrationPath).toString();
|
|
25705
|
+
const result = query.split("--> statement-breakpoint").map((it) => {
|
|
25706
|
+
return it;
|
|
25707
|
+
});
|
|
25708
|
+
const millis = formatToMillis(migrationDate);
|
|
25709
|
+
migrationQueries.push({
|
|
25710
|
+
sql: result,
|
|
25711
|
+
bps: true,
|
|
25712
|
+
folderMillis: millis,
|
|
25713
|
+
hash: crypto.createHash("sha256").update(query).digest("hex"),
|
|
25714
|
+
name: migration.name
|
|
25715
|
+
});
|
|
25716
|
+
}
|
|
25717
|
+
return migrationQueries;
|
|
25512
25718
|
}
|
|
25513
|
-
|
|
25514
|
-
|
|
25515
|
-
|
|
25516
|
-
|
|
25517
|
-
function registerSessionUsage(ctx, persistence) {
|
|
25518
|
-
ctx.inject(["webServer", "connection"], (webCtx) => {
|
|
25519
|
-
const webServer = webCtx.webServer;
|
|
25520
|
-
const connection = webCtx.get("connection");
|
|
25521
|
-
return webCtx.effect(() => webServer.register({
|
|
25522
|
-
kind: "exact",
|
|
25523
|
-
path: SESSION_USAGE_PATH,
|
|
25524
|
-
handler: async (req, res) => {
|
|
25525
|
-
const rejection = connection.requestRejection(req);
|
|
25526
|
-
if (rejection !== void 0) {
|
|
25527
|
-
res.writeHead(rejection);
|
|
25528
|
-
res.end(rejection === 401 ? "unauthorized" : "forbidden");
|
|
25529
|
-
return;
|
|
25530
|
-
}
|
|
25531
|
-
if (req.method !== "POST") {
|
|
25532
|
-
res.writeHead(405, { "content-type": "application/json" });
|
|
25533
|
-
res.end(JSON.stringify({ error: "method not allowed" }));
|
|
25534
|
-
return;
|
|
25535
|
-
}
|
|
25536
|
-
const chunks = [];
|
|
25537
|
-
for await (const chunk of req) chunks.push(chunk);
|
|
25538
|
-
const raw = Buffer.concat(chunks).toString("utf8");
|
|
25539
|
-
let envelope = {};
|
|
25540
|
-
if (raw !== "") try {
|
|
25541
|
-
envelope = JSON.parse(raw);
|
|
25542
|
-
} catch {
|
|
25543
|
-
res.writeHead(400, { "content-type": "application/json" });
|
|
25544
|
-
res.end(JSON.stringify({ error: "request body is not JSON" }));
|
|
25545
|
-
return;
|
|
25546
|
-
}
|
|
25547
|
-
try {
|
|
25548
|
-
const report = await persistence.usageReport(resolveUsageSince(parseUsageRange(envelope.range), Date.now()));
|
|
25549
|
-
res.writeHead(200, { "content-type": "application/json" });
|
|
25550
|
-
res.end(JSON.stringify(report));
|
|
25551
|
-
} catch (error) {
|
|
25552
|
-
res.writeHead(500, { "content-type": "application/json" });
|
|
25553
|
-
res.end(JSON.stringify({ error: error instanceof Error ? error.message : "usage report failed" }));
|
|
25554
|
-
}
|
|
25555
|
-
}
|
|
25556
|
-
}), `session-rdb: ${SESSION_USAGE_PATH} route`);
|
|
25557
|
-
});
|
|
25719
|
+
//#endregion
|
|
25720
|
+
//#region ../../../node_modules/.pnpm/drizzle-orm@1.0.0-rc.5-ab785fc_@opentelemetry+api@1.9.1_@types+pg@8.23.1_pg@8.23.0_typebox@1.3.7_zod@4.6.5/node_modules/drizzle-orm/node-sqlite/migrator.js
|
|
25721
|
+
function migrate$1(db, config) {
|
|
25722
|
+
return migrateSync(readMigrationFiles(config), db.session, config);
|
|
25558
25723
|
}
|
|
25559
25724
|
//#endregion
|
|
25560
25725
|
//#region ../session-rdb/src/storage-takeover/repository.ts
|
|
@@ -25815,45 +25980,44 @@ function createStorageRepository(host) {
|
|
|
25815
25980
|
}
|
|
25816
25981
|
//#endregion
|
|
25817
25982
|
//#region ../session-rdb/src/sqlite.ts
|
|
25818
|
-
/**
|
|
25819
|
-
|
|
25820
|
-
|
|
25821
|
-
|
|
25822
|
-
|
|
25823
|
-
|
|
25824
|
-
|
|
25825
|
-
|
|
25826
|
-
|
|
25827
|
-
|
|
25828
|
-
|
|
25829
|
-
CREATE INDEX IF NOT EXISTS idx_event_counts_day ON t_event_counts(f_day);`);
|
|
25830
|
-
}
|
|
25831
|
-
/** 一次性回填活动计数(表为空时从事件表重算;幂等)。 */
|
|
25832
|
-
function backfillEventCounts(db) {
|
|
25833
|
-
if (db.prepare("SELECT count(*) AS n FROM t_event_counts").get().n > 0) return;
|
|
25834
|
-
db.exec(`INSERT OR IGNORE INTO t_event_counts (f_session_id, f_day, f_type, f_count)
|
|
25835
|
-
SELECT b.f_session_id,
|
|
25836
|
-
date(e.f_created_at / 1000, 'unixepoch', 'localtime'),
|
|
25837
|
-
e.f_type,
|
|
25838
|
-
count(*)
|
|
25839
|
-
FROM t_session_events b
|
|
25840
|
-
JOIN t_events e ON e.f_event_id = b.f_event_id
|
|
25841
|
-
WHERE e.f_type IN (${COUNTED_EVENT_TYPE_SQL$1})
|
|
25842
|
-
GROUP BY 1, 2, 3`);
|
|
25983
|
+
/** 用量行的「被引用」物化表达式(`%EVENT%` 换成事件 id 列)。 */
|
|
25984
|
+
const EVENT_REFERENCED_SQL = `EXISTS (SELECT 1 FROM t_session_events rb
|
|
25985
|
+
WHERE rb.f_event_id = %EVENT%)`;
|
|
25986
|
+
/** 用量行的「被 subagent 会话引用」物化表达式(`%EVENT%` 换成事件 id 列)。 */
|
|
25987
|
+
const EVENT_SUBAGENT_SQL = `EXISTS (SELECT 1 FROM t_session_events sb
|
|
25988
|
+
JOIN t_sessions ss ON ss.f_session_id = sb.f_session_id
|
|
25989
|
+
WHERE sb.f_event_id = %EVENT% AND ss.f_origin = 'subagent')`;
|
|
25990
|
+
/** `IN (...)` 用的类型字面量(与 `COUNTED_EVENT_TYPES` 同源)。 */
|
|
25991
|
+
const COUNTED_EVENT_TYPE_SQL$1 = COUNTED_EVENT_TYPES.map((type) => `'${type}'`).join(", ");
|
|
25992
|
+
function withEvent(expression, column) {
|
|
25993
|
+
return expression.replace("%EVENT%", column);
|
|
25843
25994
|
}
|
|
25844
25995
|
/**
|
|
25845
|
-
*
|
|
25996
|
+
* 派生统计表的回填:任一表为空即按事件表全量重算(幂等;迁移删表重建后、手动清空后都靠它自愈)。
|
|
25997
|
+
* 顺序有依赖:用量行(带物化列)先落,两张会话汇总表再从它 / 事件表重算。
|
|
25846
25998
|
*/
|
|
25999
|
+
function backfillUsageTables(db) {
|
|
26000
|
+
backfillEventUsage(db);
|
|
26001
|
+
backfillSessionUsage(db);
|
|
26002
|
+
backfillSessionCounts(db);
|
|
26003
|
+
}
|
|
26004
|
+
function tableIsEmpty(db, table) {
|
|
26005
|
+
return db.prepare(`SELECT count(*) AS n FROM ${table}`).get().n === 0;
|
|
26006
|
+
}
|
|
26007
|
+
/** 用量行:一条 `assistant/message` 事件行一行,物化本地日与两个归属标记。 */
|
|
25847
26008
|
function backfillEventUsage(db) {
|
|
25848
|
-
if (db
|
|
26009
|
+
if (!tableIsEmpty(db, "t_event_usage")) return;
|
|
25849
26010
|
db.exec(`INSERT OR IGNORE INTO t_event_usage (
|
|
25850
|
-
f_event_id, f_created_at, f_provider, f_model,
|
|
26011
|
+
f_event_id, f_created_at, f_day, f_provider, f_model, f_referenced, f_subagent,
|
|
25851
26012
|
f_input_tokens, f_output_tokens, f_cache_read_tokens, f_reasoning_tokens, f_total_tokens)
|
|
25852
26013
|
SELECT e.f_event_id, e.f_created_at,
|
|
26014
|
+
date(e.f_created_at / 1000, 'unixepoch', 'localtime'),
|
|
25853
26015
|
coalesce(json_extract(e.f_data, '$.data.message.source.provider'),
|
|
25854
26016
|
json_extract(e.f_data, '$.message.source.provider')),
|
|
25855
26017
|
coalesce(json_extract(e.f_data, '$.data.message.source.model'),
|
|
25856
26018
|
json_extract(e.f_data, '$.message.source.model')),
|
|
26019
|
+
${withEvent(EVENT_REFERENCED_SQL, "e.f_event_id")},
|
|
26020
|
+
${withEvent(EVENT_SUBAGENT_SQL, "e.f_event_id")},
|
|
25857
26021
|
coalesce(json_extract(e.f_data, '$.data.usage.inputTokens'),
|
|
25858
26022
|
json_extract(e.f_data, '$.usage.inputTokens'), 0),
|
|
25859
26023
|
coalesce(json_extract(e.f_data, '$.data.usage.outputTokens'),
|
|
@@ -25869,6 +26033,35 @@ function backfillEventUsage(db) {
|
|
|
25869
26033
|
AND (json_extract(e.f_data, '$.data.usage') IS NOT NULL
|
|
25870
26034
|
OR json_extract(e.f_data, '$.usage') IS NOT NULL)`);
|
|
25871
26035
|
}
|
|
26036
|
+
/** 会话 × 本地日 × 模型的 token 汇总:从桥接行 × 用量行重算(含 fork 继承前缀)。 */
|
|
26037
|
+
function backfillSessionUsage(db) {
|
|
26038
|
+
if (!tableIsEmpty(db, "t_session_usage")) return;
|
|
26039
|
+
db.exec(`INSERT OR IGNORE INTO t_session_usage (
|
|
26040
|
+
f_session_id, f_day, f_provider, f_model,
|
|
26041
|
+
f_input_tokens, f_output_tokens, f_cache_read_tokens, f_reasoning_tokens, f_total_tokens)
|
|
26042
|
+
SELECT b.f_session_id, u.f_day, coalesce(u.f_provider, ''), coalesce(u.f_model, ''),
|
|
26043
|
+
sum(u.f_input_tokens), sum(u.f_output_tokens), sum(u.f_cache_read_tokens),
|
|
26044
|
+
sum(u.f_reasoning_tokens), sum(u.f_total_tokens)
|
|
26045
|
+
FROM t_session_events b
|
|
26046
|
+
JOIN t_event_usage u ON u.f_event_id = b.f_event_id
|
|
26047
|
+
GROUP BY b.f_session_id, u.f_day, coalesce(u.f_provider, ''), coalesce(u.f_model, '')`);
|
|
26048
|
+
}
|
|
26049
|
+
/** 会话 × 本地日的活动计数:从桥接行 × 事件类型重算。 */
|
|
26050
|
+
function backfillSessionCounts(db) {
|
|
26051
|
+
if (!tableIsEmpty(db, "t_session_counts")) return;
|
|
26052
|
+
db.exec(`INSERT OR IGNORE INTO t_session_counts (
|
|
26053
|
+
f_session_id, f_day, f_turns, f_steps, f_user_inputs, f_tool_calls)
|
|
26054
|
+
SELECT b.f_session_id,
|
|
26055
|
+
date(e.f_created_at / 1000, 'unixepoch', 'localtime'),
|
|
26056
|
+
sum(CASE WHEN e.f_type = 'turn/start' THEN 1 ELSE 0 END),
|
|
26057
|
+
sum(CASE WHEN e.f_type = 'step/start' THEN 1 ELSE 0 END),
|
|
26058
|
+
sum(CASE WHEN e.f_type = 'user/message' THEN 1 ELSE 0 END),
|
|
26059
|
+
sum(CASE WHEN e.f_type = 'tool/call' THEN 1 ELSE 0 END)
|
|
26060
|
+
FROM t_session_events b
|
|
26061
|
+
JOIN t_events e ON e.f_event_id = b.f_event_id
|
|
26062
|
+
WHERE e.f_type IN (${COUNTED_EVENT_TYPE_SQL$1})
|
|
26063
|
+
GROUP BY b.f_session_id, 2`);
|
|
26064
|
+
}
|
|
25872
26065
|
function sessionListRowOf(row) {
|
|
25873
26066
|
return {
|
|
25874
26067
|
sessionId: typeof row["session_id"] === "string" ? row["session_id"] : String(row["session_id"]),
|
|
@@ -25892,6 +26085,15 @@ function tokenTotalsOf$1(row) {
|
|
|
25892
26085
|
totalTokens: row.total_tokens ?? 0
|
|
25893
26086
|
};
|
|
25894
26087
|
}
|
|
26088
|
+
/** `t_session_counts` 的原始行(列名是 SQL 别名)折成四项活动计数。 */
|
|
26089
|
+
function activityTotalsOf$1(row) {
|
|
26090
|
+
return {
|
|
26091
|
+
turns: row.turns ?? 0,
|
|
26092
|
+
steps: row.steps ?? 0,
|
|
26093
|
+
userInputs: row.user_inputs ?? 0,
|
|
26094
|
+
toolCalls: row.tool_calls ?? 0
|
|
26095
|
+
};
|
|
26096
|
+
}
|
|
25895
26097
|
/** 一行的整套指标:token 用量 + 活动计数(轮次 / 步骤 / 用户输入 / 工具调用)。 */
|
|
25896
26098
|
function emptyActivity$1() {
|
|
25897
26099
|
return {
|
|
@@ -25901,17 +26103,10 @@ function emptyActivity$1() {
|
|
|
25901
26103
|
toolCalls: 0
|
|
25902
26104
|
};
|
|
25903
26105
|
}
|
|
25904
|
-
/** 有 token
|
|
26106
|
+
/** 有 token 用量的会话:活动计数与列表都限定在这一批会话里,口径一致(读汇总表,几百行)。 */
|
|
25905
26107
|
function hasUsageSession(column) {
|
|
25906
|
-
return `EXISTS (SELECT 1 FROM
|
|
25907
|
-
JOIN t_event_usage uu ON uu.f_event_id = ub.f_event_id
|
|
25908
|
-
WHERE ub.f_session_id = ${column})`;
|
|
26108
|
+
return `EXISTS (SELECT 1 FROM t_session_usage ub WHERE ub.f_session_id = ${column})`;
|
|
25909
26109
|
}
|
|
25910
|
-
const SUBAGENT_OF_EVENT = `EXISTS (SELECT 1 FROM t_session_events sb
|
|
25911
|
-
JOIN t_sessions ss ON ss.f_session_id = sb.f_session_id
|
|
25912
|
-
WHERE sb.f_event_id = %EVENT% AND ss.f_origin = 'subagent')`;
|
|
25913
|
-
/** `IN (...)` 用的类型字面量(与 `COUNTED_EVENT_TYPES` 同源)。 */
|
|
25914
|
-
const COUNTED_EVENT_TYPE_SQL$1 = COUNTED_EVENT_TYPES.map((type) => `'${type}'`).join(", ");
|
|
25915
26110
|
const sqliteMigrationsDir = fileURLToPath(new URL("../drizzle/sqlite/", import.meta.url));
|
|
25916
26111
|
const sqliteTxQueues = /* @__PURE__ */ new Map();
|
|
25917
26112
|
function enqueueSqliteTx(path, fn) {
|
|
@@ -25934,9 +26129,7 @@ async function openDatabase(path, journalMode, busyTimeout = DEFAULT_BUSY_TIMEOU
|
|
|
25934
26129
|
const { user_version: onDisk } = db.prepare("PRAGMA user_version").get();
|
|
25935
26130
|
if (onDisk === 2 && !hasMigrationsTable(db)) await baselineV2(db);
|
|
25936
26131
|
migrate$1(dbx, { migrationsFolder: sqliteMigrationsDir });
|
|
25937
|
-
|
|
25938
|
-
backfillEventCounts(db);
|
|
25939
|
-
backfillEventUsage(db);
|
|
26132
|
+
backfillUsageTables(db);
|
|
25940
26133
|
dbx.insert(tPersistenceState).values({
|
|
25941
26134
|
fSingleton: 1,
|
|
25942
26135
|
fStoreId: randomUUID()
|
|
@@ -26107,7 +26300,7 @@ var SqliteBackend = class SqliteBackend {
|
|
|
26107
26300
|
getHead: (id) => this.getHead(id),
|
|
26108
26301
|
getSeedLength: (id) => this.getSeedLength(id),
|
|
26109
26302
|
updateSeedLength: (id, seedLength) => this.updateSeedLength(id, seedLength),
|
|
26110
|
-
insertEvents: (events) => this.insertEvents(events),
|
|
26303
|
+
insertEvents: (events, usageRows) => this.insertEvents(events, usageRows),
|
|
26111
26304
|
insertBridges: (rows) => this.insertBridges(rows),
|
|
26112
26305
|
updateHead: (id, headEventId, headSequence) => this.updateHead(id, headEventId, headSequence),
|
|
26113
26306
|
bumpRevision: (id, lastEventAt) => this.bumpRevision(id, lastEventAt),
|
|
@@ -26140,14 +26333,10 @@ var SqliteBackend = class SqliteBackend {
|
|
|
26140
26333
|
this.db.update(tSessions).set({ fSeedLength: seedLength }).where(eq(tSessions.fSessionId, id)).run();
|
|
26141
26334
|
}
|
|
26142
26335
|
static INSERT_BATCH_ROWS = 1e3;
|
|
26143
|
-
async insertEvents(events) {
|
|
26336
|
+
async insertEvents(events, usageRows) {
|
|
26144
26337
|
if (events.length === 0) return;
|
|
26145
26338
|
for (let i = 0; i < events.length; i += SqliteBackend.INSERT_BATCH_ROWS) this.db.insert(tEvents).values(events.slice(i, i + SqliteBackend.INSERT_BATCH_ROWS).map((event) => ({ ...event }))).run();
|
|
26146
|
-
|
|
26147
|
-
const row = usageRowOf(event);
|
|
26148
|
-
return row === void 0 ? [] : [row];
|
|
26149
|
-
});
|
|
26150
|
-
if (usage.length > 0) this.db.insert(tEventUsage).values(usage).onConflictDoNothing().run();
|
|
26339
|
+
if (usageRows.length > 0) this.db.insert(tEventUsage).values(usageRows.map((row) => ({ ...row }))).onConflictDoNothing().run();
|
|
26151
26340
|
}
|
|
26152
26341
|
async insertBridges(rows) {
|
|
26153
26342
|
if (rows.length === 0) return;
|
|
@@ -26257,41 +26446,49 @@ var SqliteBackend = class SqliteBackend {
|
|
|
26257
26446
|
total: Number(counted.n)
|
|
26258
26447
|
};
|
|
26259
26448
|
}
|
|
26449
|
+
/**
|
|
26450
|
+
* 用量聚合:只读三张派生统计表——事件级用量行给去重口径的总量与「天 × 模型」桶,
|
|
26451
|
+
* `t_session_usage` / `t_session_counts` 给按会话的行(含 fork 继承前缀)。
|
|
26452
|
+
* 归属、本地日与「是否被引用」都是物化列,这里不再现算、也不回连事件表。
|
|
26453
|
+
* 事件级按毫秒时间戳过滤、汇总表按本地日过滤——`sinceMs` 对齐到本地零点时两者等价。
|
|
26454
|
+
*/
|
|
26260
26455
|
async usageReport(sinceMs) {
|
|
26261
|
-
const usageSince = sinceMs === void 0 ? "" : " AND
|
|
26456
|
+
const usageSince = sinceMs === void 0 ? "" : " AND f_created_at >= ?";
|
|
26262
26457
|
const params = sinceMs === void 0 ? [] : [sinceMs];
|
|
26263
|
-
const daySince = sinceMs === void 0 ? "" : " AND
|
|
26458
|
+
const daySince = sinceMs === void 0 ? "" : " AND %COL% >= ?";
|
|
26264
26459
|
const dayParams = sinceMs === void 0 ? [] : [localDayKey(sinceMs)];
|
|
26265
|
-
const
|
|
26266
|
-
const buckets = this.db.$client.prepare(`SELECT
|
|
26267
|
-
|
|
26268
|
-
|
|
26269
|
-
|
|
26270
|
-
sum(
|
|
26271
|
-
sum(
|
|
26272
|
-
sum(
|
|
26273
|
-
sum(
|
|
26274
|
-
sum(
|
|
26275
|
-
FROM t_event_usage
|
|
26276
|
-
WHERE
|
|
26277
|
-
GROUP BY
|
|
26278
|
-
const tokenScope = this.db.$client.prepare(`SELECT
|
|
26279
|
-
sum(
|
|
26280
|
-
sum(
|
|
26281
|
-
sum(
|
|
26282
|
-
sum(
|
|
26283
|
-
sum(
|
|
26284
|
-
FROM t_event_usage
|
|
26285
|
-
WHERE
|
|
26286
|
-
GROUP BY
|
|
26460
|
+
const dayFilter = (column) => daySince.replace("%COL%", column);
|
|
26461
|
+
const buckets = this.db.$client.prepare(`SELECT f_day AS day,
|
|
26462
|
+
f_provider AS provider,
|
|
26463
|
+
f_model AS model,
|
|
26464
|
+
f_subagent AS subagent,
|
|
26465
|
+
sum(f_input_tokens) AS input_tokens,
|
|
26466
|
+
sum(f_output_tokens) AS output_tokens,
|
|
26467
|
+
sum(f_cache_read_tokens) AS cache_read_tokens,
|
|
26468
|
+
sum(f_reasoning_tokens) AS reasoning_tokens,
|
|
26469
|
+
sum(f_total_tokens) AS total_tokens
|
|
26470
|
+
FROM t_event_usage
|
|
26471
|
+
WHERE f_referenced = 1${usageSince}
|
|
26472
|
+
GROUP BY f_day, f_provider, f_model, f_subagent`).all(...params);
|
|
26473
|
+
const tokenScope = this.db.$client.prepare(`SELECT f_subagent AS subagent,
|
|
26474
|
+
sum(f_input_tokens) AS input_tokens,
|
|
26475
|
+
sum(f_output_tokens) AS output_tokens,
|
|
26476
|
+
sum(f_cache_read_tokens) AS cache_read_tokens,
|
|
26477
|
+
sum(f_reasoning_tokens) AS reasoning_tokens,
|
|
26478
|
+
sum(f_total_tokens) AS total_tokens
|
|
26479
|
+
FROM t_event_usage
|
|
26480
|
+
WHERE f_referenced = 1${usageSince}
|
|
26481
|
+
GROUP BY f_subagent`).all(...params);
|
|
26287
26482
|
const activityScope = this.db.$client.prepare(`SELECT (s.f_origin = 'subagent') AS subagent,
|
|
26288
|
-
c.
|
|
26289
|
-
sum(c.
|
|
26290
|
-
|
|
26483
|
+
sum(c.f_turns) AS turns,
|
|
26484
|
+
sum(c.f_steps) AS steps,
|
|
26485
|
+
sum(c.f_user_inputs) AS user_inputs,
|
|
26486
|
+
sum(c.f_tool_calls) AS tool_calls
|
|
26487
|
+
FROM t_session_counts c
|
|
26291
26488
|
JOIN t_sessions s ON s.f_session_id = c.f_session_id
|
|
26292
|
-
WHERE ${hasUsageSession("c.f_session_id")}${
|
|
26293
|
-
GROUP BY subagent
|
|
26294
|
-
const tokenSessions = this.db.$client.prepare(`SELECT
|
|
26489
|
+
WHERE ${hasUsageSession("c.f_session_id")}${dayFilter("c.f_day")}
|
|
26490
|
+
GROUP BY subagent`).all(...dayParams);
|
|
26491
|
+
const tokenSessions = this.db.$client.prepare(`SELECT u.f_session_id AS session_id,
|
|
26295
26492
|
s.f_title AS title,
|
|
26296
26493
|
(s.f_origin = 'subagent') AS subagent,
|
|
26297
26494
|
(s.f_archived_at IS NOT NULL) AS archived,
|
|
@@ -26300,28 +26497,25 @@ var SqliteBackend = class SqliteBackend {
|
|
|
26300
26497
|
sum(u.f_cache_read_tokens) AS cache_read_tokens,
|
|
26301
26498
|
sum(u.f_reasoning_tokens) AS reasoning_tokens,
|
|
26302
26499
|
sum(u.f_total_tokens) AS total_tokens
|
|
26303
|
-
FROM
|
|
26304
|
-
JOIN
|
|
26305
|
-
|
|
26306
|
-
|
|
26307
|
-
|
|
26308
|
-
|
|
26309
|
-
|
|
26310
|
-
sum(
|
|
26311
|
-
|
|
26312
|
-
|
|
26313
|
-
|
|
26500
|
+
FROM t_session_usage u
|
|
26501
|
+
JOIN t_sessions s ON s.f_session_id = u.f_session_id
|
|
26502
|
+
WHERE 1 = 1${dayFilter("u.f_day")}
|
|
26503
|
+
GROUP BY u.f_session_id, s.f_title, s.f_origin, s.f_archived_at`).all(...dayParams);
|
|
26504
|
+
const activitySessions = this.db.$client.prepare(`SELECT f_session_id AS session_id,
|
|
26505
|
+
sum(f_turns) AS turns,
|
|
26506
|
+
sum(f_steps) AS steps,
|
|
26507
|
+
sum(f_user_inputs) AS user_inputs,
|
|
26508
|
+
sum(f_tool_calls) AS tool_calls
|
|
26509
|
+
FROM t_session_counts
|
|
26510
|
+
WHERE 1 = 1${dayFilter("f_day")}
|
|
26511
|
+
GROUP BY f_session_id`).all(...dayParams);
|
|
26314
26512
|
const human = emptyTotals();
|
|
26315
26513
|
const subagent = emptyTotals();
|
|
26316
26514
|
for (const row of tokenScope) addTokenTotals(row.subagent === 1 ? subagent : human, tokenTotalsOf$1(row));
|
|
26317
|
-
for (const row of activityScope)
|
|
26515
|
+
for (const row of activityScope) addActivityTotals(row.subagent === 1 ? subagent : human, activityTotalsOf$1(row));
|
|
26318
26516
|
const totals = addTotals(addTotals(emptyTotals(), human), subagent);
|
|
26319
26517
|
const activityBySession = /* @__PURE__ */ new Map();
|
|
26320
|
-
for (const row of activitySessions)
|
|
26321
|
-
const target = activityBySession.get(row.session_id) ?? emptyActivity$1();
|
|
26322
|
-
addActivityCount(target, row.f_type, row.n ?? 0);
|
|
26323
|
-
activityBySession.set(row.session_id, target);
|
|
26324
|
-
}
|
|
26518
|
+
for (const row of activitySessions) activityBySession.set(row.session_id, activityTotalsOf$1(row));
|
|
26325
26519
|
return {
|
|
26326
26520
|
totals,
|
|
26327
26521
|
subagent,
|
|
@@ -26343,28 +26537,72 @@ var SqliteBackend = class SqliteBackend {
|
|
|
26343
26537
|
}))
|
|
26344
26538
|
};
|
|
26345
26539
|
}
|
|
26346
|
-
/**
|
|
26347
|
-
async
|
|
26540
|
+
/** token 用量旁路累加:派生表 `t_session_usage`,不在写事务里(失败可丢,表可销毁重建)。 */
|
|
26541
|
+
async incrementSessionUsage(id, buckets) {
|
|
26542
|
+
if (buckets.length === 0) return;
|
|
26543
|
+
const statement = this.db.$client.prepare(`INSERT INTO t_session_usage (f_session_id, f_day, f_provider, f_model,
|
|
26544
|
+
f_input_tokens, f_output_tokens, f_cache_read_tokens, f_reasoning_tokens, f_total_tokens)
|
|
26545
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
26546
|
+
ON CONFLICT(f_session_id, f_day, f_provider, f_model) DO UPDATE SET
|
|
26547
|
+
f_input_tokens = f_input_tokens + excluded.f_input_tokens,
|
|
26548
|
+
f_output_tokens = f_output_tokens + excluded.f_output_tokens,
|
|
26549
|
+
f_cache_read_tokens = f_cache_read_tokens + excluded.f_cache_read_tokens,
|
|
26550
|
+
f_reasoning_tokens = f_reasoning_tokens + excluded.f_reasoning_tokens,
|
|
26551
|
+
f_total_tokens = f_total_tokens + excluded.f_total_tokens`);
|
|
26552
|
+
for (const bucket of buckets) statement.run(id, bucket.day, bucket.provider ?? "", bucket.model ?? "", bucket.inputTokens, bucket.outputTokens, bucket.cacheReadTokens, bucket.reasoningTokens, bucket.totalTokens);
|
|
26553
|
+
}
|
|
26554
|
+
/** 活动计数旁路累加:派生表 `t_session_counts`,同上(四项计数一次落)。 */
|
|
26555
|
+
async incrementSessionCounts(id, buckets) {
|
|
26348
26556
|
if (buckets.length === 0) return;
|
|
26349
|
-
const statement = this.db.$client.prepare(`INSERT INTO
|
|
26350
|
-
|
|
26351
|
-
|
|
26352
|
-
|
|
26353
|
-
|
|
26354
|
-
|
|
26355
|
-
|
|
26356
|
-
|
|
26557
|
+
const statement = this.db.$client.prepare(`INSERT INTO t_session_counts (f_session_id, f_day, f_turns, f_steps, f_user_inputs, f_tool_calls)
|
|
26558
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
26559
|
+
ON CONFLICT(f_session_id, f_day) DO UPDATE SET
|
|
26560
|
+
f_turns = f_turns + excluded.f_turns,
|
|
26561
|
+
f_steps = f_steps + excluded.f_steps,
|
|
26562
|
+
f_user_inputs = f_user_inputs + excluded.f_user_inputs,
|
|
26563
|
+
f_tool_calls = f_tool_calls + excluded.f_tool_calls`);
|
|
26564
|
+
for (const bucket of buckets) statement.run(id, bucket.day, bucket.turns, bucket.steps, bucket.userInputs, bucket.toolCalls);
|
|
26565
|
+
}
|
|
26566
|
+
/** 按会话重算两张汇总表(rewind / fork 之后):先删该会话的行,再从事件表重算。 */
|
|
26567
|
+
async rebuildSessionStats(id) {
|
|
26568
|
+
this.db.$client.prepare("DELETE FROM t_session_usage WHERE f_session_id = ?").run(id);
|
|
26569
|
+
this.db.$client.prepare(`INSERT INTO t_session_usage (
|
|
26570
|
+
f_session_id, f_day, f_provider, f_model,
|
|
26571
|
+
f_input_tokens, f_output_tokens, f_cache_read_tokens, f_reasoning_tokens, f_total_tokens)
|
|
26572
|
+
SELECT b.f_session_id, u.f_day, coalesce(u.f_provider, ''), coalesce(u.f_model, ''),
|
|
26573
|
+
sum(u.f_input_tokens), sum(u.f_output_tokens), sum(u.f_cache_read_tokens),
|
|
26574
|
+
sum(u.f_reasoning_tokens), sum(u.f_total_tokens)
|
|
26575
|
+
FROM t_session_events b
|
|
26576
|
+
JOIN t_event_usage u ON u.f_event_id = b.f_event_id
|
|
26577
|
+
WHERE b.f_session_id = ?
|
|
26578
|
+
GROUP BY b.f_session_id, u.f_day, coalesce(u.f_provider, ''), coalesce(u.f_model, '')`).run(id);
|
|
26579
|
+
this.db.$client.prepare("DELETE FROM t_session_counts WHERE f_session_id = ?").run(id);
|
|
26580
|
+
this.db.$client.prepare(`INSERT INTO t_session_counts (
|
|
26581
|
+
f_session_id, f_day, f_turns, f_steps, f_user_inputs, f_tool_calls)
|
|
26357
26582
|
SELECT b.f_session_id,
|
|
26358
26583
|
date(e.f_created_at / 1000, 'unixepoch', 'localtime'),
|
|
26359
|
-
e.f_type,
|
|
26360
|
-
|
|
26584
|
+
sum(CASE WHEN e.f_type = 'turn/start' THEN 1 ELSE 0 END),
|
|
26585
|
+
sum(CASE WHEN e.f_type = 'step/start' THEN 1 ELSE 0 END),
|
|
26586
|
+
sum(CASE WHEN e.f_type = 'user/message' THEN 1 ELSE 0 END),
|
|
26587
|
+
sum(CASE WHEN e.f_type = 'tool/call' THEN 1 ELSE 0 END)
|
|
26361
26588
|
FROM t_session_events b
|
|
26362
26589
|
JOIN t_events e ON e.f_event_id = b.f_event_id
|
|
26363
26590
|
WHERE b.f_session_id = ? AND e.f_type IN (${COUNTED_EVENT_TYPE_SQL$1})
|
|
26364
|
-
GROUP BY
|
|
26591
|
+
GROUP BY b.f_session_id, 2`).run(id);
|
|
26365
26592
|
}
|
|
26366
|
-
|
|
26367
|
-
|
|
26593
|
+
/**
|
|
26594
|
+
* 全量重算用量行的引用标记:rewind 截断 / fork 复用 / 会话删除都会让「引用」跨会话变化,
|
|
26595
|
+
* 只按本会话判定不够。低频路径,一次全表 UPDATE(3 万行量级)。
|
|
26596
|
+
*/
|
|
26597
|
+
async refreshEventUsageFlags() {
|
|
26598
|
+
const eventId = "t_event_usage.f_event_id";
|
|
26599
|
+
this.db.$client.exec(`UPDATE t_event_usage SET
|
|
26600
|
+
f_referenced = ${withEvent(EVENT_REFERENCED_SQL, eventId)},
|
|
26601
|
+
f_subagent = ${withEvent(EVENT_SUBAGENT_SQL, eventId)}`);
|
|
26602
|
+
}
|
|
26603
|
+
async deleteSessionStats(id) {
|
|
26604
|
+
this.db.$client.prepare("DELETE FROM t_session_usage WHERE f_session_id = ?").run(id);
|
|
26605
|
+
this.db.$client.prepare("DELETE FROM t_session_counts WHERE f_session_id = ?").run(id);
|
|
26368
26606
|
}
|
|
26369
26607
|
async vacuum() {
|
|
26370
26608
|
await enqueueSqliteTx(this.dbPath, async () => {
|
|
@@ -26428,6 +26666,15 @@ function emptyActivity() {
|
|
|
26428
26666
|
toolCalls: 0
|
|
26429
26667
|
};
|
|
26430
26668
|
}
|
|
26669
|
+
/** `t_session_counts` 的四项计数折成活动计数(pg 的 sum 回落成字符串)。 */
|
|
26670
|
+
function activityTotalsOf(row) {
|
|
26671
|
+
return {
|
|
26672
|
+
turns: numeric(row["turns"]),
|
|
26673
|
+
steps: numeric(row["steps"]),
|
|
26674
|
+
userInputs: numeric(row["user_inputs"]),
|
|
26675
|
+
toolCalls: numeric(row["tool_calls"])
|
|
26676
|
+
};
|
|
26677
|
+
}
|
|
26431
26678
|
/** `IN (...)` 用的类型字面量(与 `COUNTED_EVENT_TYPES` 同源)。 */
|
|
26432
26679
|
const COUNTED_EVENT_TYPE_SQL = COUNTED_EVENT_TYPES.map((type) => `'${type}'`).join(", ");
|
|
26433
26680
|
var PostgresBackend = class PostgresBackend {
|
|
@@ -26481,9 +26728,7 @@ var PostgresBackend = class PostgresBackend {
|
|
|
26481
26728
|
if (await this.readMeta(this.db, "schema_version") === "2") await this.baselineV2();
|
|
26482
26729
|
}
|
|
26483
26730
|
await migrate(this.db, { migrationsFolder: postgresMigrationsDir });
|
|
26484
|
-
await this.
|
|
26485
|
-
await this.backfillEventCounts();
|
|
26486
|
-
await this.backfillEventUsage();
|
|
26731
|
+
await this.backfillUsageTables();
|
|
26487
26732
|
const storeId = await this.db.transaction(async (tx) => {
|
|
26488
26733
|
await tx.insert(this.tables["t_persistence_state"]).values({
|
|
26489
26734
|
fSingleton: 1,
|
|
@@ -26544,7 +26789,7 @@ var PostgresBackend = class PostgresBackend {
|
|
|
26544
26789
|
getHead: (id) => this.getHead(tx, id),
|
|
26545
26790
|
getSeedLength: (id) => this.getSeedLength(tx, id),
|
|
26546
26791
|
updateSeedLength: (id, seedLength) => this.updateSeedLength(tx, id, seedLength),
|
|
26547
|
-
insertEvents: (events) => this.insertEvents(tx, events),
|
|
26792
|
+
insertEvents: (events, usageRows) => this.insertEvents(tx, events, usageRows),
|
|
26548
26793
|
insertBridges: (rows) => this.insertBridges(tx, rows),
|
|
26549
26794
|
updateHead: (id, headEventId, headSequence) => this.updateHead(tx, id, headEventId, headSequence),
|
|
26550
26795
|
bumpRevision: (id, lastEventAt) => this.bumpRevision(tx, id, lastEventAt),
|
|
@@ -26581,14 +26826,10 @@ var PostgresBackend = class PostgresBackend {
|
|
|
26581
26826
|
await exec.update(this.tables["t_sessions"]).set({ fSeedLength: seedLength }).where(eq(this.tables["t_sessions"].fSessionId, id)).execute();
|
|
26582
26827
|
}
|
|
26583
26828
|
static INSERT_BATCH_ROWS = 1e3;
|
|
26584
|
-
async insertEvents(exec, events) {
|
|
26829
|
+
async insertEvents(exec, events, usageRows) {
|
|
26585
26830
|
if (events.length === 0) return;
|
|
26586
26831
|
for (let i = 0; i < events.length; i += PostgresBackend.INSERT_BATCH_ROWS) await exec.insert(this.tables["t_events"]).values(events.slice(i, i + PostgresBackend.INSERT_BATCH_ROWS).map((event) => ({ ...event }))).execute();
|
|
26587
|
-
|
|
26588
|
-
const row = usageRowOf(event);
|
|
26589
|
-
return row === void 0 ? [] : [row];
|
|
26590
|
-
});
|
|
26591
|
-
if (usage.length > 0) await exec.insert(this.tables["t_event_usage"]).values(usage).onConflictDoNothing().execute();
|
|
26832
|
+
if (usageRows.length > 0) for (let i = 0; i < usageRows.length; i += PostgresBackend.INSERT_BATCH_ROWS) await exec.insert(this.tables["t_event_usage"]).values(usageRows.slice(i, i + PostgresBackend.INSERT_BATCH_ROWS).map((row) => ({ ...row }))).onConflictDoNothing().execute();
|
|
26592
26833
|
}
|
|
26593
26834
|
async insertBridges(exec, rows) {
|
|
26594
26835
|
if (rows.length === 0) return;
|
|
@@ -26658,21 +26899,41 @@ var PostgresBackend = class PostgresBackend {
|
|
|
26658
26899
|
async vacuum() {
|
|
26659
26900
|
await this.db.execute(sql`VACUUM ANALYZE`);
|
|
26660
26901
|
}
|
|
26661
|
-
/**
|
|
26902
|
+
/** 派生统计表的回填:任一表为空即按事件表全量重算(幂等,迁移删表重建后自愈)。 */
|
|
26903
|
+
async backfillUsageTables() {
|
|
26904
|
+
await this.backfillEventUsage();
|
|
26905
|
+
await this.backfillSessionUsage();
|
|
26906
|
+
await this.backfillSessionCounts();
|
|
26907
|
+
}
|
|
26908
|
+
async tableIsEmpty(table) {
|
|
26909
|
+
const existing = await this.db.execute(sql.raw(`SELECT count(*) AS n FROM ${table}`));
|
|
26910
|
+
return Number(existing.rows[0]?.["n"] ?? 0) === 0;
|
|
26911
|
+
}
|
|
26912
|
+
/**
|
|
26913
|
+
* 用量行:一条 `assistant/message` 事件行一行,物化本地日与两个归属标记。
|
|
26914
|
+
* 本地日按数据库会话时区渲染(部署前提是它与 host 时区一致,`localDayKey` 同口径)。
|
|
26915
|
+
*/
|
|
26662
26916
|
async backfillEventUsage() {
|
|
26917
|
+
if (!await this.tableIsEmpty(this.qualifiedTable("t_event_usage"))) return;
|
|
26663
26918
|
const tEventUsage = this.tables["t_event_usage"];
|
|
26664
26919
|
const tEvents = this.tables["t_events"];
|
|
26665
|
-
const
|
|
26666
|
-
|
|
26920
|
+
const tSessionEvents = this.tables["t_session_events"];
|
|
26921
|
+
const tSessions = this.tables["t_sessions"];
|
|
26667
26922
|
await this.db.execute(sql`
|
|
26668
26923
|
INSERT INTO ${tEventUsage} (
|
|
26669
|
-
f_event_id, f_created_at, f_provider, f_model,
|
|
26924
|
+
f_event_id, f_created_at, f_day, f_provider, f_model, f_referenced, f_subagent,
|
|
26670
26925
|
f_input_tokens, f_output_tokens, f_cache_read_tokens, f_reasoning_tokens, f_total_tokens)
|
|
26671
26926
|
SELECT e.f_event_id, e.f_created_at,
|
|
26927
|
+
to_char(to_timestamp(e.f_created_at / 1000.0), 'YYYY-MM-DD'),
|
|
26672
26928
|
coalesce(e.f_data::json #>> '{data,message,source,provider}',
|
|
26673
26929
|
e.f_data::json #>> '{message,source,provider}'),
|
|
26674
26930
|
coalesce(e.f_data::json #>> '{data,message,source,model}',
|
|
26675
26931
|
e.f_data::json #>> '{message,source,model}'),
|
|
26932
|
+
(EXISTS (SELECT 1 FROM ${tSessionEvents} rb
|
|
26933
|
+
WHERE rb.f_event_id = e.f_event_id))::int,
|
|
26934
|
+
(EXISTS (SELECT 1 FROM ${tSessionEvents} sb
|
|
26935
|
+
JOIN ${tSessions} ss ON ss.f_session_id = sb.f_session_id
|
|
26936
|
+
WHERE sb.f_event_id = e.f_event_id AND ss.f_origin = 'subagent'))::int,
|
|
26676
26937
|
coalesce((e.f_data::json #>> '{data,usage,inputTokens}')::integer,
|
|
26677
26938
|
(e.f_data::json #>> '{usage,inputTokens}')::integer, 0),
|
|
26678
26939
|
coalesce((e.f_data::json #>> '{data,usage,outputTokens}')::integer,
|
|
@@ -26688,6 +26949,47 @@ var PostgresBackend = class PostgresBackend {
|
|
|
26688
26949
|
AND (e.f_data::json #> '{data,usage}' IS NOT NULL
|
|
26689
26950
|
OR e.f_data::json #> '{usage}' IS NOT NULL)
|
|
26690
26951
|
ON CONFLICT DO NOTHING
|
|
26952
|
+
`);
|
|
26953
|
+
}
|
|
26954
|
+
/** 会话 × 本地日 × 模型的 token 汇总:从桥接行 × 用量行重算(含 fork 继承前缀)。 */
|
|
26955
|
+
async backfillSessionUsage() {
|
|
26956
|
+
if (!await this.tableIsEmpty(this.qualifiedTable("t_session_usage"))) return;
|
|
26957
|
+
const tSessionUsage = this.tables["t_session_usage"];
|
|
26958
|
+
const tSessionEvents = this.tables["t_session_events"];
|
|
26959
|
+
const tEventUsage = this.tables["t_event_usage"];
|
|
26960
|
+
await this.db.execute(sql`
|
|
26961
|
+
INSERT INTO ${tSessionUsage} (
|
|
26962
|
+
f_session_id, f_day, f_provider, f_model,
|
|
26963
|
+
f_input_tokens, f_output_tokens, f_cache_read_tokens, f_reasoning_tokens, f_total_tokens)
|
|
26964
|
+
SELECT b.f_session_id, u.f_day, coalesce(u.f_provider, ''), coalesce(u.f_model, ''),
|
|
26965
|
+
sum(u.f_input_tokens), sum(u.f_output_tokens), sum(u.f_cache_read_tokens),
|
|
26966
|
+
sum(u.f_reasoning_tokens), sum(u.f_total_tokens)
|
|
26967
|
+
FROM ${tSessionEvents} b
|
|
26968
|
+
JOIN ${tEventUsage} u ON u.f_event_id = b.f_event_id
|
|
26969
|
+
GROUP BY 1, 2, 3, 4
|
|
26970
|
+
ON CONFLICT DO NOTHING
|
|
26971
|
+
`);
|
|
26972
|
+
}
|
|
26973
|
+
/** 会话 × 本地日的活动计数:从桥接行 × 事件类型重算。 */
|
|
26974
|
+
async backfillSessionCounts() {
|
|
26975
|
+
if (!await this.tableIsEmpty(this.qualifiedTable("t_session_counts"))) return;
|
|
26976
|
+
const tSessionCounts = this.tables["t_session_counts"];
|
|
26977
|
+
const tSessionEvents = this.tables["t_session_events"];
|
|
26978
|
+
const tEvents = this.tables["t_events"];
|
|
26979
|
+
await this.db.execute(sql`
|
|
26980
|
+
INSERT INTO ${tSessionCounts} (
|
|
26981
|
+
f_session_id, f_day, f_turns, f_steps, f_user_inputs, f_tool_calls)
|
|
26982
|
+
SELECT b.f_session_id,
|
|
26983
|
+
to_char(to_timestamp(e.f_created_at / 1000.0), 'YYYY-MM-DD'),
|
|
26984
|
+
sum(CASE WHEN e.f_type = 'turn/start' THEN 1 ELSE 0 END),
|
|
26985
|
+
sum(CASE WHEN e.f_type = 'step/start' THEN 1 ELSE 0 END),
|
|
26986
|
+
sum(CASE WHEN e.f_type = 'user/message' THEN 1 ELSE 0 END),
|
|
26987
|
+
sum(CASE WHEN e.f_type = 'tool/call' THEN 1 ELSE 0 END)
|
|
26988
|
+
FROM ${tSessionEvents} b
|
|
26989
|
+
JOIN ${tEvents} e ON e.f_event_id = b.f_event_id
|
|
26990
|
+
WHERE e.f_type IN (${sql.raw(COUNTED_EVENT_TYPE_SQL)})
|
|
26991
|
+
GROUP BY 1, 2
|
|
26992
|
+
ON CONFLICT DO NOTHING
|
|
26691
26993
|
`);
|
|
26692
26994
|
}
|
|
26693
26995
|
/** 用量聚合:与 SQLite 侧同形,读 `t_event_usage`,数值列回来是字符串。 */
|
|
@@ -26740,85 +27042,78 @@ var PostgresBackend = class PostgresBackend {
|
|
|
26740
27042
|
total: numeric(counted.rows[0]?.["n"] ?? 0)
|
|
26741
27043
|
};
|
|
26742
27044
|
}
|
|
27045
|
+
/**
|
|
27046
|
+
* 用量聚合:与 SQLite 侧同形——只读三张派生统计表(用量行给去重口径的总量与「天 × 模型」桶,
|
|
27047
|
+
* 两张会话汇总表给按会话的行),数值列回来是字符串。物化列让这里不做 `EXISTS`、不回连事件表。
|
|
27048
|
+
*/
|
|
26743
27049
|
async usageReport(sinceMs) {
|
|
26744
|
-
const usageSince = sinceMs === void 0 ? sql`` : sql` AND
|
|
26745
|
-
const
|
|
27050
|
+
const usageSince = sinceMs === void 0 ? sql`` : sql` AND f_created_at >= ${sinceMs}`;
|
|
27051
|
+
const dayFilter = (column) => sinceMs === void 0 ? sql`` : sql` AND ${sql.raw(column)} >= ${localDayKey(sinceMs)}`;
|
|
26746
27052
|
const tEventUsage = this.tables["t_event_usage"];
|
|
26747
|
-
const
|
|
27053
|
+
const tSessionUsage = this.tables["t_session_usage"];
|
|
27054
|
+
const tSessionCounts = this.tables["t_session_counts"];
|
|
26748
27055
|
const tSessions = this.tables["t_sessions"];
|
|
26749
|
-
const
|
|
26750
|
-
|
|
26751
|
-
|
|
26752
|
-
|
|
26753
|
-
|
|
26754
|
-
JOIN ${tSessions} ss ON ss.f_session_id = sb.f_session_id
|
|
26755
|
-
WHERE sb.f_session_id = c.f_session_id AND ss.f_origin = 'subagent')`;
|
|
26756
|
-
const hasUsageSession = (column) => sql`EXISTS (SELECT 1 FROM ${tSessionEvents} ub
|
|
26757
|
-
JOIN ${tEventUsage} uu ON uu.f_event_id = ub.f_event_id
|
|
26758
|
-
WHERE ub.f_session_id = ${sql.raw(column)})`;
|
|
26759
|
-
const tokenColumns = sql.raw(`sum(u.f_input_tokens) AS input_tokens,
|
|
26760
|
-
sum(u.f_output_tokens) AS output_tokens,
|
|
26761
|
-
sum(u.f_cache_read_tokens) AS cache_read_tokens,
|
|
26762
|
-
sum(u.f_reasoning_tokens) AS reasoning_tokens,
|
|
26763
|
-
sum(u.f_total_tokens) AS total_tokens`);
|
|
27056
|
+
const tokenColumns = (alias) => sql.raw(`sum(${alias}.f_input_tokens) AS input_tokens,
|
|
27057
|
+
sum(${alias}.f_output_tokens) AS output_tokens,
|
|
27058
|
+
sum(${alias}.f_cache_read_tokens) AS cache_read_tokens,
|
|
27059
|
+
sum(${alias}.f_reasoning_tokens) AS reasoning_tokens,
|
|
27060
|
+
sum(${alias}.f_total_tokens) AS total_tokens`);
|
|
26764
27061
|
const buckets = await this.db.execute(sql`
|
|
26765
|
-
SELECT
|
|
26766
|
-
|
|
26767
|
-
|
|
26768
|
-
|
|
26769
|
-
${tokenColumns}
|
|
26770
|
-
FROM ${tEventUsage}
|
|
26771
|
-
WHERE
|
|
27062
|
+
SELECT a.f_day AS day,
|
|
27063
|
+
a.f_provider AS provider,
|
|
27064
|
+
a.f_model AS model,
|
|
27065
|
+
(a.f_subagent = 1) AS subagent,
|
|
27066
|
+
${tokenColumns("a")}
|
|
27067
|
+
FROM ${tEventUsage} a
|
|
27068
|
+
WHERE a.f_referenced = 1${usageSince}
|
|
26772
27069
|
GROUP BY 1, 2, 3, 4
|
|
26773
27070
|
`);
|
|
26774
27071
|
const tokenScope = await this.db.execute(sql`
|
|
26775
|
-
SELECT
|
|
26776
|
-
${tokenColumns}
|
|
26777
|
-
FROM ${tEventUsage}
|
|
26778
|
-
WHERE
|
|
27072
|
+
SELECT (a.f_subagent = 1) AS subagent,
|
|
27073
|
+
${tokenColumns("a")}
|
|
27074
|
+
FROM ${tEventUsage} a
|
|
27075
|
+
WHERE a.f_referenced = 1${usageSince}
|
|
26779
27076
|
GROUP BY 1
|
|
26780
27077
|
`);
|
|
26781
27078
|
const activityScope = await this.db.execute(sql`
|
|
26782
|
-
SELECT
|
|
26783
|
-
c.
|
|
26784
|
-
sum(c.
|
|
26785
|
-
|
|
27079
|
+
SELECT (s.f_origin = 'subagent') AS subagent,
|
|
27080
|
+
sum(c.f_turns) AS turns,
|
|
27081
|
+
sum(c.f_steps) AS steps,
|
|
27082
|
+
sum(c.f_user_inputs) AS user_inputs,
|
|
27083
|
+
sum(c.f_tool_calls) AS tool_calls
|
|
27084
|
+
FROM ${tSessionCounts} c
|
|
26786
27085
|
JOIN ${tSessions} s ON s.f_session_id = c.f_session_id
|
|
26787
|
-
WHERE ${
|
|
26788
|
-
GROUP BY 1
|
|
27086
|
+
WHERE EXISTS (SELECT 1 FROM ${tSessionUsage} ub WHERE ub.f_session_id = c.f_session_id)${dayFilter("c.f_day")}
|
|
27087
|
+
GROUP BY 1
|
|
26789
27088
|
`);
|
|
26790
27089
|
const tokenSessions = await this.db.execute(sql`
|
|
26791
|
-
SELECT
|
|
27090
|
+
SELECT u.f_session_id AS session_id,
|
|
26792
27091
|
s.f_title AS title,
|
|
26793
27092
|
(s.f_origin = 'subagent') AS subagent,
|
|
26794
27093
|
(s.f_archived_at IS NOT NULL) AS archived,
|
|
26795
|
-
${tokenColumns}
|
|
26796
|
-
FROM ${
|
|
26797
|
-
JOIN ${
|
|
26798
|
-
|
|
26799
|
-
|
|
26800
|
-
GROUP BY b.f_session_id, s.f_title, s.f_origin, s.f_archived_at
|
|
27094
|
+
${tokenColumns("u")}
|
|
27095
|
+
FROM ${tSessionUsage} u
|
|
27096
|
+
JOIN ${tSessions} s ON s.f_session_id = u.f_session_id
|
|
27097
|
+
WHERE 1 = 1${dayFilter("u.f_day")}
|
|
27098
|
+
GROUP BY u.f_session_id, s.f_title, s.f_origin, s.f_archived_at
|
|
26801
27099
|
`);
|
|
26802
27100
|
const activitySessions = await this.db.execute(sql`
|
|
26803
27101
|
SELECT c.f_session_id AS session_id,
|
|
26804
|
-
c.
|
|
26805
|
-
sum(c.
|
|
26806
|
-
|
|
26807
|
-
|
|
26808
|
-
|
|
27102
|
+
sum(c.f_turns) AS turns,
|
|
27103
|
+
sum(c.f_steps) AS steps,
|
|
27104
|
+
sum(c.f_user_inputs) AS user_inputs,
|
|
27105
|
+
sum(c.f_tool_calls) AS tool_calls
|
|
27106
|
+
FROM ${tSessionCounts} c
|
|
27107
|
+
WHERE 1 = 1${dayFilter("c.f_day")}
|
|
27108
|
+
GROUP BY c.f_session_id
|
|
26809
27109
|
`);
|
|
26810
27110
|
const human = emptyTotals();
|
|
26811
27111
|
const subagent = emptyTotals();
|
|
26812
27112
|
for (const row of tokenScope.rows) addTokenTotals(row["subagent"] === true ? subagent : human, tokenTotalsOf(row));
|
|
26813
|
-
for (const row of activityScope.rows)
|
|
27113
|
+
for (const row of activityScope.rows) addActivityTotals(row["subagent"] === true ? subagent : human, activityTotalsOf(row));
|
|
26814
27114
|
const totals = addTotals(addTotals(emptyTotals(), human), subagent);
|
|
26815
27115
|
const activityBySession = /* @__PURE__ */ new Map();
|
|
26816
|
-
for (const row of activitySessions.rows)
|
|
26817
|
-
const key = String(row["session_id"]);
|
|
26818
|
-
const target = activityBySession.get(key) ?? emptyActivity();
|
|
26819
|
-
addActivityCount(target, String(row["f_type"]), numeric(row["n"]));
|
|
26820
|
-
activityBySession.set(key, target);
|
|
26821
|
-
}
|
|
27116
|
+
for (const row of activitySessions.rows) activityBySession.set(String(row["session_id"]), activityTotalsOf(row));
|
|
26822
27117
|
return {
|
|
26823
27118
|
totals,
|
|
26824
27119
|
subagent,
|
|
@@ -26840,76 +27135,100 @@ var PostgresBackend = class PostgresBackend {
|
|
|
26840
27135
|
}))
|
|
26841
27136
|
};
|
|
26842
27137
|
}
|
|
26843
|
-
/** 表名限定到配置的 schema
|
|
27138
|
+
/** 表名限定到配置的 schema。 */
|
|
26844
27139
|
qualifiedTable(name) {
|
|
26845
27140
|
const schema = this.options.schema ?? "public";
|
|
26846
27141
|
return schema === "public" ? name : `"${schema}".${name}`;
|
|
26847
27142
|
}
|
|
26848
|
-
|
|
26849
|
-
|
|
26850
|
-
|
|
26851
|
-
|
|
26852
|
-
|
|
26853
|
-
|
|
26854
|
-
|
|
26855
|
-
|
|
26856
|
-
|
|
26857
|
-
|
|
26858
|
-
|
|
26859
|
-
|
|
26860
|
-
|
|
26861
|
-
|
|
26862
|
-
|
|
26863
|
-
|
|
26864
|
-
|
|
26865
|
-
|
|
26866
|
-
const tEvents = this.tables["t_events"];
|
|
26867
|
-
await this.db.execute(sql`
|
|
26868
|
-
INSERT INTO ${sql.raw(table)} (f_session_id, f_day, f_type, f_count)
|
|
26869
|
-
SELECT ${tSessionEvents}.f_session_id,
|
|
26870
|
-
to_char(to_timestamp(${tEvents}.f_created_at / 1000.0), 'YYYY-MM-DD'),
|
|
26871
|
-
${tEvents}.f_type,
|
|
26872
|
-
count(*)
|
|
26873
|
-
FROM ${tSessionEvents}
|
|
26874
|
-
JOIN ${tEvents} ON ${tEvents}.f_event_id = ${tSessionEvents}.f_event_id
|
|
26875
|
-
WHERE ${tEvents}.f_type IN (${sql.raw(COUNTED_EVENT_TYPE_SQL)})
|
|
26876
|
-
GROUP BY 1, 2, 3
|
|
26877
|
-
ON CONFLICT DO NOTHING
|
|
26878
|
-
`);
|
|
27143
|
+
/** token 用量旁路累加:派生表 `t_session_usage`,不在写事务里(失败可丢,表可销毁重建)。 */
|
|
27144
|
+
async incrementSessionUsage(id, buckets) {
|
|
27145
|
+
if (buckets.length === 0) return;
|
|
27146
|
+
const tSessionUsage = this.tables["t_session_usage"];
|
|
27147
|
+
for (const bucket of buckets) await this.db.execute(sql`
|
|
27148
|
+
INSERT INTO ${tSessionUsage} (
|
|
27149
|
+
f_session_id, f_day, f_provider, f_model,
|
|
27150
|
+
f_input_tokens, f_output_tokens, f_cache_read_tokens, f_reasoning_tokens, f_total_tokens)
|
|
27151
|
+
VALUES (${id}, ${bucket.day}, ${bucket.provider ?? ""}, ${bucket.model ?? ""},
|
|
27152
|
+
${bucket.inputTokens}, ${bucket.outputTokens}, ${bucket.cacheReadTokens},
|
|
27153
|
+
${bucket.reasoningTokens}, ${bucket.totalTokens})
|
|
27154
|
+
ON CONFLICT (f_session_id, f_day, f_provider, f_model) DO UPDATE SET
|
|
27155
|
+
f_input_tokens = ${tSessionUsage}.f_input_tokens + EXCLUDED.f_input_tokens,
|
|
27156
|
+
f_output_tokens = ${tSessionUsage}.f_output_tokens + EXCLUDED.f_output_tokens,
|
|
27157
|
+
f_cache_read_tokens = ${tSessionUsage}.f_cache_read_tokens + EXCLUDED.f_cache_read_tokens,
|
|
27158
|
+
f_reasoning_tokens = ${tSessionUsage}.f_reasoning_tokens + EXCLUDED.f_reasoning_tokens,
|
|
27159
|
+
f_total_tokens = ${tSessionUsage}.f_total_tokens + EXCLUDED.f_total_tokens
|
|
27160
|
+
`);
|
|
26879
27161
|
}
|
|
26880
|
-
/**
|
|
26881
|
-
async
|
|
27162
|
+
/** 活动计数旁路累加:派生表 `t_session_counts`,同上(四项计数一次落)。 */
|
|
27163
|
+
async incrementSessionCounts(id, buckets) {
|
|
26882
27164
|
if (buckets.length === 0) return;
|
|
26883
|
-
const
|
|
27165
|
+
const tSessionCounts = this.tables["t_session_counts"];
|
|
26884
27166
|
for (const bucket of buckets) await this.db.execute(sql`
|
|
26885
|
-
INSERT INTO ${
|
|
26886
|
-
|
|
26887
|
-
|
|
26888
|
-
|
|
27167
|
+
INSERT INTO ${tSessionCounts} (
|
|
27168
|
+
f_session_id, f_day, f_turns, f_steps, f_user_inputs, f_tool_calls)
|
|
27169
|
+
VALUES (${id}, ${bucket.day}, ${bucket.turns}, ${bucket.steps},
|
|
27170
|
+
${bucket.userInputs}, ${bucket.toolCalls})
|
|
27171
|
+
ON CONFLICT (f_session_id, f_day) DO UPDATE SET
|
|
27172
|
+
f_turns = ${tSessionCounts}.f_turns + EXCLUDED.f_turns,
|
|
27173
|
+
f_steps = ${tSessionCounts}.f_steps + EXCLUDED.f_steps,
|
|
27174
|
+
f_user_inputs = ${tSessionCounts}.f_user_inputs + EXCLUDED.f_user_inputs,
|
|
27175
|
+
f_tool_calls = ${tSessionCounts}.f_tool_calls + EXCLUDED.f_tool_calls
|
|
26889
27176
|
`);
|
|
26890
27177
|
}
|
|
26891
|
-
/**
|
|
26892
|
-
async
|
|
26893
|
-
const
|
|
27178
|
+
/** 按会话重算两张汇总表(rewind / fork 之后):先删该会话的行,再从事件表重算。 */
|
|
27179
|
+
async rebuildSessionStats(id) {
|
|
27180
|
+
const tSessionUsage = this.tables["t_session_usage"];
|
|
27181
|
+
const tSessionCounts = this.tables["t_session_counts"];
|
|
26894
27182
|
const tSessionEvents = this.tables["t_session_events"];
|
|
27183
|
+
const tEventUsage = this.tables["t_event_usage"];
|
|
26895
27184
|
const tEvents = this.tables["t_events"];
|
|
26896
|
-
await this.db.execute(sql`DELETE FROM ${
|
|
27185
|
+
await this.db.execute(sql`DELETE FROM ${tSessionUsage} WHERE f_session_id = ${id}`);
|
|
27186
|
+
await this.db.execute(sql`
|
|
27187
|
+
INSERT INTO ${tSessionUsage} (
|
|
27188
|
+
f_session_id, f_day, f_provider, f_model,
|
|
27189
|
+
f_input_tokens, f_output_tokens, f_cache_read_tokens, f_reasoning_tokens, f_total_tokens)
|
|
27190
|
+
SELECT b.f_session_id, u.f_day, coalesce(u.f_provider, ''), coalesce(u.f_model, ''),
|
|
27191
|
+
sum(u.f_input_tokens), sum(u.f_output_tokens), sum(u.f_cache_read_tokens),
|
|
27192
|
+
sum(u.f_reasoning_tokens), sum(u.f_total_tokens)
|
|
27193
|
+
FROM ${tSessionEvents} b
|
|
27194
|
+
JOIN ${tEventUsage} u ON u.f_event_id = b.f_event_id
|
|
27195
|
+
WHERE b.f_session_id = ${id}
|
|
27196
|
+
GROUP BY 1, 2, 3, 4
|
|
27197
|
+
`);
|
|
27198
|
+
await this.db.execute(sql`DELETE FROM ${tSessionCounts} WHERE f_session_id = ${id}`);
|
|
27199
|
+
await this.db.execute(sql`
|
|
27200
|
+
INSERT INTO ${tSessionCounts} (
|
|
27201
|
+
f_session_id, f_day, f_turns, f_steps, f_user_inputs, f_tool_calls)
|
|
27202
|
+
SELECT b.f_session_id,
|
|
27203
|
+
to_char(to_timestamp(e.f_created_at / 1000.0), 'YYYY-MM-DD'),
|
|
27204
|
+
sum(CASE WHEN e.f_type = 'turn/start' THEN 1 ELSE 0 END),
|
|
27205
|
+
sum(CASE WHEN e.f_type = 'step/start' THEN 1 ELSE 0 END),
|
|
27206
|
+
sum(CASE WHEN e.f_type = 'user/message' THEN 1 ELSE 0 END),
|
|
27207
|
+
sum(CASE WHEN e.f_type = 'tool/call' THEN 1 ELSE 0 END)
|
|
27208
|
+
FROM ${tSessionEvents} b
|
|
27209
|
+
JOIN ${tEvents} e ON e.f_event_id = b.f_event_id
|
|
27210
|
+
WHERE b.f_session_id = ${id} AND e.f_type IN (${sql.raw(COUNTED_EVENT_TYPE_SQL)})
|
|
27211
|
+
GROUP BY 1, 2
|
|
27212
|
+
`);
|
|
27213
|
+
}
|
|
27214
|
+
/** 全量重算用量行的引用标记(rewind / fork / 会话删除之后)。 */
|
|
27215
|
+
async refreshEventUsageFlags() {
|
|
27216
|
+
const tEventUsage = this.tables["t_event_usage"];
|
|
27217
|
+
const tSessionEvents = this.tables["t_session_events"];
|
|
27218
|
+
const tSessions = this.tables["t_sessions"];
|
|
26897
27219
|
await this.db.execute(sql`
|
|
26898
|
-
|
|
26899
|
-
|
|
26900
|
-
|
|
26901
|
-
|
|
26902
|
-
|
|
26903
|
-
|
|
26904
|
-
|
|
26905
|
-
WHERE ${tSessionEvents}.f_session_id = ${id}
|
|
26906
|
-
AND ${tEvents}.f_type IN (${sql.raw(COUNTED_EVENT_TYPE_SQL)})
|
|
26907
|
-
GROUP BY 1, 2, 3
|
|
27220
|
+
UPDATE ${tEventUsage} SET
|
|
27221
|
+
f_referenced = (EXISTS (SELECT 1 FROM ${tSessionEvents} rb
|
|
27222
|
+
WHERE rb.f_event_id = ${tEventUsage}.f_event_id))::int,
|
|
27223
|
+
f_subagent = (EXISTS (SELECT 1 FROM ${tSessionEvents} sb
|
|
27224
|
+
JOIN ${tSessions} ss ON ss.f_session_id = sb.f_session_id
|
|
27225
|
+
WHERE sb.f_event_id = ${tEventUsage}.f_event_id
|
|
27226
|
+
AND ss.f_origin = 'subagent'))::int
|
|
26908
27227
|
`);
|
|
26909
27228
|
}
|
|
26910
|
-
async
|
|
26911
|
-
|
|
26912
|
-
await this.db.execute(sql`DELETE FROM ${
|
|
27229
|
+
async deleteSessionStats(id) {
|
|
27230
|
+
await this.db.execute(sql`DELETE FROM ${this.tables["t_session_usage"]} WHERE f_session_id = ${id}`);
|
|
27231
|
+
await this.db.execute(sql`DELETE FROM ${this.tables["t_session_counts"]} WHERE f_session_id = ${id}`);
|
|
26913
27232
|
}
|
|
26914
27233
|
async getPrevBridge(exec, id, sequence) {
|
|
26915
27234
|
return (await exec.select({
|
|
@@ -27147,7 +27466,7 @@ var SessionBranchRdbProvider = class {
|
|
|
27147
27466
|
internals.dropReuseEventIds(childId);
|
|
27148
27467
|
throw error;
|
|
27149
27468
|
}
|
|
27150
|
-
await this.persistence.
|
|
27469
|
+
await this.persistence.rebuildSessionStats(childId);
|
|
27151
27470
|
return childId;
|
|
27152
27471
|
}
|
|
27153
27472
|
async rewind(id, toBoundary, signal) {
|
|
@@ -27220,7 +27539,7 @@ var SessionBranchRdbProvider = class {
|
|
|
27220
27539
|
headSeq: keepLength - 1,
|
|
27221
27540
|
snapshotEvents: () => []
|
|
27222
27541
|
});
|
|
27223
|
-
await this.persistence.
|
|
27542
|
+
await this.persistence.rebuildSessionStats(id);
|
|
27224
27543
|
return {
|
|
27225
27544
|
header: rowToMeta(row),
|
|
27226
27545
|
revision: await internals.readStoredRevision(id)
|
|
@@ -35021,6 +35340,7 @@ var SessionObservationReader = class {
|
|
|
35021
35340
|
* @param sessionId - logical Session identity.
|
|
35022
35341
|
* @param options - cancellation and all-or-none projection computation for this read.
|
|
35023
35342
|
* @returns one exact immutable observation.
|
|
35343
|
+
* @throws {@link SessionQueryError} with code `SESSION_QUERY_CORRUPT_SESSION` when live or prepared projection computation fails.
|
|
35024
35344
|
*/
|
|
35025
35345
|
async read(sessionId, options = {}) {
|
|
35026
35346
|
const { signal, projectionMode = "all" } = options;
|
|
@@ -35152,7 +35472,12 @@ var SessionObservationReader = class {
|
|
|
35152
35472
|
live(session, projectionMode) {
|
|
35153
35473
|
const seq = session.seq;
|
|
35154
35474
|
let materialized;
|
|
35155
|
-
|
|
35475
|
+
let projections;
|
|
35476
|
+
try {
|
|
35477
|
+
projections = projectionMode === "none" ? void 0 : this.ctx.get("sessionProjections")?.snapshot(session);
|
|
35478
|
+
} catch (error) {
|
|
35479
|
+
throw new SessionQueryError(`failed to project session "${session.id}": ${errorMessage(error)}`, "SESSION_QUERY_CORRUPT_SESSION", { cause: error });
|
|
35480
|
+
}
|
|
35156
35481
|
const lease = () => {
|
|
35157
35482
|
let disposed = false;
|
|
35158
35483
|
return {
|
|
@@ -36565,38 +36890,62 @@ var RdbSessionHandle = class RdbSessionHandle {
|
|
|
36565
36890
|
}
|
|
36566
36891
|
static LIVE_WRITE_BATCH_MAX_DELAY_MS = 200;
|
|
36567
36892
|
};
|
|
36893
|
+
/**
|
|
36894
|
+
* 本地化说明:`description()` 的类型签名只声明 `string`,而 meta 本身接受 `Dict<string>`
|
|
36895
|
+
* (`vendor/schemastery/src/index.ts` 的 `mergeDesc` 就是按字典合并的),所以这里只做一次类型放行。
|
|
36896
|
+
*/
|
|
36897
|
+
const localized = (text) => text;
|
|
36898
|
+
/** 投影 checkpoint 的写回节流:两个分支共用。 */
|
|
36899
|
+
const projectionCacheSchema = Schema.object({
|
|
36900
|
+
writeEveryEvents: Schema.natural().min(1).default(DEFAULT_PROJECTION_WRITE_EVERY_EVENTS).description(localized({
|
|
36901
|
+
zh: "每累计这么多事件写回一次投影 checkpoint。",
|
|
36902
|
+
en: "Write the projection checkpoint back after this many events."
|
|
36903
|
+
})),
|
|
36904
|
+
writeIntervalMs: Schema.natural().min(1).default(DEFAULT_PROJECTION_WRITE_INTERVAL_MS).description(localized({
|
|
36905
|
+
zh: "距离上次写回超过这个间隔(毫秒)时也写一次。",
|
|
36906
|
+
en: "Also write back when this many ms passed since the last one."
|
|
36907
|
+
}))
|
|
36908
|
+
}).default({
|
|
36909
|
+
writeEveryEvents: DEFAULT_PROJECTION_WRITE_EVERY_EVENTS,
|
|
36910
|
+
writeIntervalMs: DEFAULT_PROJECTION_WRITE_INTERVAL_MS
|
|
36911
|
+
});
|
|
36568
36912
|
var SessionPersistenceRdb = class extends SessionPersistence {
|
|
36569
|
-
config;
|
|
36570
36913
|
static inject = ["sessions"];
|
|
36571
36914
|
static Config = Schema.union([Schema.object({
|
|
36572
36915
|
type: Schema.const("sqlite"),
|
|
36573
|
-
path: Schema.string().required()
|
|
36916
|
+
path: Schema.string().required().description(localized({
|
|
36917
|
+
zh: "SQLite 数据库文件路径;`:memory:` 是内存库(进程结束即丢)。",
|
|
36918
|
+
en: "SQLite database file path; `:memory:` keeps it in memory and loses it with the process."
|
|
36919
|
+
})),
|
|
36574
36920
|
journalMode: Schema.union([
|
|
36575
36921
|
"wal",
|
|
36576
36922
|
"delete",
|
|
36577
36923
|
"truncate",
|
|
36578
36924
|
"persist"
|
|
36579
|
-
]).default("wal")
|
|
36580
|
-
|
|
36581
|
-
|
|
36582
|
-
|
|
36583
|
-
|
|
36584
|
-
|
|
36585
|
-
|
|
36586
|
-
|
|
36587
|
-
|
|
36925
|
+
]).default("wal").description(localized({
|
|
36926
|
+
zh: "SQLite 日志模式;默认 wal(并发读最好)。",
|
|
36927
|
+
en: "SQLite journal mode; `wal` by default (best concurrent reads)."
|
|
36928
|
+
})),
|
|
36929
|
+
busyTimeout: Schema.number().step(1).min(0).default(DEFAULT_BUSY_TIMEOUT_MS).description(localized({
|
|
36930
|
+
zh: "写锁冲突时的忙等超时(毫秒)。",
|
|
36931
|
+
en: "Busy timeout in ms while a write lock is contended."
|
|
36932
|
+
})),
|
|
36933
|
+
projectionCache: projectionCacheSchema
|
|
36588
36934
|
}), Schema.object({
|
|
36589
36935
|
type: Schema.const("postgres"),
|
|
36590
|
-
connectionString: Schema.string().required()
|
|
36591
|
-
|
|
36592
|
-
|
|
36593
|
-
|
|
36594
|
-
|
|
36595
|
-
|
|
36596
|
-
|
|
36597
|
-
|
|
36598
|
-
|
|
36599
|
-
})])
|
|
36936
|
+
connectionString: Schema.string().required().description(localized({
|
|
36937
|
+
zh: "PostgreSQL 连接串(含库名与凭证)。",
|
|
36938
|
+
en: "PostgreSQL connection string (database and credentials)."
|
|
36939
|
+
})),
|
|
36940
|
+
schema: Schema.string().default("public").description(localized({
|
|
36941
|
+
zh: "库内的 schema 名;默认 public。",
|
|
36942
|
+
en: "Schema name inside the database; `public` by default."
|
|
36943
|
+
})),
|
|
36944
|
+
projectionCache: projectionCacheSchema
|
|
36945
|
+
})]).description(localized({
|
|
36946
|
+
zh: "会话持久化后端:改这里的连接参数会在这一行重挂时生效(Loader 会重装被改的行)。",
|
|
36947
|
+
en: "Session persistence backend: edits apply when this row is remounted (the Loader re-installs changed rows)."
|
|
36948
|
+
})).volatile();
|
|
36600
36949
|
name = "session-rdb";
|
|
36601
36950
|
tracker = new RdbBackendTracker(this.name);
|
|
36602
36951
|
backend;
|
|
@@ -36606,10 +36955,12 @@ var SessionPersistenceRdb = class extends SessionPersistence {
|
|
|
36606
36955
|
reuseEventIds = /* @__PURE__ */ new Map();
|
|
36607
36956
|
liveBuffers = /* @__PURE__ */ new Map();
|
|
36608
36957
|
liveReady = /* @__PURE__ */ new Map();
|
|
36958
|
+
/** 本次装配用的配置快照(整段 Config 是 volatile,重挂会给新的)。 */
|
|
36959
|
+
config;
|
|
36609
36960
|
constructor(ctx, config, injectedBackend) {
|
|
36610
36961
|
super(ctx);
|
|
36611
|
-
this.config = config;
|
|
36612
|
-
this.backend = injectedBackend ?? createBackend(config);
|
|
36962
|
+
this.config = config.get();
|
|
36963
|
+
this.backend = injectedBackend ?? createBackend(this.config);
|
|
36613
36964
|
this.ready = this.init();
|
|
36614
36965
|
this.installLiveRouting(ctx);
|
|
36615
36966
|
new SessionBranchRdb(this.ctx);
|
|
@@ -36755,7 +37106,7 @@ var SessionPersistenceRdb = class extends SessionPersistence {
|
|
|
36755
37106
|
this.liveFailures.delete(id);
|
|
36756
37107
|
this.liveDropWarned.delete(id);
|
|
36757
37108
|
this.reuseEventIds.delete(id);
|
|
36758
|
-
await this.
|
|
37109
|
+
await this.dropSessionStats(id);
|
|
36759
37110
|
}
|
|
36760
37111
|
/** GC 通道:回收已无桥接行引用的事件行(孤儿),返回删除行数。 */
|
|
36761
37112
|
async collectOrphans() {
|
|
@@ -36774,7 +37125,7 @@ var SessionPersistenceRdb = class extends SessionPersistence {
|
|
|
36774
37125
|
this.liveFailures.delete(id);
|
|
36775
37126
|
this.liveDropWarned.delete(id);
|
|
36776
37127
|
this.reuseEventIds.delete(id);
|
|
36777
|
-
await this.
|
|
37128
|
+
await this.dropSessionStats(id);
|
|
36778
37129
|
}
|
|
36779
37130
|
return deleted;
|
|
36780
37131
|
}
|
|
@@ -36825,9 +37176,11 @@ var SessionPersistenceRdb = class extends SessionPersistence {
|
|
|
36825
37176
|
async appendBatch(meta, inheritedEventCount, events, tornTruncateTo) {
|
|
36826
37177
|
await this.ready;
|
|
36827
37178
|
if (events.length === 0) return false;
|
|
37179
|
+
sealOwnEvents(events);
|
|
36828
37180
|
validateStoredEvents(meta, [...events]);
|
|
36829
37181
|
const reuse = this.reuseEventIds.get(meta.id);
|
|
36830
37182
|
let confirmedHead = -1;
|
|
37183
|
+
let usageRows = [];
|
|
36831
37184
|
await this.backend.transaction(async (tx) => {
|
|
36832
37185
|
if (tornTruncateTo !== void 0) {
|
|
36833
37186
|
await tx.deleteBridgeTail(meta.id, tornTruncateTo);
|
|
@@ -36841,18 +37194,19 @@ var SessionPersistenceRdb = class extends SessionPersistence {
|
|
|
36841
37194
|
}, randomUUID());
|
|
36842
37195
|
const head = await tx.getHead(meta.id);
|
|
36843
37196
|
this.writeGuard.assertNoConcurrentWriter(meta.id, head.fHeadSequence);
|
|
36844
|
-
const
|
|
37197
|
+
const appended = await appendEventTail(tx, meta, events, {
|
|
36845
37198
|
parentId: head.fHeadEventId,
|
|
36846
37199
|
nextSeq: head.fHeadSequence + 1
|
|
36847
37200
|
}, reuse);
|
|
36848
|
-
|
|
37201
|
+
usageRows = appended.usageRows;
|
|
37202
|
+
await tx.updateHead(meta.id, appended.headEventId, appended.headSequence);
|
|
36849
37203
|
await tx.bumpRevision(meta.id, maxEventTime(events));
|
|
36850
|
-
confirmedHead = headSequence;
|
|
37204
|
+
confirmedHead = appended.headSequence;
|
|
36851
37205
|
});
|
|
36852
37206
|
if (reuse !== void 0) this.reuseEventIds.delete(meta.id);
|
|
36853
37207
|
this.writeGuard.confirmHead(meta.id, confirmedHead);
|
|
36854
37208
|
this.tracker.materialized(meta.id);
|
|
36855
|
-
await this.
|
|
37209
|
+
await this.recordSessionStats(meta.id, events, usageRows);
|
|
36856
37210
|
return true;
|
|
36857
37211
|
}
|
|
36858
37212
|
async readLog(id, options = {}, signal) {
|
|
@@ -36890,7 +37244,7 @@ var SessionPersistenceRdb = class extends SessionPersistence {
|
|
|
36890
37244
|
};
|
|
36891
37245
|
}
|
|
36892
37246
|
const { preserved, tornFrom } = scanRows(eventRows, options.fromSeq ?? 0);
|
|
36893
|
-
if (
|
|
37247
|
+
if (needsShapeAdoption(preserved)) {
|
|
36894
37248
|
const adopted = adoptLegacyRows(row, eventRows);
|
|
36895
37249
|
return {
|
|
36896
37250
|
meta: adopted.meta,
|
|
@@ -36915,32 +37269,43 @@ var SessionPersistenceRdb = class extends SessionPersistence {
|
|
|
36915
37269
|
};
|
|
36916
37270
|
}
|
|
36917
37271
|
/**
|
|
36918
|
-
*
|
|
36919
|
-
*
|
|
37272
|
+
* 统计衍生表的**旁路累加**:不在写事务里、失败只 warn——两张会话汇总表都是可销毁重建的
|
|
37273
|
+
* 派生表,丢几次累加不影响可用性(需要时用 `rebuildSessionStats` 重算)。
|
|
37274
|
+
* token 走写路径已折好的用量行,活动计数走本批事件类型。
|
|
36920
37275
|
*/
|
|
36921
|
-
async
|
|
36922
|
-
const
|
|
36923
|
-
if (
|
|
36924
|
-
|
|
36925
|
-
|
|
37276
|
+
async recordSessionStats(id, events, usageRows) {
|
|
37277
|
+
const usageBuckets = sessionUsageBuckets(usageRows);
|
|
37278
|
+
if (usageBuckets.length > 0) try {
|
|
37279
|
+
await this.backend.incrementSessionUsage(id, usageBuckets);
|
|
37280
|
+
} catch (error) {
|
|
37281
|
+
this.ctx.logger.warn(`session-rdb: token usage for "${id}" not updated (${describeError(error)})`);
|
|
37282
|
+
}
|
|
37283
|
+
const countBuckets = sessionCountBuckets(events);
|
|
37284
|
+
if (countBuckets.length > 0) try {
|
|
37285
|
+
await this.backend.incrementSessionCounts(id, countBuckets);
|
|
36926
37286
|
} catch (error) {
|
|
36927
37287
|
this.ctx.logger.warn(`session-rdb: activity counts for "${id}" not updated (${describeError(error)})`);
|
|
36928
37288
|
}
|
|
36929
37289
|
}
|
|
36930
|
-
/**
|
|
36931
|
-
|
|
37290
|
+
/**
|
|
37291
|
+
* 重算一个会话的统计(rewind / fork 之后;best-effort,同 `recordSessionStats`):
|
|
37292
|
+
* 两张汇总表按会话重算,用量行的引用标记全量重算(引用可能跨会话消失)。
|
|
37293
|
+
*/
|
|
37294
|
+
async rebuildSessionStats(id) {
|
|
36932
37295
|
try {
|
|
36933
|
-
await this.backend.
|
|
37296
|
+
await this.backend.rebuildSessionStats(id);
|
|
37297
|
+
await this.backend.refreshEventUsageFlags();
|
|
36934
37298
|
} catch (error) {
|
|
36935
|
-
this.ctx.logger.warn(`session-rdb:
|
|
37299
|
+
this.ctx.logger.warn(`session-rdb: session stats for "${id}" not rebuilt (${describeError(error)})`);
|
|
36936
37300
|
}
|
|
36937
37301
|
}
|
|
36938
|
-
/**
|
|
36939
|
-
async
|
|
37302
|
+
/** 会话删除时清掉它的汇总行,并重算用量行的引用标记(被删会话引用的行要立刻退出统计)。 */
|
|
37303
|
+
async dropSessionStats(id) {
|
|
36940
37304
|
try {
|
|
36941
|
-
await this.backend.
|
|
37305
|
+
await this.backend.deleteSessionStats(id);
|
|
37306
|
+
await this.backend.refreshEventUsageFlags();
|
|
36942
37307
|
} catch (error) {
|
|
36943
|
-
this.ctx.logger.warn(`session-rdb:
|
|
37308
|
+
this.ctx.logger.warn(`session-rdb: session stats for "${id}" not deleted (${describeError(error)})`);
|
|
36944
37309
|
}
|
|
36945
37310
|
}
|
|
36946
37311
|
async rewriteMigratedLog(id, log) {
|
|
@@ -36953,13 +37318,14 @@ var SessionPersistenceRdb = class extends SessionPersistence {
|
|
|
36953
37318
|
meta: log.meta,
|
|
36954
37319
|
inheritedEventCount: SessionLogOffset(log.inheritedEventCount)
|
|
36955
37320
|
}, randomUUID());
|
|
36956
|
-
const
|
|
37321
|
+
const appended = await appendEventTail(tx, log.meta, log.events, {
|
|
36957
37322
|
parentId: "",
|
|
36958
37323
|
nextSeq: 0
|
|
36959
37324
|
});
|
|
36960
|
-
await tx.updateHead(id, headEventId, headSequence);
|
|
37325
|
+
await tx.updateHead(id, appended.headEventId, appended.headSequence);
|
|
36961
37326
|
await tx.bumpRevision(id, maxEventTime(log.events));
|
|
36962
37327
|
});
|
|
37328
|
+
await this.rebuildSessionStats(id);
|
|
36963
37329
|
}
|
|
36964
37330
|
async listSnapshots(signal) {
|
|
36965
37331
|
signal?.throwIfAborted();
|
|
@@ -37189,19 +37555,46 @@ function seedCoversPrefix(seed, prefix) {
|
|
|
37189
37555
|
return seedEvent !== void 0 && JSON.stringify(seedEvent) === JSON.stringify(event);
|
|
37190
37556
|
});
|
|
37191
37557
|
}
|
|
37192
|
-
/**
|
|
37193
|
-
function
|
|
37558
|
+
/** 一批事件折成活动计数的桶:只数四种类型,按本地日聚合成四项。 */
|
|
37559
|
+
function sessionCountBuckets(events) {
|
|
37194
37560
|
const counted = /* @__PURE__ */ new Map();
|
|
37195
37561
|
for (const event of events) {
|
|
37196
37562
|
if (!COUNTED_EVENT_TYPES.includes(event.type)) continue;
|
|
37197
37563
|
const day = localDayKey(event.time);
|
|
37198
|
-
const
|
|
37199
|
-
const bucket = counted.get(key) ?? {
|
|
37564
|
+
const bucket = counted.get(day) ?? {
|
|
37200
37565
|
day,
|
|
37201
|
-
|
|
37202
|
-
|
|
37566
|
+
turns: 0,
|
|
37567
|
+
steps: 0,
|
|
37568
|
+
userInputs: 0,
|
|
37569
|
+
toolCalls: 0
|
|
37570
|
+
};
|
|
37571
|
+
addActivityCount(bucket, event.type, 1);
|
|
37572
|
+
counted.set(day, bucket);
|
|
37573
|
+
}
|
|
37574
|
+
return [...counted.values()];
|
|
37575
|
+
}
|
|
37576
|
+
/** 一批用量行折成 token 桶:按「本地日 × 模型」聚合(模型未知落空串,与表口径一致)。 */
|
|
37577
|
+
function sessionUsageBuckets(usageRows) {
|
|
37578
|
+
const counted = /* @__PURE__ */ new Map();
|
|
37579
|
+
for (const row of usageRows) {
|
|
37580
|
+
const provider = row.fProvider ?? "";
|
|
37581
|
+
const model = row.fModel ?? "";
|
|
37582
|
+
const key = `${row.fDay}#${provider}#${model}`;
|
|
37583
|
+
const bucket = counted.get(key) ?? {
|
|
37584
|
+
day: row.fDay,
|
|
37585
|
+
provider,
|
|
37586
|
+
model,
|
|
37587
|
+
inputTokens: 0,
|
|
37588
|
+
outputTokens: 0,
|
|
37589
|
+
cacheReadTokens: 0,
|
|
37590
|
+
reasoningTokens: 0,
|
|
37591
|
+
totalTokens: 0
|
|
37203
37592
|
};
|
|
37204
|
-
bucket.
|
|
37593
|
+
bucket.inputTokens += row.fInputTokens;
|
|
37594
|
+
bucket.outputTokens += row.fOutputTokens;
|
|
37595
|
+
bucket.cacheReadTokens += row.fCacheReadTokens;
|
|
37596
|
+
bucket.reasoningTokens += row.fReasoningTokens;
|
|
37597
|
+
bucket.totalTokens += row.fTotalTokens;
|
|
37205
37598
|
counted.set(key, bucket);
|
|
37206
37599
|
}
|
|
37207
37600
|
return [...counted.values()];
|
|
@@ -37233,6 +37626,7 @@ async function appendEventTail(tx, meta, events, anchor, reuse) {
|
|
|
37233
37626
|
let parentId = anchor.parentId;
|
|
37234
37627
|
let nextSeq = anchor.nextSeq;
|
|
37235
37628
|
const eventRows = [];
|
|
37629
|
+
const usageRows = [];
|
|
37236
37630
|
const bridgeRows = [];
|
|
37237
37631
|
for (const event of events) {
|
|
37238
37632
|
const reusedId = reuse?.get(event.seq);
|
|
@@ -37240,7 +37634,7 @@ async function appendEventTail(tx, meta, events, anchor, reuse) {
|
|
|
37240
37634
|
if (reusedId === void 0) {
|
|
37241
37635
|
const { kind, role, name, actionId } = eventDimensions(event);
|
|
37242
37636
|
const { data, surfaceOp: _surfaceOp, sourceEventSeqs: _sourceEventSeqs, ...envelope } = event;
|
|
37243
|
-
|
|
37637
|
+
const row = {
|
|
37244
37638
|
fEventId: eventId,
|
|
37245
37639
|
fParentId: parentId,
|
|
37246
37640
|
fType: event.type,
|
|
@@ -37254,7 +37648,10 @@ async function appendEventTail(tx, meta, events, anchor, reuse) {
|
|
|
37254
37648
|
data
|
|
37255
37649
|
}),
|
|
37256
37650
|
fCreatedAt: event.time
|
|
37257
|
-
}
|
|
37651
|
+
};
|
|
37652
|
+
eventRows.push(row);
|
|
37653
|
+
const usageRow = usageRowOf(row, meta.origin === "subagent");
|
|
37654
|
+
if (usageRow !== void 0) usageRows.push(usageRow);
|
|
37258
37655
|
}
|
|
37259
37656
|
const surfaceOp = event.surfaceOp === void 0 ? null : JSON.stringify(event.surfaceOp);
|
|
37260
37657
|
bridgeRows.push({
|
|
@@ -37266,12 +37663,13 @@ async function appendEventTail(tx, meta, events, anchor, reuse) {
|
|
|
37266
37663
|
parentId = eventId;
|
|
37267
37664
|
nextSeq++;
|
|
37268
37665
|
}
|
|
37269
|
-
if (eventRows.length > 0) await tx.insertEvents(eventRows);
|
|
37666
|
+
if (eventRows.length > 0) await tx.insertEvents(eventRows, usageRows);
|
|
37270
37667
|
await tx.insertBridges(bridgeRows);
|
|
37271
37668
|
if (events.some((event) => event.type === "session/title")) await tx.refreshTitle(meta.id);
|
|
37272
37669
|
return {
|
|
37273
37670
|
headEventId: parentId,
|
|
37274
|
-
headSequence: nextSeq - 1
|
|
37671
|
+
headSequence: nextSeq - 1,
|
|
37672
|
+
usageRows
|
|
37275
37673
|
};
|
|
37276
37674
|
}
|
|
37277
37675
|
//#endregion
|