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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/README.md +17 -10
  2. package/dist/artifact.d.mts +2 -101
  3. package/dist/artifact.mjs +2 -2
  4. package/dist/{branch-BjUR6wfe.mjs → branch-7eOh_tbO.mjs} +3 -3
  5. package/dist/deletion.d.mts +1 -1
  6. package/dist/deletion.mjs +1 -1
  7. package/dist/import.d.mts +1 -1
  8. package/dist/import.mjs +1 -1
  9. package/dist/index.d.mts +1 -1
  10. package/dist/index.mjs +3 -3
  11. package/dist/{backend-E0bmTXpz.d.mts → log-BbleqD1M.d.mts} +171 -24
  12. package/dist/{log-qCNkDjh5.mjs → log-DTcvsMz7.mjs} +189 -12
  13. package/dist/{sqlite-zcXVyMxr.mjs → sqlite-ChM_sHAG.mjs} +320 -259
  14. package/dist/{src-B9X8BKyh.mjs → src-BWqePRA7.mjs} +290 -162
  15. package/dist/storage.d.mts +20 -7
  16. package/dist/storage.mjs +2 -2
  17. package/dist/testing.d.mts +1 -1
  18. package/dist/testing.mjs +1 -1
  19. package/drizzle/postgres/20260923120000_v3_usage_materialized/migration.sql +48 -0
  20. package/drizzle/sqlite/20260923120000_v3_usage_materialized/migration.sql +48 -0
  21. package/package.json +20 -20
  22. package/src/backend.ts +41 -17
  23. package/src/branch.ts +4 -4
  24. package/src/drizzle/postgres-v3.ts +2 -0
  25. package/src/drizzle/sqlite-v3.ts +2 -0
  26. package/src/entities/v3/event-usage.ts +8 -1
  27. package/src/entities/v3/index.ts +8 -0
  28. package/src/entities/v3/session-counts.ts +23 -0
  29. package/src/entities/v3/session-usage.ts +29 -0
  30. package/src/index.ts +109 -43
  31. package/src/log.ts +58 -17
  32. package/src/postgres.ts +248 -149
  33. package/src/rows.ts +2 -1
  34. package/src/schema.ts +4 -0
  35. package/src/sqlite.ts +263 -124
  36. package/src/storage-takeover/repository.ts +1 -4
  37. package/src/usage.ts +10 -6
@@ -1,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
- * - 本仓库自己写过、上游已删除的事件类型(见 `LEGACY_OWN_EVENT_TYPES`)标 `ignorable: true`——v4 的
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 (LEGACY_OWN_EVENT_TYPES.has(type)) {
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
- * 本仓库写过、后来连同机制一起删除的事件类型:读的时候标 `ignorable: true` 让校验跳过它们。
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 LEGACY_OWN_EVENT_TYPES = /* @__PURE__ */ new Set(["session-branch/version"]);
412
- /** 行里是否还带着旧代消息形状(`session-branch/version` 这类自造事件也算)——写路径曾把回退视图的结果
413
- * 以当前版本号落库,所以**版本号不可信**,读之前要按内容判一次。 */
414
- function hasLegacyShape(events) {
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 (LEGACY_OWN_EVENT_TYPES.has(type)) return true;
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 { sessionInsertRow as _, orphanInboxSpliceSeqs as a, toJsonlArtifact as b, repairAssistantSettlement as c, repairRequestHeaders as d, repairSurfaceOps as f, sessionConflictRow as g, scanRows as h, normalizeToCurrentShape as i, repairOrphanInboxSplices as l, rowToMeta as m, findSurfaceRepairs as n, recomputeReplaceProvenance as o, rowToEvent as p, hasLegacyShape as r, renameLegacyPtcEvents as s, LEGACY_OWN_EVENT_TYPES as t, repairReadView as u, syncMeteringRanges as v, usageRowOf as x, titleOfEventData as y };
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 };