@morlay/session-rdb 0.0.21-alpha.7 → 0.0.21-alpha.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +17 -10
- package/dist/artifact.d.mts +2 -101
- package/dist/artifact.mjs +2 -2
- package/dist/{branch-BjUR6wfe.mjs → branch-7eOh_tbO.mjs} +3 -3
- package/dist/deletion.d.mts +1 -1
- package/dist/deletion.mjs +1 -1
- package/dist/import.d.mts +1 -1
- package/dist/import.mjs +1 -1
- package/dist/index.d.mts +1 -1
- package/dist/index.mjs +3 -3
- package/dist/{backend-E0bmTXpz.d.mts → log-BbleqD1M.d.mts} +171 -24
- package/dist/{log-qCNkDjh5.mjs → log-DTcvsMz7.mjs} +189 -12
- package/dist/{sqlite-zcXVyMxr.mjs → sqlite-ChM_sHAG.mjs} +320 -259
- package/dist/{src-B9X8BKyh.mjs → src-BWqePRA7.mjs} +290 -162
- package/dist/storage.d.mts +20 -7
- package/dist/storage.mjs +2 -2
- package/dist/testing.d.mts +1 -1
- package/dist/testing.mjs +1 -1
- package/drizzle/postgres/20260923120000_v3_usage_materialized/migration.sql +48 -0
- package/drizzle/sqlite/20260923120000_v3_usage_materialized/migration.sql +48 -0
- package/package.json +20 -20
- package/src/backend.ts +41 -17
- package/src/branch.ts +4 -4
- package/src/drizzle/postgres-v3.ts +2 -0
- package/src/drizzle/sqlite-v3.ts +2 -0
- package/src/entities/v3/event-usage.ts +8 -1
- package/src/entities/v3/index.ts +8 -0
- package/src/entities/v3/session-counts.ts +23 -0
- package/src/entities/v3/session-usage.ts +29 -0
- package/src/index.ts +109 -43
- package/src/log.ts +58 -17
- package/src/postgres.ts +248 -149
- package/src/rows.ts +2 -1
- package/src/schema.ts +4 -0
- package/src/sqlite.ts +263 -124
- package/src/storage-takeover/repository.ts +1 -4
- package/src/usage.ts +10 -6
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { C as COUNTED_EVENT_TYPES, D as addTotals, E as addTokenTotals, O as emptyTotals, T as addActivityTotals, _ as sessionConflictRow, b as titleOfEventData, k as localDayKey, v as sessionInsertRow } from "./log-DTcvsMz7.mjs";
|
|
2
2
|
import { randomUUID } from "node:crypto";
|
|
3
3
|
import { SESSION_FORMAT_VERSION, SessionId, SessionLogOffset, SessionSeq } from "@deepseek-ai/dsh-session";
|
|
4
4
|
import { and, desc, eq, gte, inArray, isNotNull, lt, notInArray, sql } from "drizzle-orm";
|
|
@@ -453,7 +453,9 @@ const sessionProjcacheRows = {
|
|
|
453
453
|
//#region src/entities/v3/event-usage.ts
|
|
454
454
|
/**
|
|
455
455
|
* 用量日志:一条 `assistant/message` 事件一行(按事件行唯一,fork 共享行不重复)。
|
|
456
|
-
*
|
|
456
|
+
* 读侧维度全部物化在行上——本地日、是否被会话引用、是否被子代理会话引用——统计因此不做
|
|
457
|
+
* `EXISTS` 判定、也不回连事件表;事件行本身仍留在 `t_events`。
|
|
458
|
+
* 取舍见 ADR-统计衍生表物化归属与汇总。
|
|
457
459
|
*/
|
|
458
460
|
const eventUsage = {
|
|
459
461
|
name: "t_event_usage",
|
|
@@ -471,8 +473,22 @@ const eventUsage = {
|
|
|
471
473
|
type: "bigint",
|
|
472
474
|
notNull: true
|
|
473
475
|
},
|
|
476
|
+
f_day: {
|
|
477
|
+
type: "text",
|
|
478
|
+
notNull: true
|
|
479
|
+
},
|
|
474
480
|
f_provider: { type: "text" },
|
|
475
481
|
f_model: { type: "text" },
|
|
482
|
+
f_referenced: {
|
|
483
|
+
type: "integer",
|
|
484
|
+
notNull: true,
|
|
485
|
+
default: 0
|
|
486
|
+
},
|
|
487
|
+
f_subagent: {
|
|
488
|
+
type: "integer",
|
|
489
|
+
notNull: true,
|
|
490
|
+
default: 0
|
|
491
|
+
},
|
|
476
492
|
f_input_tokens: {
|
|
477
493
|
type: "integer",
|
|
478
494
|
notNull: true,
|
|
@@ -502,6 +518,118 @@ const eventUsage = {
|
|
|
502
518
|
indexes: { idx_event_usage_created_at: { columns: ["f_created_at"] } }
|
|
503
519
|
};
|
|
504
520
|
//#endregion
|
|
521
|
+
//#region src/entities/v3/session-usage.ts
|
|
522
|
+
/**
|
|
523
|
+
* 会话 × 本地日 × 模型的 token 汇总(派生统计表):**按会话的行**读它,因此保住
|
|
524
|
+
* 「fork 子会话含继承前缀」的口径(各会话行之和 ≥ 总量)而不回连事件表。
|
|
525
|
+
* 旁路累加、可按会话重建,读侧只读;取舍见 ADR-统计衍生表物化归属与汇总。
|
|
526
|
+
*/
|
|
527
|
+
const sessionUsage = {
|
|
528
|
+
name: "t_session_usage",
|
|
529
|
+
columns: {
|
|
530
|
+
f_session_id: {
|
|
531
|
+
type: "text",
|
|
532
|
+
notNull: true,
|
|
533
|
+
references: {
|
|
534
|
+
table: "t_sessions",
|
|
535
|
+
column: "f_session_id",
|
|
536
|
+
onDelete: "cascade"
|
|
537
|
+
}
|
|
538
|
+
},
|
|
539
|
+
f_day: {
|
|
540
|
+
type: "text",
|
|
541
|
+
notNull: true
|
|
542
|
+
},
|
|
543
|
+
f_provider: {
|
|
544
|
+
type: "text",
|
|
545
|
+
notNull: true,
|
|
546
|
+
default: ""
|
|
547
|
+
},
|
|
548
|
+
f_model: {
|
|
549
|
+
type: "text",
|
|
550
|
+
notNull: true,
|
|
551
|
+
default: ""
|
|
552
|
+
},
|
|
553
|
+
f_input_tokens: {
|
|
554
|
+
type: "integer",
|
|
555
|
+
notNull: true,
|
|
556
|
+
default: 0
|
|
557
|
+
},
|
|
558
|
+
f_output_tokens: {
|
|
559
|
+
type: "integer",
|
|
560
|
+
notNull: true,
|
|
561
|
+
default: 0
|
|
562
|
+
},
|
|
563
|
+
f_cache_read_tokens: {
|
|
564
|
+
type: "integer",
|
|
565
|
+
notNull: true,
|
|
566
|
+
default: 0
|
|
567
|
+
},
|
|
568
|
+
f_reasoning_tokens: {
|
|
569
|
+
type: "integer",
|
|
570
|
+
notNull: true,
|
|
571
|
+
default: 0
|
|
572
|
+
},
|
|
573
|
+
f_total_tokens: {
|
|
574
|
+
type: "integer",
|
|
575
|
+
notNull: true,
|
|
576
|
+
default: 0
|
|
577
|
+
}
|
|
578
|
+
},
|
|
579
|
+
uniques: { uq_session_usage_key: { columns: [
|
|
580
|
+
"f_session_id",
|
|
581
|
+
"f_day",
|
|
582
|
+
"f_provider",
|
|
583
|
+
"f_model"
|
|
584
|
+
] } }
|
|
585
|
+
};
|
|
586
|
+
//#endregion
|
|
587
|
+
//#region src/entities/v3/session-counts.ts
|
|
588
|
+
/**
|
|
589
|
+
* 会话 × 本地日的活动计数(派生统计表):轮次 / 步骤 / 用户输入 / 工具调用四列直接读,
|
|
590
|
+
* 不再存事件类型、也不在读取时折。旁路累加、可按会话重建,读侧只读;
|
|
591
|
+
* 取舍见 ADR-统计衍生表物化归属与汇总。
|
|
592
|
+
*/
|
|
593
|
+
const sessionCounts = {
|
|
594
|
+
name: "t_session_counts",
|
|
595
|
+
columns: {
|
|
596
|
+
f_session_id: {
|
|
597
|
+
type: "text",
|
|
598
|
+
notNull: true,
|
|
599
|
+
references: {
|
|
600
|
+
table: "t_sessions",
|
|
601
|
+
column: "f_session_id",
|
|
602
|
+
onDelete: "cascade"
|
|
603
|
+
}
|
|
604
|
+
},
|
|
605
|
+
f_day: {
|
|
606
|
+
type: "text",
|
|
607
|
+
notNull: true
|
|
608
|
+
},
|
|
609
|
+
f_turns: {
|
|
610
|
+
type: "integer",
|
|
611
|
+
notNull: true,
|
|
612
|
+
default: 0
|
|
613
|
+
},
|
|
614
|
+
f_steps: {
|
|
615
|
+
type: "integer",
|
|
616
|
+
notNull: true,
|
|
617
|
+
default: 0
|
|
618
|
+
},
|
|
619
|
+
f_user_inputs: {
|
|
620
|
+
type: "integer",
|
|
621
|
+
notNull: true,
|
|
622
|
+
default: 0
|
|
623
|
+
},
|
|
624
|
+
f_tool_calls: {
|
|
625
|
+
type: "integer",
|
|
626
|
+
notNull: true,
|
|
627
|
+
default: 0
|
|
628
|
+
}
|
|
629
|
+
},
|
|
630
|
+
uniques: { uq_session_counts_key: { columns: ["f_session_id", "f_day"] } }
|
|
631
|
+
};
|
|
632
|
+
//#endregion
|
|
505
633
|
//#region src/entities/v3/index.ts
|
|
506
634
|
const sqliteTableDefs = [
|
|
507
635
|
persistenceState,
|
|
@@ -514,7 +642,9 @@ const sqliteTableDefs = [
|
|
|
514
642
|
workspaceSessions,
|
|
515
643
|
workspaceState,
|
|
516
644
|
sessionProjcacheRows,
|
|
517
|
-
eventUsage
|
|
645
|
+
eventUsage,
|
|
646
|
+
sessionUsage,
|
|
647
|
+
sessionCounts
|
|
518
648
|
];
|
|
519
649
|
const postgresTableDefs = [
|
|
520
650
|
persistenceState,
|
|
@@ -527,7 +657,9 @@ const postgresTableDefs = [
|
|
|
527
657
|
workspaceSessions,
|
|
528
658
|
workspaceState,
|
|
529
659
|
sessionProjcacheRows,
|
|
530
|
-
eventUsage
|
|
660
|
+
eventUsage,
|
|
661
|
+
sessionUsage,
|
|
662
|
+
sessionCounts
|
|
531
663
|
];
|
|
532
664
|
({ ...sessionEvents$1 }), { ...sessionEvents$1.columns };
|
|
533
665
|
//#endregion
|
|
@@ -547,6 +679,8 @@ const tWorkspaceSessions = sqliteTables["t_workspace_sessions"];
|
|
|
547
679
|
const tWorkspaceState = sqliteTables["t_workspace_state"];
|
|
548
680
|
const tSessionProjcacheRows = sqliteTables["t_session_projcache_row"];
|
|
549
681
|
const tEventUsage = sqliteTables["t_event_usage"];
|
|
682
|
+
const tSessionUsage = sqliteTables["t_session_usage"];
|
|
683
|
+
const tSessionCounts = sqliteTables["t_session_counts"];
|
|
550
684
|
const DEFAULT_BUSY_TIMEOUT_MS = 5e3;
|
|
551
685
|
function eventKind(event) {
|
|
552
686
|
switch (event.type) {
|
|
@@ -703,152 +837,6 @@ function eventDimensions(event) {
|
|
|
703
837
|
}
|
|
704
838
|
}
|
|
705
839
|
//#endregion
|
|
706
|
-
//#region src/usage.ts
|
|
707
|
-
const SESSION_USAGE_PATH = "/api/session.usage";
|
|
708
|
-
const DAY_MS = 864e5;
|
|
709
|
-
const ROLLING_DAYS = {
|
|
710
|
-
"7d": 7,
|
|
711
|
-
"30d": 30,
|
|
712
|
-
"90d": 90
|
|
713
|
-
};
|
|
714
|
-
function localDayStart(now) {
|
|
715
|
-
const start = new Date(now);
|
|
716
|
-
start.setHours(0, 0, 0, 0);
|
|
717
|
-
return start;
|
|
718
|
-
}
|
|
719
|
-
/**
|
|
720
|
-
* 范围起点(含),不限时为 undefined。自然日 / 自然周按 host 的本地时区算,周边界是周一 00:00。
|
|
721
|
-
* @param range - 语义键。
|
|
722
|
-
* @param now - 当前时刻。
|
|
723
|
-
* @returns 起点毫秒时间戳,或 undefined。
|
|
724
|
-
*/
|
|
725
|
-
function resolveUsageSince(range, now) {
|
|
726
|
-
switch (range) {
|
|
727
|
-
case "all": return;
|
|
728
|
-
case "day": return localDayStart(now).getTime();
|
|
729
|
-
case "week": {
|
|
730
|
-
const start = localDayStart(now);
|
|
731
|
-
start.setDate(start.getDate() - (start.getDay() + 6) % 7);
|
|
732
|
-
return start.getTime();
|
|
733
|
-
}
|
|
734
|
-
default: return now - ROLLING_DAYS[range] * DAY_MS;
|
|
735
|
-
}
|
|
736
|
-
}
|
|
737
|
-
function parseUsageRange(value) {
|
|
738
|
-
return value === "day" || value === "week" || value === "7d" || value === "30d" || value === "90d" ? value : "all";
|
|
739
|
-
}
|
|
740
|
-
/** 计入活动计数的事件类型(轮次 / 步骤 / 用户输入 / 工具调用)。 */
|
|
741
|
-
const COUNTED_EVENT_TYPES = [
|
|
742
|
-
"turn/start",
|
|
743
|
-
"step/start",
|
|
744
|
-
"user/message",
|
|
745
|
-
"tool/call"
|
|
746
|
-
];
|
|
747
|
-
/** 按事件类型把计数累加到活动指标上(表里按类型存,读的时候折成四项)。 */
|
|
748
|
-
function addActivityCount(target, type, count) {
|
|
749
|
-
switch (type) {
|
|
750
|
-
case "turn/start":
|
|
751
|
-
target.turns += count;
|
|
752
|
-
return target;
|
|
753
|
-
case "step/start":
|
|
754
|
-
target.steps += count;
|
|
755
|
-
return target;
|
|
756
|
-
case "user/message":
|
|
757
|
-
target.userInputs += count;
|
|
758
|
-
return target;
|
|
759
|
-
case "tool/call":
|
|
760
|
-
target.toolCalls += count;
|
|
761
|
-
return target;
|
|
762
|
-
default: return target;
|
|
763
|
-
}
|
|
764
|
-
}
|
|
765
|
-
/** 毫秒时间戳 → host 本地日(`YYYY-MM-DD`,与 SQL 的 localtime 口径一致)。 */
|
|
766
|
-
function localDayKey(ms) {
|
|
767
|
-
const date = new Date(ms);
|
|
768
|
-
const pad = (value) => String(value).padStart(2, "0");
|
|
769
|
-
return `${String(date.getFullYear())}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`;
|
|
770
|
-
}
|
|
771
|
-
function emptyTotals() {
|
|
772
|
-
return {
|
|
773
|
-
turns: 0,
|
|
774
|
-
steps: 0,
|
|
775
|
-
userInputs: 0,
|
|
776
|
-
toolCalls: 0,
|
|
777
|
-
inputTokens: 0,
|
|
778
|
-
outputTokens: 0,
|
|
779
|
-
cacheReadTokens: 0,
|
|
780
|
-
reasoningTokens: 0,
|
|
781
|
-
totalTokens: 0
|
|
782
|
-
};
|
|
783
|
-
}
|
|
784
|
-
function addTokenTotals(target, row) {
|
|
785
|
-
target.inputTokens += row.inputTokens;
|
|
786
|
-
target.outputTokens += row.outputTokens;
|
|
787
|
-
target.cacheReadTokens += row.cacheReadTokens;
|
|
788
|
-
target.reasoningTokens += row.reasoningTokens;
|
|
789
|
-
target.totalTokens += row.totalTokens;
|
|
790
|
-
return target;
|
|
791
|
-
}
|
|
792
|
-
function addActivityTotals(target, row) {
|
|
793
|
-
target.turns += row.turns;
|
|
794
|
-
target.steps += row.steps;
|
|
795
|
-
target.userInputs += row.userInputs;
|
|
796
|
-
target.toolCalls += row.toolCalls;
|
|
797
|
-
return target;
|
|
798
|
-
}
|
|
799
|
-
/** 把一行的整套指标累加进目标(后端合并「是否子代理」两组时用)。 */
|
|
800
|
-
function addTotals(target, row) {
|
|
801
|
-
addActivityTotals(target, row);
|
|
802
|
-
addTokenTotals(target, row);
|
|
803
|
-
return target;
|
|
804
|
-
}
|
|
805
|
-
/**
|
|
806
|
-
* 用量统计通道:一次请求回报总览(含 subagent 拆分)、按天 × 模型的桶与按会话的行。
|
|
807
|
-
* 聚合只算被会话引用的事件行——fork 共享行因此只计一次,已删会话留下的孤儿行不计。
|
|
808
|
-
*/
|
|
809
|
-
function registerSessionUsage(ctx, persistence) {
|
|
810
|
-
ctx.inject(["webServer", "connection"], (webCtx) => {
|
|
811
|
-
const webServer = webCtx.webServer;
|
|
812
|
-
const connection = webCtx.get("connection");
|
|
813
|
-
return webCtx.effect(() => webServer.register({
|
|
814
|
-
kind: "exact",
|
|
815
|
-
path: SESSION_USAGE_PATH,
|
|
816
|
-
handler: async (req, res) => {
|
|
817
|
-
const rejection = connection.requestRejection(req);
|
|
818
|
-
if (rejection !== void 0) {
|
|
819
|
-
res.writeHead(rejection);
|
|
820
|
-
res.end(rejection === 401 ? "unauthorized" : "forbidden");
|
|
821
|
-
return;
|
|
822
|
-
}
|
|
823
|
-
if (req.method !== "POST") {
|
|
824
|
-
res.writeHead(405, { "content-type": "application/json" });
|
|
825
|
-
res.end(JSON.stringify({ error: "method not allowed" }));
|
|
826
|
-
return;
|
|
827
|
-
}
|
|
828
|
-
const chunks = [];
|
|
829
|
-
for await (const chunk of req) chunks.push(chunk);
|
|
830
|
-
const raw = Buffer.concat(chunks).toString("utf8");
|
|
831
|
-
let envelope = {};
|
|
832
|
-
if (raw !== "") try {
|
|
833
|
-
envelope = JSON.parse(raw);
|
|
834
|
-
} catch {
|
|
835
|
-
res.writeHead(400, { "content-type": "application/json" });
|
|
836
|
-
res.end(JSON.stringify({ error: "request body is not JSON" }));
|
|
837
|
-
return;
|
|
838
|
-
}
|
|
839
|
-
try {
|
|
840
|
-
const report = await persistence.usageReport(resolveUsageSince(parseUsageRange(envelope.range), Date.now()));
|
|
841
|
-
res.writeHead(200, { "content-type": "application/json" });
|
|
842
|
-
res.end(JSON.stringify(report));
|
|
843
|
-
} catch (error) {
|
|
844
|
-
res.writeHead(500, { "content-type": "application/json" });
|
|
845
|
-
res.end(JSON.stringify({ error: error instanceof Error ? error.message : "usage report failed" }));
|
|
846
|
-
}
|
|
847
|
-
}
|
|
848
|
-
}), `session-rdb: ${SESSION_USAGE_PATH} route`);
|
|
849
|
-
});
|
|
850
|
-
}
|
|
851
|
-
//#endregion
|
|
852
840
|
//#region src/storage-takeover/repository.ts
|
|
853
841
|
async function allRows(query) {
|
|
854
842
|
const q = query;
|
|
@@ -1107,45 +1095,44 @@ function createStorageRepository(host) {
|
|
|
1107
1095
|
}
|
|
1108
1096
|
//#endregion
|
|
1109
1097
|
//#region src/sqlite.ts
|
|
1110
|
-
/**
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
CREATE INDEX IF NOT EXISTS idx_event_counts_day ON t_event_counts(f_day);`);
|
|
1122
|
-
}
|
|
1123
|
-
/** 一次性回填活动计数(表为空时从事件表重算;幂等)。 */
|
|
1124
|
-
function backfillEventCounts(db) {
|
|
1125
|
-
if (db.prepare("SELECT count(*) AS n FROM t_event_counts").get().n > 0) return;
|
|
1126
|
-
db.exec(`INSERT OR IGNORE INTO t_event_counts (f_session_id, f_day, f_type, f_count)
|
|
1127
|
-
SELECT b.f_session_id,
|
|
1128
|
-
date(e.f_created_at / 1000, 'unixepoch', 'localtime'),
|
|
1129
|
-
e.f_type,
|
|
1130
|
-
count(*)
|
|
1131
|
-
FROM t_session_events b
|
|
1132
|
-
JOIN t_events e ON e.f_event_id = b.f_event_id
|
|
1133
|
-
WHERE e.f_type IN (${COUNTED_EVENT_TYPE_SQL})
|
|
1134
|
-
GROUP BY 1, 2, 3`);
|
|
1098
|
+
/** 用量行的「被引用」物化表达式(`%EVENT%` 换成事件 id 列)。 */
|
|
1099
|
+
const EVENT_REFERENCED_SQL = `EXISTS (SELECT 1 FROM t_session_events rb
|
|
1100
|
+
WHERE rb.f_event_id = %EVENT%)`;
|
|
1101
|
+
/** 用量行的「被 subagent 会话引用」物化表达式(`%EVENT%` 换成事件 id 列)。 */
|
|
1102
|
+
const EVENT_SUBAGENT_SQL = `EXISTS (SELECT 1 FROM t_session_events sb
|
|
1103
|
+
JOIN t_sessions ss ON ss.f_session_id = sb.f_session_id
|
|
1104
|
+
WHERE sb.f_event_id = %EVENT% AND ss.f_origin = 'subagent')`;
|
|
1105
|
+
/** `IN (...)` 用的类型字面量(与 `COUNTED_EVENT_TYPES` 同源)。 */
|
|
1106
|
+
const COUNTED_EVENT_TYPE_SQL = COUNTED_EVENT_TYPES.map((type) => `'${type}'`).join(", ");
|
|
1107
|
+
function withEvent(expression, column) {
|
|
1108
|
+
return expression.replace("%EVENT%", column);
|
|
1135
1109
|
}
|
|
1136
1110
|
/**
|
|
1137
|
-
*
|
|
1111
|
+
* 派生统计表的回填:任一表为空即按事件表全量重算(幂等;迁移删表重建后、手动清空后都靠它自愈)。
|
|
1112
|
+
* 顺序有依赖:用量行(带物化列)先落,两张会话汇总表再从它 / 事件表重算。
|
|
1138
1113
|
*/
|
|
1114
|
+
function backfillUsageTables(db) {
|
|
1115
|
+
backfillEventUsage(db);
|
|
1116
|
+
backfillSessionUsage(db);
|
|
1117
|
+
backfillSessionCounts(db);
|
|
1118
|
+
}
|
|
1119
|
+
function tableIsEmpty(db, table) {
|
|
1120
|
+
return db.prepare(`SELECT count(*) AS n FROM ${table}`).get().n === 0;
|
|
1121
|
+
}
|
|
1122
|
+
/** 用量行:一条 `assistant/message` 事件行一行,物化本地日与两个归属标记。 */
|
|
1139
1123
|
function backfillEventUsage(db) {
|
|
1140
|
-
if (db
|
|
1124
|
+
if (!tableIsEmpty(db, "t_event_usage")) return;
|
|
1141
1125
|
db.exec(`INSERT OR IGNORE INTO t_event_usage (
|
|
1142
|
-
f_event_id, f_created_at, f_provider, f_model,
|
|
1126
|
+
f_event_id, f_created_at, f_day, f_provider, f_model, f_referenced, f_subagent,
|
|
1143
1127
|
f_input_tokens, f_output_tokens, f_cache_read_tokens, f_reasoning_tokens, f_total_tokens)
|
|
1144
1128
|
SELECT e.f_event_id, e.f_created_at,
|
|
1129
|
+
date(e.f_created_at / 1000, 'unixepoch', 'localtime'),
|
|
1145
1130
|
coalesce(json_extract(e.f_data, '$.data.message.source.provider'),
|
|
1146
1131
|
json_extract(e.f_data, '$.message.source.provider')),
|
|
1147
1132
|
coalesce(json_extract(e.f_data, '$.data.message.source.model'),
|
|
1148
1133
|
json_extract(e.f_data, '$.message.source.model')),
|
|
1134
|
+
${withEvent(EVENT_REFERENCED_SQL, "e.f_event_id")},
|
|
1135
|
+
${withEvent(EVENT_SUBAGENT_SQL, "e.f_event_id")},
|
|
1149
1136
|
coalesce(json_extract(e.f_data, '$.data.usage.inputTokens'),
|
|
1150
1137
|
json_extract(e.f_data, '$.usage.inputTokens'), 0),
|
|
1151
1138
|
coalesce(json_extract(e.f_data, '$.data.usage.outputTokens'),
|
|
@@ -1161,6 +1148,35 @@ function backfillEventUsage(db) {
|
|
|
1161
1148
|
AND (json_extract(e.f_data, '$.data.usage') IS NOT NULL
|
|
1162
1149
|
OR json_extract(e.f_data, '$.usage') IS NOT NULL)`);
|
|
1163
1150
|
}
|
|
1151
|
+
/** 会话 × 本地日 × 模型的 token 汇总:从桥接行 × 用量行重算(含 fork 继承前缀)。 */
|
|
1152
|
+
function backfillSessionUsage(db) {
|
|
1153
|
+
if (!tableIsEmpty(db, "t_session_usage")) return;
|
|
1154
|
+
db.exec(`INSERT OR IGNORE INTO t_session_usage (
|
|
1155
|
+
f_session_id, f_day, f_provider, f_model,
|
|
1156
|
+
f_input_tokens, f_output_tokens, f_cache_read_tokens, f_reasoning_tokens, f_total_tokens)
|
|
1157
|
+
SELECT b.f_session_id, u.f_day, coalesce(u.f_provider, ''), coalesce(u.f_model, ''),
|
|
1158
|
+
sum(u.f_input_tokens), sum(u.f_output_tokens), sum(u.f_cache_read_tokens),
|
|
1159
|
+
sum(u.f_reasoning_tokens), sum(u.f_total_tokens)
|
|
1160
|
+
FROM t_session_events b
|
|
1161
|
+
JOIN t_event_usage u ON u.f_event_id = b.f_event_id
|
|
1162
|
+
GROUP BY b.f_session_id, u.f_day, coalesce(u.f_provider, ''), coalesce(u.f_model, '')`);
|
|
1163
|
+
}
|
|
1164
|
+
/** 会话 × 本地日的活动计数:从桥接行 × 事件类型重算。 */
|
|
1165
|
+
function backfillSessionCounts(db) {
|
|
1166
|
+
if (!tableIsEmpty(db, "t_session_counts")) return;
|
|
1167
|
+
db.exec(`INSERT OR IGNORE INTO t_session_counts (
|
|
1168
|
+
f_session_id, f_day, f_turns, f_steps, f_user_inputs, f_tool_calls)
|
|
1169
|
+
SELECT b.f_session_id,
|
|
1170
|
+
date(e.f_created_at / 1000, 'unixepoch', 'localtime'),
|
|
1171
|
+
sum(CASE WHEN e.f_type = 'turn/start' THEN 1 ELSE 0 END),
|
|
1172
|
+
sum(CASE WHEN e.f_type = 'step/start' THEN 1 ELSE 0 END),
|
|
1173
|
+
sum(CASE WHEN e.f_type = 'user/message' THEN 1 ELSE 0 END),
|
|
1174
|
+
sum(CASE WHEN e.f_type = 'tool/call' THEN 1 ELSE 0 END)
|
|
1175
|
+
FROM t_session_events b
|
|
1176
|
+
JOIN t_events e ON e.f_event_id = b.f_event_id
|
|
1177
|
+
WHERE e.f_type IN (${COUNTED_EVENT_TYPE_SQL})
|
|
1178
|
+
GROUP BY b.f_session_id, 2`);
|
|
1179
|
+
}
|
|
1164
1180
|
function sessionListRowOf(row) {
|
|
1165
1181
|
return {
|
|
1166
1182
|
sessionId: typeof row["session_id"] === "string" ? row["session_id"] : String(row["session_id"]),
|
|
@@ -1184,6 +1200,15 @@ function tokenTotalsOf(row) {
|
|
|
1184
1200
|
totalTokens: row.total_tokens ?? 0
|
|
1185
1201
|
};
|
|
1186
1202
|
}
|
|
1203
|
+
/** `t_session_counts` 的原始行(列名是 SQL 别名)折成四项活动计数。 */
|
|
1204
|
+
function activityTotalsOf(row) {
|
|
1205
|
+
return {
|
|
1206
|
+
turns: row.turns ?? 0,
|
|
1207
|
+
steps: row.steps ?? 0,
|
|
1208
|
+
userInputs: row.user_inputs ?? 0,
|
|
1209
|
+
toolCalls: row.tool_calls ?? 0
|
|
1210
|
+
};
|
|
1211
|
+
}
|
|
1187
1212
|
/** 一行的整套指标:token 用量 + 活动计数(轮次 / 步骤 / 用户输入 / 工具调用)。 */
|
|
1188
1213
|
function emptyActivity() {
|
|
1189
1214
|
return {
|
|
@@ -1193,17 +1218,10 @@ function emptyActivity() {
|
|
|
1193
1218
|
toolCalls: 0
|
|
1194
1219
|
};
|
|
1195
1220
|
}
|
|
1196
|
-
/** 有 token
|
|
1221
|
+
/** 有 token 用量的会话:活动计数与列表都限定在这一批会话里,口径一致(读汇总表,几百行)。 */
|
|
1197
1222
|
function hasUsageSession(column) {
|
|
1198
|
-
return `EXISTS (SELECT 1 FROM
|
|
1199
|
-
JOIN t_event_usage uu ON uu.f_event_id = ub.f_event_id
|
|
1200
|
-
WHERE ub.f_session_id = ${column})`;
|
|
1223
|
+
return `EXISTS (SELECT 1 FROM t_session_usage ub WHERE ub.f_session_id = ${column})`;
|
|
1201
1224
|
}
|
|
1202
|
-
const SUBAGENT_OF_EVENT = `EXISTS (SELECT 1 FROM t_session_events sb
|
|
1203
|
-
JOIN t_sessions ss ON ss.f_session_id = sb.f_session_id
|
|
1204
|
-
WHERE sb.f_event_id = %EVENT% AND ss.f_origin = 'subagent')`;
|
|
1205
|
-
/** `IN (...)` 用的类型字面量(与 `COUNTED_EVENT_TYPES` 同源)。 */
|
|
1206
|
-
const COUNTED_EVENT_TYPE_SQL = COUNTED_EVENT_TYPES.map((type) => `'${type}'`).join(", ");
|
|
1207
1225
|
const sqliteMigrationsDir = fileURLToPath(new URL("../drizzle/sqlite/", import.meta.url));
|
|
1208
1226
|
const sqliteTxQueues = /* @__PURE__ */ new Map();
|
|
1209
1227
|
function enqueueSqliteTx(path, fn) {
|
|
@@ -1226,9 +1244,7 @@ async function openDatabase(path, journalMode, busyTimeout = DEFAULT_BUSY_TIMEOU
|
|
|
1226
1244
|
const { user_version: onDisk } = db.prepare("PRAGMA user_version").get();
|
|
1227
1245
|
if (onDisk === 2 && !hasMigrationsTable(db)) await baselineV2(db);
|
|
1228
1246
|
migrate(dbx, { migrationsFolder: sqliteMigrationsDir });
|
|
1229
|
-
|
|
1230
|
-
backfillEventCounts(db);
|
|
1231
|
-
backfillEventUsage(db);
|
|
1247
|
+
backfillUsageTables(db);
|
|
1232
1248
|
dbx.insert(tPersistenceState).values({
|
|
1233
1249
|
fSingleton: 1,
|
|
1234
1250
|
fStoreId: randomUUID()
|
|
@@ -1399,7 +1415,7 @@ var SqliteBackend = class SqliteBackend {
|
|
|
1399
1415
|
getHead: (id) => this.getHead(id),
|
|
1400
1416
|
getSeedLength: (id) => this.getSeedLength(id),
|
|
1401
1417
|
updateSeedLength: (id, seedLength) => this.updateSeedLength(id, seedLength),
|
|
1402
|
-
insertEvents: (events) => this.insertEvents(events),
|
|
1418
|
+
insertEvents: (events, usageRows) => this.insertEvents(events, usageRows),
|
|
1403
1419
|
insertBridges: (rows) => this.insertBridges(rows),
|
|
1404
1420
|
updateHead: (id, headEventId, headSequence) => this.updateHead(id, headEventId, headSequence),
|
|
1405
1421
|
bumpRevision: (id, lastEventAt) => this.bumpRevision(id, lastEventAt),
|
|
@@ -1432,14 +1448,10 @@ var SqliteBackend = class SqliteBackend {
|
|
|
1432
1448
|
this.db.update(tSessions).set({ fSeedLength: seedLength }).where(eq(tSessions.fSessionId, id)).run();
|
|
1433
1449
|
}
|
|
1434
1450
|
static INSERT_BATCH_ROWS = 1e3;
|
|
1435
|
-
async insertEvents(events) {
|
|
1451
|
+
async insertEvents(events, usageRows) {
|
|
1436
1452
|
if (events.length === 0) return;
|
|
1437
1453
|
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();
|
|
1438
|
-
|
|
1439
|
-
const row = usageRowOf(event);
|
|
1440
|
-
return row === void 0 ? [] : [row];
|
|
1441
|
-
});
|
|
1442
|
-
if (usage.length > 0) this.db.insert(tEventUsage).values(usage).onConflictDoNothing().run();
|
|
1454
|
+
if (usageRows.length > 0) this.db.insert(tEventUsage).values(usageRows.map((row) => ({ ...row }))).onConflictDoNothing().run();
|
|
1443
1455
|
}
|
|
1444
1456
|
async insertBridges(rows) {
|
|
1445
1457
|
if (rows.length === 0) return;
|
|
@@ -1549,41 +1561,49 @@ var SqliteBackend = class SqliteBackend {
|
|
|
1549
1561
|
total: Number(counted.n)
|
|
1550
1562
|
};
|
|
1551
1563
|
}
|
|
1564
|
+
/**
|
|
1565
|
+
* 用量聚合:只读三张派生统计表——事件级用量行给去重口径的总量与「天 × 模型」桶,
|
|
1566
|
+
* `t_session_usage` / `t_session_counts` 给按会话的行(含 fork 继承前缀)。
|
|
1567
|
+
* 归属、本地日与「是否被引用」都是物化列,这里不再现算、也不回连事件表。
|
|
1568
|
+
* 事件级按毫秒时间戳过滤、汇总表按本地日过滤——`sinceMs` 对齐到本地零点时两者等价。
|
|
1569
|
+
*/
|
|
1552
1570
|
async usageReport(sinceMs) {
|
|
1553
|
-
const usageSince = sinceMs === void 0 ? "" : " AND
|
|
1571
|
+
const usageSince = sinceMs === void 0 ? "" : " AND f_created_at >= ?";
|
|
1554
1572
|
const params = sinceMs === void 0 ? [] : [sinceMs];
|
|
1555
|
-
const daySince = sinceMs === void 0 ? "" : " AND
|
|
1573
|
+
const daySince = sinceMs === void 0 ? "" : " AND %COL% >= ?";
|
|
1556
1574
|
const dayParams = sinceMs === void 0 ? [] : [localDayKey(sinceMs)];
|
|
1557
|
-
const
|
|
1558
|
-
const buckets = this.db.$client.prepare(`SELECT
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
sum(
|
|
1563
|
-
sum(
|
|
1564
|
-
sum(
|
|
1565
|
-
sum(
|
|
1566
|
-
sum(
|
|
1567
|
-
FROM t_event_usage
|
|
1568
|
-
WHERE
|
|
1569
|
-
GROUP BY
|
|
1570
|
-
const tokenScope = this.db.$client.prepare(`SELECT
|
|
1571
|
-
sum(
|
|
1572
|
-
sum(
|
|
1573
|
-
sum(
|
|
1574
|
-
sum(
|
|
1575
|
-
sum(
|
|
1576
|
-
FROM t_event_usage
|
|
1577
|
-
WHERE
|
|
1578
|
-
GROUP BY
|
|
1575
|
+
const dayFilter = (column) => daySince.replace("%COL%", column);
|
|
1576
|
+
const buckets = this.db.$client.prepare(`SELECT f_day AS day,
|
|
1577
|
+
f_provider AS provider,
|
|
1578
|
+
f_model AS model,
|
|
1579
|
+
f_subagent AS subagent,
|
|
1580
|
+
sum(f_input_tokens) AS input_tokens,
|
|
1581
|
+
sum(f_output_tokens) AS output_tokens,
|
|
1582
|
+
sum(f_cache_read_tokens) AS cache_read_tokens,
|
|
1583
|
+
sum(f_reasoning_tokens) AS reasoning_tokens,
|
|
1584
|
+
sum(f_total_tokens) AS total_tokens
|
|
1585
|
+
FROM t_event_usage
|
|
1586
|
+
WHERE f_referenced = 1${usageSince}
|
|
1587
|
+
GROUP BY f_day, f_provider, f_model, f_subagent`).all(...params);
|
|
1588
|
+
const tokenScope = this.db.$client.prepare(`SELECT f_subagent AS subagent,
|
|
1589
|
+
sum(f_input_tokens) AS input_tokens,
|
|
1590
|
+
sum(f_output_tokens) AS output_tokens,
|
|
1591
|
+
sum(f_cache_read_tokens) AS cache_read_tokens,
|
|
1592
|
+
sum(f_reasoning_tokens) AS reasoning_tokens,
|
|
1593
|
+
sum(f_total_tokens) AS total_tokens
|
|
1594
|
+
FROM t_event_usage
|
|
1595
|
+
WHERE f_referenced = 1${usageSince}
|
|
1596
|
+
GROUP BY f_subagent`).all(...params);
|
|
1579
1597
|
const activityScope = this.db.$client.prepare(`SELECT (s.f_origin = 'subagent') AS subagent,
|
|
1580
|
-
c.
|
|
1581
|
-
sum(c.
|
|
1582
|
-
|
|
1598
|
+
sum(c.f_turns) AS turns,
|
|
1599
|
+
sum(c.f_steps) AS steps,
|
|
1600
|
+
sum(c.f_user_inputs) AS user_inputs,
|
|
1601
|
+
sum(c.f_tool_calls) AS tool_calls
|
|
1602
|
+
FROM t_session_counts c
|
|
1583
1603
|
JOIN t_sessions s ON s.f_session_id = c.f_session_id
|
|
1584
|
-
WHERE ${hasUsageSession("c.f_session_id")}${
|
|
1585
|
-
GROUP BY subagent
|
|
1586
|
-
const tokenSessions = this.db.$client.prepare(`SELECT
|
|
1604
|
+
WHERE ${hasUsageSession("c.f_session_id")}${dayFilter("c.f_day")}
|
|
1605
|
+
GROUP BY subagent`).all(...dayParams);
|
|
1606
|
+
const tokenSessions = this.db.$client.prepare(`SELECT u.f_session_id AS session_id,
|
|
1587
1607
|
s.f_title AS title,
|
|
1588
1608
|
(s.f_origin = 'subagent') AS subagent,
|
|
1589
1609
|
(s.f_archived_at IS NOT NULL) AS archived,
|
|
@@ -1592,28 +1612,25 @@ var SqliteBackend = class SqliteBackend {
|
|
|
1592
1612
|
sum(u.f_cache_read_tokens) AS cache_read_tokens,
|
|
1593
1613
|
sum(u.f_reasoning_tokens) AS reasoning_tokens,
|
|
1594
1614
|
sum(u.f_total_tokens) AS total_tokens
|
|
1595
|
-
FROM
|
|
1596
|
-
JOIN
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
sum(
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1615
|
+
FROM t_session_usage u
|
|
1616
|
+
JOIN t_sessions s ON s.f_session_id = u.f_session_id
|
|
1617
|
+
WHERE 1 = 1${dayFilter("u.f_day")}
|
|
1618
|
+
GROUP BY u.f_session_id, s.f_title, s.f_origin, s.f_archived_at`).all(...dayParams);
|
|
1619
|
+
const activitySessions = this.db.$client.prepare(`SELECT f_session_id AS session_id,
|
|
1620
|
+
sum(f_turns) AS turns,
|
|
1621
|
+
sum(f_steps) AS steps,
|
|
1622
|
+
sum(f_user_inputs) AS user_inputs,
|
|
1623
|
+
sum(f_tool_calls) AS tool_calls
|
|
1624
|
+
FROM t_session_counts
|
|
1625
|
+
WHERE 1 = 1${dayFilter("f_day")}
|
|
1626
|
+
GROUP BY f_session_id`).all(...dayParams);
|
|
1606
1627
|
const human = emptyTotals();
|
|
1607
1628
|
const subagent = emptyTotals();
|
|
1608
1629
|
for (const row of tokenScope) addTokenTotals(row.subagent === 1 ? subagent : human, tokenTotalsOf(row));
|
|
1609
|
-
for (const row of activityScope)
|
|
1630
|
+
for (const row of activityScope) addActivityTotals(row.subagent === 1 ? subagent : human, activityTotalsOf(row));
|
|
1610
1631
|
const totals = addTotals(addTotals(emptyTotals(), human), subagent);
|
|
1611
1632
|
const activityBySession = /* @__PURE__ */ new Map();
|
|
1612
|
-
for (const row of activitySessions)
|
|
1613
|
-
const target = activityBySession.get(row.session_id) ?? emptyActivity();
|
|
1614
|
-
addActivityCount(target, row.f_type, row.n ?? 0);
|
|
1615
|
-
activityBySession.set(row.session_id, target);
|
|
1616
|
-
}
|
|
1633
|
+
for (const row of activitySessions) activityBySession.set(row.session_id, activityTotalsOf(row));
|
|
1617
1634
|
return {
|
|
1618
1635
|
totals,
|
|
1619
1636
|
subagent,
|
|
@@ -1635,28 +1652,72 @@ var SqliteBackend = class SqliteBackend {
|
|
|
1635
1652
|
}))
|
|
1636
1653
|
};
|
|
1637
1654
|
}
|
|
1638
|
-
/**
|
|
1639
|
-
async
|
|
1655
|
+
/** token 用量旁路累加:派生表 `t_session_usage`,不在写事务里(失败可丢,表可销毁重建)。 */
|
|
1656
|
+
async incrementSessionUsage(id, buckets) {
|
|
1657
|
+
if (buckets.length === 0) return;
|
|
1658
|
+
const statement = this.db.$client.prepare(`INSERT INTO t_session_usage (f_session_id, f_day, f_provider, f_model,
|
|
1659
|
+
f_input_tokens, f_output_tokens, f_cache_read_tokens, f_reasoning_tokens, f_total_tokens)
|
|
1660
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
1661
|
+
ON CONFLICT(f_session_id, f_day, f_provider, f_model) DO UPDATE SET
|
|
1662
|
+
f_input_tokens = f_input_tokens + excluded.f_input_tokens,
|
|
1663
|
+
f_output_tokens = f_output_tokens + excluded.f_output_tokens,
|
|
1664
|
+
f_cache_read_tokens = f_cache_read_tokens + excluded.f_cache_read_tokens,
|
|
1665
|
+
f_reasoning_tokens = f_reasoning_tokens + excluded.f_reasoning_tokens,
|
|
1666
|
+
f_total_tokens = f_total_tokens + excluded.f_total_tokens`);
|
|
1667
|
+
for (const bucket of buckets) statement.run(id, bucket.day, bucket.provider ?? "", bucket.model ?? "", bucket.inputTokens, bucket.outputTokens, bucket.cacheReadTokens, bucket.reasoningTokens, bucket.totalTokens);
|
|
1668
|
+
}
|
|
1669
|
+
/** 活动计数旁路累加:派生表 `t_session_counts`,同上(四项计数一次落)。 */
|
|
1670
|
+
async incrementSessionCounts(id, buckets) {
|
|
1640
1671
|
if (buckets.length === 0) return;
|
|
1641
|
-
const statement = this.db.$client.prepare(`INSERT INTO
|
|
1642
|
-
|
|
1643
|
-
|
|
1672
|
+
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)
|
|
1673
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
1674
|
+
ON CONFLICT(f_session_id, f_day) DO UPDATE SET
|
|
1675
|
+
f_turns = f_turns + excluded.f_turns,
|
|
1676
|
+
f_steps = f_steps + excluded.f_steps,
|
|
1677
|
+
f_user_inputs = f_user_inputs + excluded.f_user_inputs,
|
|
1678
|
+
f_tool_calls = f_tool_calls + excluded.f_tool_calls`);
|
|
1679
|
+
for (const bucket of buckets) statement.run(id, bucket.day, bucket.turns, bucket.steps, bucket.userInputs, bucket.toolCalls);
|
|
1644
1680
|
}
|
|
1645
|
-
/**
|
|
1646
|
-
async
|
|
1647
|
-
this.db.$client.prepare("DELETE FROM
|
|
1648
|
-
this.db.$client.prepare(`INSERT INTO
|
|
1681
|
+
/** 按会话重算两张汇总表(rewind / fork 之后):先删该会话的行,再从事件表重算。 */
|
|
1682
|
+
async rebuildSessionStats(id) {
|
|
1683
|
+
this.db.$client.prepare("DELETE FROM t_session_usage WHERE f_session_id = ?").run(id);
|
|
1684
|
+
this.db.$client.prepare(`INSERT INTO t_session_usage (
|
|
1685
|
+
f_session_id, f_day, f_provider, f_model,
|
|
1686
|
+
f_input_tokens, f_output_tokens, f_cache_read_tokens, f_reasoning_tokens, f_total_tokens)
|
|
1687
|
+
SELECT b.f_session_id, u.f_day, coalesce(u.f_provider, ''), coalesce(u.f_model, ''),
|
|
1688
|
+
sum(u.f_input_tokens), sum(u.f_output_tokens), sum(u.f_cache_read_tokens),
|
|
1689
|
+
sum(u.f_reasoning_tokens), sum(u.f_total_tokens)
|
|
1690
|
+
FROM t_session_events b
|
|
1691
|
+
JOIN t_event_usage u ON u.f_event_id = b.f_event_id
|
|
1692
|
+
WHERE b.f_session_id = ?
|
|
1693
|
+
GROUP BY b.f_session_id, u.f_day, coalesce(u.f_provider, ''), coalesce(u.f_model, '')`).run(id);
|
|
1694
|
+
this.db.$client.prepare("DELETE FROM t_session_counts WHERE f_session_id = ?").run(id);
|
|
1695
|
+
this.db.$client.prepare(`INSERT INTO t_session_counts (
|
|
1696
|
+
f_session_id, f_day, f_turns, f_steps, f_user_inputs, f_tool_calls)
|
|
1649
1697
|
SELECT b.f_session_id,
|
|
1650
1698
|
date(e.f_created_at / 1000, 'unixepoch', 'localtime'),
|
|
1651
|
-
e.f_type,
|
|
1652
|
-
|
|
1699
|
+
sum(CASE WHEN e.f_type = 'turn/start' THEN 1 ELSE 0 END),
|
|
1700
|
+
sum(CASE WHEN e.f_type = 'step/start' THEN 1 ELSE 0 END),
|
|
1701
|
+
sum(CASE WHEN e.f_type = 'user/message' THEN 1 ELSE 0 END),
|
|
1702
|
+
sum(CASE WHEN e.f_type = 'tool/call' THEN 1 ELSE 0 END)
|
|
1653
1703
|
FROM t_session_events b
|
|
1654
1704
|
JOIN t_events e ON e.f_event_id = b.f_event_id
|
|
1655
1705
|
WHERE b.f_session_id = ? AND e.f_type IN (${COUNTED_EVENT_TYPE_SQL})
|
|
1656
|
-
GROUP BY
|
|
1706
|
+
GROUP BY b.f_session_id, 2`).run(id);
|
|
1707
|
+
}
|
|
1708
|
+
/**
|
|
1709
|
+
* 全量重算用量行的引用标记:rewind 截断 / fork 复用 / 会话删除都会让「引用」跨会话变化,
|
|
1710
|
+
* 只按本会话判定不够。低频路径,一次全表 UPDATE(3 万行量级)。
|
|
1711
|
+
*/
|
|
1712
|
+
async refreshEventUsageFlags() {
|
|
1713
|
+
const eventId = "t_event_usage.f_event_id";
|
|
1714
|
+
this.db.$client.exec(`UPDATE t_event_usage SET
|
|
1715
|
+
f_referenced = ${withEvent(EVENT_REFERENCED_SQL, eventId)},
|
|
1716
|
+
f_subagent = ${withEvent(EVENT_SUBAGENT_SQL, eventId)}`);
|
|
1657
1717
|
}
|
|
1658
|
-
async
|
|
1659
|
-
this.db.$client.prepare("DELETE FROM
|
|
1718
|
+
async deleteSessionStats(id) {
|
|
1719
|
+
this.db.$client.prepare("DELETE FROM t_session_usage WHERE f_session_id = ?").run(id);
|
|
1720
|
+
this.db.$client.prepare("DELETE FROM t_session_counts WHERE f_session_id = ?").run(id);
|
|
1660
1721
|
}
|
|
1661
1722
|
async vacuum() {
|
|
1662
1723
|
await enqueueSqliteTx(this.dbPath, async () => {
|
|
@@ -1679,4 +1740,4 @@ var SqliteBackend = class SqliteBackend {
|
|
|
1679
1740
|
}
|
|
1680
1741
|
};
|
|
1681
1742
|
//#endregion
|
|
1682
|
-
export {
|
|
1743
|
+
export { postgresTableDefs as C, tWorkspaces as S, WriteGuard as T, tSessionUsage as _, EVENT_ENCODING as a, tWorkspaceSessions as b, eventDimensions as c, tEvents as d, tPersistenceState as f, tSessionProjcacheRows as g, tSessionEvents as h, DEFAULT_BUSY_TIMEOUT_MS as i, eventKind as l, tSessionCounts as m, openDatabase as n, SCHEMA_VERSION as o, tSchemaMeta as p, createStorageRepository as r, SESSION_PERSISTENCE_SQLITE_APPLICATION_ID as s, SqliteBackend as t, tEventUsage as u, tSessions as v, toPostgresSchema as w, tWorkspaceState as x, tStorageUnits as y };
|