@morlay/session-rdb 0.0.21-alpha.6 → 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 +11 -11
- package/drizzle/postgres/20260923120000_v3_usage_materialized/migration.sql +48 -0
- package/drizzle/sqlite/20260923120000_v3_usage_materialized/migration.sql +48 -0
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +25 -23
- 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,5 +1,156 @@
|
|
|
1
1
|
import { SessionSeq } from "@deepseek-ai/dsh-session";
|
|
2
2
|
import { sessionFormatCatalog } from "@deepseek-ai/dsh-session-format-catalog";
|
|
3
|
+
//#region src/usage.ts
|
|
4
|
+
const SESSION_USAGE_PATH = "/api/session.usage";
|
|
5
|
+
const ROLLING_DAYS = {
|
|
6
|
+
"7d": 7,
|
|
7
|
+
"30d": 30,
|
|
8
|
+
"90d": 90
|
|
9
|
+
};
|
|
10
|
+
function localDayStart(now) {
|
|
11
|
+
const start = new Date(now);
|
|
12
|
+
start.setHours(0, 0, 0, 0);
|
|
13
|
+
return start;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* 范围起点(含),不限时为 undefined。**所有起点都对齐到 host 本地时区的零点**——`day` / `week` 是
|
|
17
|
+
* 今天 / 本周一的零点,`7d` / `30d` / `90d` 是「今天零点往前 N-1 天」(含今天共 N 个自然日)。
|
|
18
|
+
* 对齐的意义:事件级表按毫秒过滤、会话汇总表按本地日过滤,两者因此严格等价。
|
|
19
|
+
* @param range - 语义键。
|
|
20
|
+
* @param now - 当前时刻。
|
|
21
|
+
* @returns 起点毫秒时间戳,或 undefined。
|
|
22
|
+
*/
|
|
23
|
+
function resolveUsageSince(range, now) {
|
|
24
|
+
switch (range) {
|
|
25
|
+
case "all": return;
|
|
26
|
+
case "day": return localDayStart(now).getTime();
|
|
27
|
+
case "week": {
|
|
28
|
+
const start = localDayStart(now);
|
|
29
|
+
start.setDate(start.getDate() - (start.getDay() + 6) % 7);
|
|
30
|
+
return start.getTime();
|
|
31
|
+
}
|
|
32
|
+
default: {
|
|
33
|
+
const start = localDayStart(now);
|
|
34
|
+
start.setDate(start.getDate() - (ROLLING_DAYS[range] - 1));
|
|
35
|
+
return start.getTime();
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
function parseUsageRange(value) {
|
|
40
|
+
return value === "day" || value === "week" || value === "7d" || value === "30d" || value === "90d" ? value : "all";
|
|
41
|
+
}
|
|
42
|
+
/** 计入活动计数的事件类型(轮次 / 步骤 / 用户输入 / 工具调用)。 */
|
|
43
|
+
const COUNTED_EVENT_TYPES = [
|
|
44
|
+
"turn/start",
|
|
45
|
+
"step/start",
|
|
46
|
+
"user/message",
|
|
47
|
+
"tool/call"
|
|
48
|
+
];
|
|
49
|
+
/** 按事件类型把计数累加到活动指标上(表里按类型存,读的时候折成四项)。 */
|
|
50
|
+
function addActivityCount(target, type, count) {
|
|
51
|
+
switch (type) {
|
|
52
|
+
case "turn/start":
|
|
53
|
+
target.turns += count;
|
|
54
|
+
return target;
|
|
55
|
+
case "step/start":
|
|
56
|
+
target.steps += count;
|
|
57
|
+
return target;
|
|
58
|
+
case "user/message":
|
|
59
|
+
target.userInputs += count;
|
|
60
|
+
return target;
|
|
61
|
+
case "tool/call":
|
|
62
|
+
target.toolCalls += count;
|
|
63
|
+
return target;
|
|
64
|
+
default: return target;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
/** 毫秒时间戳 → host 本地日(`YYYY-MM-DD`,与 SQL 的 localtime 口径一致)。 */
|
|
68
|
+
function localDayKey(ms) {
|
|
69
|
+
const date = new Date(ms);
|
|
70
|
+
const pad = (value) => String(value).padStart(2, "0");
|
|
71
|
+
return `${String(date.getFullYear())}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`;
|
|
72
|
+
}
|
|
73
|
+
function emptyTotals() {
|
|
74
|
+
return {
|
|
75
|
+
turns: 0,
|
|
76
|
+
steps: 0,
|
|
77
|
+
userInputs: 0,
|
|
78
|
+
toolCalls: 0,
|
|
79
|
+
inputTokens: 0,
|
|
80
|
+
outputTokens: 0,
|
|
81
|
+
cacheReadTokens: 0,
|
|
82
|
+
reasoningTokens: 0,
|
|
83
|
+
totalTokens: 0
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
function addTokenTotals(target, row) {
|
|
87
|
+
target.inputTokens += row.inputTokens;
|
|
88
|
+
target.outputTokens += row.outputTokens;
|
|
89
|
+
target.cacheReadTokens += row.cacheReadTokens;
|
|
90
|
+
target.reasoningTokens += row.reasoningTokens;
|
|
91
|
+
target.totalTokens += row.totalTokens;
|
|
92
|
+
return target;
|
|
93
|
+
}
|
|
94
|
+
function addActivityTotals(target, row) {
|
|
95
|
+
target.turns += row.turns;
|
|
96
|
+
target.steps += row.steps;
|
|
97
|
+
target.userInputs += row.userInputs;
|
|
98
|
+
target.toolCalls += row.toolCalls;
|
|
99
|
+
return target;
|
|
100
|
+
}
|
|
101
|
+
/** 把一行的整套指标累加进目标(后端合并「是否子代理」两组时用)。 */
|
|
102
|
+
function addTotals(target, row) {
|
|
103
|
+
addActivityTotals(target, row);
|
|
104
|
+
addTokenTotals(target, row);
|
|
105
|
+
return target;
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* 用量统计通道:一次请求回报总览(含 subagent 拆分)、按天 × 模型的桶与按会话的行。
|
|
109
|
+
* 聚合只算被会话引用的事件行——fork 共享行因此只计一次,已删会话留下的孤儿行不计。
|
|
110
|
+
*/
|
|
111
|
+
function registerSessionUsage(ctx, persistence) {
|
|
112
|
+
ctx.inject(["webServer", "connection"], (webCtx) => {
|
|
113
|
+
const webServer = webCtx.webServer;
|
|
114
|
+
const connection = webCtx.get("connection");
|
|
115
|
+
return webCtx.effect(() => webServer.register({
|
|
116
|
+
kind: "exact",
|
|
117
|
+
path: SESSION_USAGE_PATH,
|
|
118
|
+
handler: async (req, res) => {
|
|
119
|
+
const rejection = connection.requestRejection(req);
|
|
120
|
+
if (rejection !== void 0) {
|
|
121
|
+
res.writeHead(rejection);
|
|
122
|
+
res.end(rejection === 401 ? "unauthorized" : "forbidden");
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
if (req.method !== "POST") {
|
|
126
|
+
res.writeHead(405, { "content-type": "application/json" });
|
|
127
|
+
res.end(JSON.stringify({ error: "method not allowed" }));
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
const chunks = [];
|
|
131
|
+
for await (const chunk of req) chunks.push(chunk);
|
|
132
|
+
const raw = Buffer.concat(chunks).toString("utf8");
|
|
133
|
+
let envelope = {};
|
|
134
|
+
if (raw !== "") try {
|
|
135
|
+
envelope = JSON.parse(raw);
|
|
136
|
+
} catch {
|
|
137
|
+
res.writeHead(400, { "content-type": "application/json" });
|
|
138
|
+
res.end(JSON.stringify({ error: "request body is not JSON" }));
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
try {
|
|
142
|
+
const report = await persistence.usageReport(resolveUsageSince(parseUsageRange(envelope.range), Date.now()));
|
|
143
|
+
res.writeHead(200, { "content-type": "application/json" });
|
|
144
|
+
res.end(JSON.stringify(report));
|
|
145
|
+
} catch (error) {
|
|
146
|
+
res.writeHead(500, { "content-type": "application/json" });
|
|
147
|
+
res.end(JSON.stringify({ error: error instanceof Error ? error.message : "usage report failed" }));
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}), `session-rdb: ${SESSION_USAGE_PATH} route`);
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
//#endregion
|
|
3
154
|
//#region src/log.ts
|
|
4
155
|
function normalizeSurfaceOp(value) {
|
|
5
156
|
if (typeof value !== "object" || value === null || Array.isArray(value)) return value;
|
|
@@ -354,15 +505,15 @@ function renameLegacyPtcEvents(events) {
|
|
|
354
505
|
* (v4 起 system 消息只认这个 kind);
|
|
355
506
|
* - `tool/result` 的消息:v3 是 user 角色、结果块包在 `content[0]`;v4 是 `role: 'tool'` + 顶层
|
|
356
507
|
* `toolCallId` + `content` 就是结果块;
|
|
357
|
-
* -
|
|
358
|
-
*
|
|
508
|
+
* - **本仓库自造、上游不认识的事件类型**(见 `OWN_EVENT_TYPES`:历史遗留的与我们当前在写的)标
|
|
509
|
+
* `ignorable: true`——v4 的校验对未知类型只接受可忽略信封。
|
|
359
510
|
*
|
|
360
511
|
* 只认这张白名单:**其它未知类型保持 fail loud**(那是「数据来自更新的 harness」的信号,不能吞掉)。
|
|
361
512
|
*/
|
|
362
513
|
function normalizeToCurrentShape(events) {
|
|
363
514
|
for (const event of events) {
|
|
364
515
|
const type = event.type;
|
|
365
|
-
if (
|
|
516
|
+
if (OWN_EVENT_TYPES.has(type)) {
|
|
366
517
|
event.ignorable = true;
|
|
367
518
|
continue;
|
|
368
519
|
}
|
|
@@ -405,16 +556,36 @@ function liftToolResultMessage(message) {
|
|
|
405
556
|
};
|
|
406
557
|
}
|
|
407
558
|
/**
|
|
408
|
-
*
|
|
409
|
-
*
|
|
559
|
+
* 落库前给**本仓库自造的事件类型**补上 `ignorable: true` 信封。
|
|
560
|
+
*
|
|
561
|
+
* 上游的持久化校验(`validateStoredEvents`)只放行「已知类型」或「带可忽略信封的未知类型」,我们的类型
|
|
562
|
+
* 按构造就在已知表之外——不补的话连**写入**都会被拒(`appendBatch` 里那道校验)。上游 `Session.append`
|
|
563
|
+
* 没有承载这个信封的口子(第三个参数只服务 surface 事件),所以这一笔由持久化层补:canonical log 里的
|
|
564
|
+
* 形状不变,落库的那一份带上信封。
|
|
565
|
+
* @param events - 即将落库的批次(`materializeAppendBatch` 的 JSON 快照副本,可写)。
|
|
566
|
+
*/
|
|
567
|
+
function sealOwnEvents(events) {
|
|
568
|
+
for (const event of events) if (OWN_EVENT_TYPES.has(event.type)) event.ignorable = true;
|
|
569
|
+
}
|
|
570
|
+
/**
|
|
571
|
+
* 本仓库自造、上游不认识的事件类型:读的时候标 `ignorable: true`,v4 的持久化校验才肯跳过它们
|
|
572
|
+
* (`validateStoredEvents` 只放行「已知类型」或「带可忽略信封的未知类型」)。
|
|
573
|
+
*
|
|
574
|
+
* 两类都在这张表里:
|
|
575
|
+
* - **历史遗留**:写过、后来连同机制一起删除的(`session-branch/version`);
|
|
576
|
+
* - **当前在写**:本仓库的插件自己声明的事件类型(`session-mode/selected`)——上游的
|
|
577
|
+
* `KNOWN_SESSION_EVENT_TYPES` 由上游仓库的声明生成,外部插件的事件按构造就在它之外。
|
|
578
|
+
*
|
|
579
|
+
* 新增自造事件类型时**必须**登记到这里,否则读路径 fail loud(真回归:`session-mode/selected` 漏登记,
|
|
580
|
+
* 会话打不开)。**不要**用「所有未知类型」代替(那会掩盖更新版本的未知事件——那种必须拒读)。
|
|
410
581
|
*/
|
|
411
|
-
const
|
|
412
|
-
/**
|
|
413
|
-
*
|
|
414
|
-
function
|
|
582
|
+
const OWN_EVENT_TYPES = /* @__PURE__ */ new Set(["session-branch/version", "session-mode/selected"]);
|
|
583
|
+
/** 这批事件是否需要按当前形状归一(旧代消息形状,或尚未标 `ignorable` 的自造事件类型)——写路径曾把回退
|
|
584
|
+
* 视图的结果以当前版本号落库,所以**版本号不可信**,读之前要按内容判一次。 */
|
|
585
|
+
function needsShapeAdoption(events) {
|
|
415
586
|
for (const event of events) {
|
|
416
587
|
const type = event.type;
|
|
417
|
-
if (
|
|
588
|
+
if (OWN_EVENT_TYPES.has(type)) return true;
|
|
418
589
|
if (type === "system/message") {
|
|
419
590
|
if ((event.data["message"]?.["source"])?.["kind"] === "plugin") return true;
|
|
420
591
|
continue;
|
|
@@ -555,10 +726,13 @@ function textField(value) {
|
|
|
555
726
|
/**
|
|
556
727
|
* 一条事件行的用量(只有带 `usage` 的 `assistant/message` 才有):写路径据此记录
|
|
557
728
|
* `t_event_usage`,统计因此不必逐行解析 JSON。两种 `f_data` 结构(带信封 / 老格式)都认。
|
|
729
|
+
* 读侧维度一起物化:本地日由事件时间算,`f_referenced` 直接 1(这一批事件行与桥接行同事务,
|
|
730
|
+
* 提交即被引用),`f_subagent` 取写入它的会话是否子代理。
|
|
558
731
|
* @param event - 待写入的事件行。
|
|
732
|
+
* @param subagent - 写入它的会话是否 `origin === 'subagent'`。
|
|
559
733
|
* @returns 用量行,或该事件没有用量时的 undefined。
|
|
560
734
|
*/
|
|
561
|
-
function usageRowOf(event) {
|
|
735
|
+
function usageRowOf(event, subagent) {
|
|
562
736
|
if (event.fType !== "assistant/message") return void 0;
|
|
563
737
|
let parsed;
|
|
564
738
|
try {
|
|
@@ -573,8 +747,11 @@ function usageRowOf(event) {
|
|
|
573
747
|
return {
|
|
574
748
|
fEventId: event.fEventId,
|
|
575
749
|
fCreatedAt: event.fCreatedAt,
|
|
750
|
+
fDay: localDayKey(event.fCreatedAt),
|
|
576
751
|
fProvider: textField(source?.["provider"]),
|
|
577
752
|
fModel: textField(source?.["model"]),
|
|
753
|
+
fReferenced: 1,
|
|
754
|
+
fSubagent: subagent ? 1 : 0,
|
|
578
755
|
fInputTokens: numericField(usage["inputTokens"]),
|
|
579
756
|
fOutputTokens: numericField(usage["outputTokens"]),
|
|
580
757
|
fCacheReadTokens: numericField(usage["cacheReadTokens"]),
|
|
@@ -583,4 +760,4 @@ function usageRowOf(event) {
|
|
|
583
760
|
};
|
|
584
761
|
}
|
|
585
762
|
//#endregion
|
|
586
|
-
export {
|
|
763
|
+
export { registerSessionUsage as A, COUNTED_EVENT_TYPES as C, addTotals as D, addTokenTotals as E, emptyTotals as O, usageRowOf as S, addActivityTotals as T, sessionConflictRow as _, orphanInboxSpliceSeqs as a, titleOfEventData as b, repairAssistantSettlement as c, repairRequestHeaders as d, repairSurfaceOps as f, sealOwnEvents as g, scanRows as h, normalizeToCurrentShape as i, localDayKey as k, repairOrphanInboxSplices as l, rowToMeta as m, findSurfaceRepairs as n, recomputeReplaceProvenance as o, rowToEvent as p, needsShapeAdoption as r, renameLegacyPtcEvents as s, OWN_EVENT_TYPES as t, repairReadView as u, sessionInsertRow as v, addActivityCount as w, toJsonlArtifact as x, syncMeteringRanges as y };
|