@morlay/session-rdb 0.0.21-alpha.7 → 0.0.21-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.
Files changed (45) hide show
  1. package/README.md +31 -12
  2. package/dist/artifact.d.mts +2 -101
  3. package/dist/artifact.mjs +2 -2
  4. package/dist/{branch-BjUR6wfe.mjs → branch-D4nVzQQM.mjs} +3 -3
  5. package/dist/deletion.d.mts +1 -1
  6. package/dist/deletion.mjs +1 -1
  7. package/dist/{dist-vIVO6bA-.mjs → dist-DHer1cql.mjs} +14 -7
  8. package/dist/export.d.mts +8 -0
  9. package/dist/export.mjs +76 -0
  10. package/dist/gc.d.mts +8 -0
  11. package/dist/gc.mjs +51 -0
  12. package/dist/import.d.mts +1 -1
  13. package/dist/import.mjs +1 -1
  14. package/dist/index.d.mts +2 -2
  15. package/dist/index.mjs +3 -3
  16. package/dist/{log-qCNkDjh5.mjs → log-BWVFYAh5.mjs} +39 -12
  17. package/dist/{backend-E0bmTXpz.d.mts → log-CUjKQbOR.d.mts} +222 -39
  18. package/dist/{sqlite-zcXVyMxr.mjs → sqlite-SCiSnwaz.mjs} +321 -259
  19. package/dist/{src-B9X8BKyh.mjs → src-BZ1tffd5.mjs} +342 -310
  20. package/dist/storage.d.mts +20 -7
  21. package/dist/storage.mjs +2 -2
  22. package/dist/testing.d.mts +1 -1
  23. package/dist/testing.mjs +48 -29
  24. package/dist/usage.d.mts +2 -0
  25. package/dist/usage.mjs +152 -0
  26. package/drizzle/postgres/20260923120000_v3_usage_materialized/migration.sql +48 -0
  27. package/drizzle/sqlite/20260923120000_v3_usage_materialized/migration.sql +48 -0
  28. package/package.json +25 -30
  29. package/src/backend.ts +41 -17
  30. package/src/branch.ts +4 -4
  31. package/src/drizzle/postgres-v3.ts +2 -0
  32. package/src/drizzle/sqlite-v3.ts +2 -0
  33. package/src/entities/v3/event-usage.ts +8 -1
  34. package/src/entities/v3/index.ts +8 -0
  35. package/src/entities/v3/session-counts.ts +23 -0
  36. package/src/entities/v3/session-usage.ts +29 -0
  37. package/src/index.ts +230 -88
  38. package/src/log.ts +58 -17
  39. package/src/postgres.ts +248 -149
  40. package/src/rows.ts +2 -1
  41. package/src/schema.ts +4 -0
  42. package/src/sqlite.ts +263 -124
  43. package/src/storage-takeover/repository.ts +1 -4
  44. package/src/usage.ts +10 -6
  45. package/cordis.patch.yml +0 -13
package/README.md CHANGED
@@ -7,8 +7,8 @@ SQLite 或 PostgreSQL 后端。表结构、原样存储与各条流程的设计
7
7
 
8
8
  ## 配置
9
9
 
10
- 配置**就是这一行的 config**(`cordis.patch.yml` 的 `session-rdb` 行,或 profile patch /
11
- 设置页改的那份 entry config):
10
+ 配置**就是这一行的 config**(聚合层 `@morlay/better-session` 的 `session-rdb` 行、profile patch,
11
+ 或设置页改的那份 entry config):
12
12
 
13
13
  ```yaml
14
14
  - id: session-rdb
@@ -54,6 +54,18 @@ type Config =
54
54
  };
55
55
  ```
56
56
 
57
+ ### 设置页上的这一行
58
+
59
+ `Config` **整段标了 `.volatile()`**(判别式 union 只能整段标:schemastery 不许在 union 分支内部标 volatile),
60
+ 所以 host 会描述这一行,插件管理页里按 `type` 展开当前后端的字段——与上面 YAML 里的字段一一对应。
61
+
62
+ - **切后端**:`type` 是各分支的判别标签,页面上那一行给的是一个**切换控件**(`"sqlite"` ↔ `"postgres"`);选另一支
63
+ 就换成那一套字段(`path` ↔ `connectionString` / `schema`),页面结构与 host 侧校验的选支依据同一份。
64
+ - **生效方式**:settings 写完 → Loader 重装这一行(`reconcileProfilePatches`)→ 用新 config 重新建连接。所以这里
65
+ 装配时取一次快照(`this.config = config.get()`),没有运行期重连逻辑;换后端 = 换库,旧数据留在旧后端。
66
+ - 默认值都在 schema 上(`journalMode: wal`、`busyTimeout`、`schema: public`、`projectionCache` 的节流),页面上
67
+ 清空即回到这些默认。
68
+
57
69
  ## 分支能力(session-branch 闭环)
58
70
 
59
71
  除 `ctx.sessionPersistence` 外,本包还实现 `@morlay/session-branch` 的 provider
@@ -80,10 +92,10 @@ web 模式经 `POST /api/session.delete`(body `{ sessionId }`)暴露,状
80
92
 
81
93
  ## 会话列表(两条路)
82
94
 
83
- | 用途 | 接缝 | 内容 |
84
- | --------------------- | --------------------------------------------------------------------------------------------------------------- | ------------------------------------------------ |
85
- | 官方 UI(上游 `ui-*`)| `session/list` → `ctx.sessionQuery.listSessions`(本包接管,**默认排除已归档**) | 未归档的可见语料,标题走投影(`projections.title`) |
86
- | 我们自己的管理面 | `POST /api/session.rows`(本包路由) | **完整语料(含归档)** + 标题 + 最后活动时间 + 工作区归属,**搜索 / 子代理过滤 / 分页都在后端** |
95
+ | 用途 | 接缝 | 内容 |
96
+ | ---------------------- | -------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------- |
97
+ | 官方 UI(上游 `ui-*`) | `session/list` → `ctx.sessionQuery.listSessions`(本包接管,**默认排除已归档**) | 未归档的可见语料,标题走投影(`projections.title`) |
98
+ | 我们自己的管理面 | `POST /api/session.rows`(本包路由) | **完整语料(含归档)** + 标题 + 最后活动时间 + 工作区归属,**搜索 / 子代理过滤 / 分页都在后端** |
87
99
 
88
100
  排除归档是本部署的策略:归档是「收起来」的会话,不该混进日常列表与搜索。管理动作(取消归档 / 删除 /
89
101
  导入)需要完整集合,所以另给一条自有路由,两条路不混。决策与取舍见
@@ -118,13 +130,20 @@ subagent 会话**(`origin = 'subagent'`、父不在表里;有 open handle /
118
130
  ## 用量统计
119
131
 
120
132
  `POST /api/session.usage` 一次回报三份数据:`totals`(含 `subagent` / `human` 拆分)、按
121
- 天 × provider/model × subagent 的桶、按会话的行。
133
+ 天 × provider/model × subagent 的桶、按会话的行(活动计数:轮次 / 步骤 / 用户输入 / 工具调用)。
134
+
135
+ 统计读侧只读三张**可销毁重建的派生表**,读侧维度(会话 / 模型 / 本地日 / 是否被引用 / 是否子代理)
136
+ 全部在写入时物化,查询不做 `EXISTS`、也不回连事件表:
137
+
138
+ - `t_event_usage`——一条 `assistant/message` 事件行一行(去重口径的总量与「天 × 模型」桶读它);
139
+ - `t_session_usage`——会话 × 本地日 × 模型的 token 汇总(按会话的行读它,含 fork 继承前缀);
140
+ - `t_session_counts`——会话 × 本地日的四项活动计数。
122
141
 
123
- 用量写在**专用表 `t_event_usage`**(写路径顺带记录,一条 `assistant/message` 事件行一行),统计只读它,
124
- 不再逐行解析事件 JSON——真实库实测 **1.6s → ~140ms**。老库在首次打开时**一次性回填**(表为空才跑,
125
- 约 2.2s / 1.99 万行),两种 `f_data` 结构都认。口径不变:只算被会话引用的事件行(fork 共享行计一次、
126
- 孤儿行不计),GC 顺带回收不再有事件行的用量行。口径与取舍见
127
- [ADR-用量统计走专用用量日志表](.agents/adrs/20260918-用量统计走专用用量日志表.md)。
142
+ 写入时在事务内落用量行、事务后**旁路累加**两张汇总表;rewind / fork / 删除会话之后按会话重算并全量重算
143
+ 引用标记;表为空(首次 / 迁移后 / 手动清空)在打开库时全量回填——迁移对这三张表先删后建。真实库实测
144
+ 单次统计从 **1.6s → ~140ms → ~30ms**(口径不变,旧口径逐项交叉核对一致),升级后首次打开付一次回填
145
+ (2.4s / 3.09 万用量行,此后不再发生)。口径与取舍见
146
+ [ADR-统计衍生表物化归属与汇总](.agents/adrs/20260923-统计衍生表物化归属与汇总.md)。
128
147
 
129
148
  ## 查询接管(`ctx.sessionQuery`)
130
149
 
@@ -1,102 +1,3 @@
1
- import { i as EventRow, s as SessionRow } from "./backend-E0bmTXpz.mjs";
1
+ import { C as usageRowOf, S as toJsonlArtifact, _ as sealOwnEvents, a as normalizeToCurrentShape, b as syncMeteringRanges, c as renameLegacyPtcEvents, d as repairReadView, f as repairRequestHeaders, g as scanRows, h as rowToMeta, i as needsShapeAdoption, l as repairAssistantSettlement, m as rowToEvent, n as OWN_EVENT_TYPES, o as orphanInboxSpliceSeqs, p as repairSurfaceOps, r as findSurfaceRepairs, s as recomputeReplaceProvenance, t as EventUsageRow, u as repairOrphanInboxSplices, v as sessionConflictRow, x as titleOfEventData, y as sessionInsertRow } from "./log-CUjKQbOR.mjs";
2
2
  import { SESSION_IMPORT_PATH, SESSION_LOG_ARTIFACT_FILENAME, parseImportZip, parseJsonlArtifact, persistImport, registerSessionImport } from "./import.mjs";
3
- import { SessionStorageMetadata } from "@deepseek-ai/dsh-session-persistence";
4
- import { SessionEvent, SessionHeader } from "@deepseek-ai/dsh-session";
5
- //#region src/log.d.ts
6
- declare function rowToMeta(row: SessionRow): SessionHeader;
7
- declare function sessionInsertRow(storage: SessionStorageMetadata, incarnation: string): {
8
- fSessionId: string;
9
- fHeadEventId: string;
10
- fHeadSequence: number;
11
- fVersion: number;
12
- fCreatedAt: number;
13
- fCwd: string | null;
14
- fParentSession: string | null;
15
- fSeedLength: number | null;
16
- fOrigin: string | null;
17
- fDelegationDepth: number | null;
18
- fAgentPreset: string | null;
19
- fIncarnation: string;
20
- fRevision: number;
21
- };
22
- declare function sessionConflictRow(storage: SessionStorageMetadata): {
23
- fVersion: number;
24
- fCreatedAt: number;
25
- fCwd: string | null;
26
- fParentSession: string | null;
27
- fSeedLength: number | null;
28
- fOrigin: string | null;
29
- fDelegationDepth: number | null;
30
- fAgentPreset: string | null;
31
- };
32
- declare function rowToEvent(row: EventRow): SessionEvent;
33
- declare function recomputeReplaceProvenance(events: SessionEvent[]): void;
34
- declare function findSurfaceRepairs(events: readonly SessionEvent[]): {
35
- degradeToAppend: Set<number>;
36
- addAppendMarker: Set<number>;
37
- clearSurfaceOp: Set<number>;
38
- clampEnd: Map<number, number>;
39
- };
40
- declare function repairSurfaceOps(events: SessionEvent[]): void;
41
- declare function repairAssistantSettlement(events: SessionEvent[]): void;
42
- declare function repairRequestHeaders(events: SessionEvent[]): void;
43
- declare function syncMeteringRanges(events: SessionEvent[]): void;
44
- declare function renameLegacyPtcEvents(events: SessionEvent[]): void;
45
- /**
46
- * 把**旧代(v3 及更早)的消息形状**归一到当前格式(v4):迁移链(严格)拒绝那些形状时,读路径仍要
47
- * 能把它们读出来——只动字段与标记,不改坐标、不改内容。
48
- *
49
- * 三件事:
50
- * - `system/message` 的 source:`{ kind: 'plugin', plugin: '@deepseek-ai/dsh-system-prompt' }` → `{ kind: 'system-prompt' }`
51
- * (v4 起 system 消息只认这个 kind);
52
- * - `tool/result` 的消息:v3 是 user 角色、结果块包在 `content[0]`;v4 是 `role: 'tool'` + 顶层
53
- * `toolCallId` + `content` 就是结果块;
54
- * - 本仓库自己写过、上游已删除的事件类型(见 `LEGACY_OWN_EVENT_TYPES`)标 `ignorable: true`——v4 的
55
- * 校验对未知类型只接受可忽略信封。
56
- *
57
- * 只认这张白名单:**其它未知类型保持 fail loud**(那是「数据来自更新的 harness」的信号,不能吞掉)。
58
- */
59
- declare function normalizeToCurrentShape(events: SessionEvent[]): void;
60
- /**
61
- * 本仓库写过、后来连同机制一起删除的事件类型:读的时候标 `ignorable: true` 让校验跳过它们。
62
- * 新增历史类型时登记到这里——**不要**用「所有未知类型」代替(那会掩盖更新版本的未知事件)。
63
- */
64
- declare const LEGACY_OWN_EVENT_TYPES: ReadonlySet<string>;
65
- /** 行里是否还带着旧代消息形状(`session-branch/version` 这类自造事件也算)——写路径曾把回退视图的结果
66
- * 以当前版本号落库,所以**版本号不可信**,读之前要按内容判一次。 */
67
- declare function hasLegacyShape(events: readonly SessionEvent[]): boolean;
68
- declare function repairReadView(events: SessionEvent[]): void;
69
- declare function orphanInboxSpliceSeqs(events: readonly SessionEvent[]): Set<number>;
70
- declare function repairOrphanInboxSplices(events: SessionEvent[]): void;
71
- declare function scanRows(rows: readonly EventRow[], base?: number): {
72
- preserved: SessionEvent[];
73
- tornFrom?: number;
74
- };
75
- declare function toJsonlArtifact(meta: SessionHeader, inheritedEventCount: number, events: readonly SessionEvent[]): string;
76
- declare function titleOfEventData(data: string): string | undefined;
77
- /** 用量行的列(`t_event_usage`)。 */
78
- interface EventUsageRow {
79
- fEventId: string;
80
- fCreatedAt: number;
81
- fProvider: string | null;
82
- fModel: string | null;
83
- fInputTokens: number;
84
- fOutputTokens: number;
85
- fCacheReadTokens: number;
86
- fReasoningTokens: number;
87
- fTotalTokens: number;
88
- }
89
- /**
90
- * 一条事件行的用量(只有带 `usage` 的 `assistant/message` 才有):写路径据此记录
91
- * `t_event_usage`,统计因此不必逐行解析 JSON。两种 `f_data` 结构(带信封 / 老格式)都认。
92
- * @param event - 待写入的事件行。
93
- * @returns 用量行,或该事件没有用量时的 undefined。
94
- */
95
- declare function usageRowOf(event: {
96
- fEventId: string;
97
- fCreatedAt: number;
98
- fType: string;
99
- fData: string;
100
- }): EventUsageRow | undefined;
101
- //#endregion
102
- export { EventUsageRow, LEGACY_OWN_EVENT_TYPES, SESSION_IMPORT_PATH, SESSION_LOG_ARTIFACT_FILENAME, findSurfaceRepairs, hasLegacyShape, normalizeToCurrentShape, orphanInboxSpliceSeqs, parseImportZip, parseJsonlArtifact, persistImport, recomputeReplaceProvenance, registerSessionImport, renameLegacyPtcEvents, repairAssistantSettlement, repairOrphanInboxSplices, repairReadView, repairRequestHeaders, repairSurfaceOps, rowToEvent, rowToMeta, scanRows, sessionConflictRow, sessionInsertRow, syncMeteringRanges, titleOfEventData, toJsonlArtifact, usageRowOf };
3
+ export { EventUsageRow, OWN_EVENT_TYPES, SESSION_IMPORT_PATH, SESSION_LOG_ARTIFACT_FILENAME, findSurfaceRepairs, needsShapeAdoption, normalizeToCurrentShape, orphanInboxSpliceSeqs, parseImportZip, parseJsonlArtifact, persistImport, recomputeReplaceProvenance, registerSessionImport, renameLegacyPtcEvents, repairAssistantSettlement, repairOrphanInboxSplices, repairReadView, repairRequestHeaders, repairSurfaceOps, rowToEvent, rowToMeta, scanRows, sealOwnEvents, sessionConflictRow, sessionInsertRow, syncMeteringRanges, titleOfEventData, toJsonlArtifact, usageRowOf };
package/dist/artifact.mjs CHANGED
@@ -1,3 +1,3 @@
1
- import { _ as sessionInsertRow, a as orphanInboxSpliceSeqs, b as toJsonlArtifact, c as repairAssistantSettlement, d as repairRequestHeaders, f as repairSurfaceOps, g as sessionConflictRow, h as scanRows, i as normalizeToCurrentShape, l as repairOrphanInboxSplices, m as rowToMeta, n as findSurfaceRepairs, o as recomputeReplaceProvenance, p as rowToEvent, r as hasLegacyShape, s as renameLegacyPtcEvents, t as LEGACY_OWN_EVENT_TYPES, u as repairReadView, v as syncMeteringRanges, x as usageRowOf, y as titleOfEventData } from "./log-qCNkDjh5.mjs";
1
+ import { S as usageRowOf, _ as sessionConflictRow, a as orphanInboxSpliceSeqs, b as titleOfEventData, c as repairAssistantSettlement, d as repairRequestHeaders, f as repairSurfaceOps, g as sealOwnEvents, h as scanRows, i as normalizeToCurrentShape, l as repairOrphanInboxSplices, m as rowToMeta, n as findSurfaceRepairs, o as recomputeReplaceProvenance, p as rowToEvent, r as needsShapeAdoption, s as renameLegacyPtcEvents, t as OWN_EVENT_TYPES, u as repairReadView, v as sessionInsertRow, x as toJsonlArtifact, y as syncMeteringRanges } from "./log-BWVFYAh5.mjs";
2
2
  import { SESSION_IMPORT_PATH, SESSION_LOG_ARTIFACT_FILENAME, parseImportZip, parseJsonlArtifact, persistImport, registerSessionImport } from "./import.mjs";
3
- export { LEGACY_OWN_EVENT_TYPES, SESSION_IMPORT_PATH, SESSION_LOG_ARTIFACT_FILENAME, findSurfaceRepairs, hasLegacyShape, normalizeToCurrentShape, orphanInboxSpliceSeqs, parseImportZip, parseJsonlArtifact, persistImport, recomputeReplaceProvenance, registerSessionImport, renameLegacyPtcEvents, repairAssistantSettlement, repairOrphanInboxSplices, repairReadView, repairRequestHeaders, repairSurfaceOps, rowToEvent, rowToMeta, scanRows, sessionConflictRow, sessionInsertRow, syncMeteringRanges, titleOfEventData, toJsonlArtifact, usageRowOf };
3
+ export { OWN_EVENT_TYPES, SESSION_IMPORT_PATH, SESSION_LOG_ARTIFACT_FILENAME, findSurfaceRepairs, needsShapeAdoption, normalizeToCurrentShape, orphanInboxSpliceSeqs, parseImportZip, parseJsonlArtifact, persistImport, recomputeReplaceProvenance, registerSessionImport, renameLegacyPtcEvents, repairAssistantSettlement, repairOrphanInboxSplices, repairReadView, repairRequestHeaders, repairSurfaceOps, rowToEvent, rowToMeta, scanRows, sealOwnEvents, sessionConflictRow, sessionInsertRow, syncMeteringRanges, titleOfEventData, toJsonlArtifact, usageRowOf };
@@ -1,4 +1,4 @@
1
- import { d as repairRequestHeaders, h as scanRows, i as normalizeToCurrentShape, m as rowToMeta } from "./log-qCNkDjh5.mjs";
1
+ import { d as repairRequestHeaders, h as scanRows, i as normalizeToCurrentShape, m as rowToMeta } from "./log-BWVFYAh5.mjs";
2
2
  import { randomUUID } from "node:crypto";
3
3
  import { SESSION_FORMAT_VERSION, SessionLogOffset } from "@deepseek-ai/dsh-session";
4
4
  import { createSessionFormatCatalogWithChildren } from "@deepseek-ai/dsh-session-format-catalog";
@@ -217,7 +217,7 @@ var SessionBranchRdbProvider = class {
217
217
  internals.dropReuseEventIds(childId);
218
218
  throw error;
219
219
  }
220
- await this.persistence.rebuildEventCounts(childId);
220
+ await this.persistence.rebuildSessionStats(childId);
221
221
  return childId;
222
222
  }
223
223
  async rewind(id, toBoundary, signal) {
@@ -290,7 +290,7 @@ var SessionBranchRdbProvider = class {
290
290
  headSeq: keepLength - 1,
291
291
  snapshotEvents: () => []
292
292
  });
293
- await this.persistence.rebuildEventCounts(id);
293
+ await this.persistence.rebuildSessionStats(id);
294
294
  return {
295
295
  header: rowToMeta(row),
296
296
  revision: await internals.readStoredRevision(id)
@@ -1,4 +1,4 @@
1
- import { p as SessionPersistenceRdb } from "./backend-E0bmTXpz.mjs";
1
+ import { tt as SessionPersistenceRdb } from "./log-CUjKQbOR.mjs";
2
2
  import { Context } from "@deepseek-ai/cordis";
3
3
  //#region src/deletion.d.ts
4
4
  declare const SESSION_DELETE_PATH = "/api/session.delete";
package/dist/deletion.mjs CHANGED
@@ -1,2 +1,2 @@
1
- import { a as registerSessionDeletion, i as SESSION_DELETE_PATH } from "./src-B9X8BKyh.mjs";
1
+ import { a as registerSessionDeletion, i as SESSION_DELETE_PATH } from "./src-BZ1tffd5.mjs";
2
2
  export { SESSION_DELETE_PATH, registerSessionDeletion };
@@ -93,7 +93,7 @@ function encode(decoded) {
93
93
  return writer.flush();
94
94
  }
95
95
  //#endregion
96
- //#region ../../../node_modules/.pnpm/magic-string@1.4.1/node_modules/magic-string/dist/index.mjs
96
+ //#region ../../../node_modules/.pnpm/magic-string@1.4.2/node_modules/magic-string/dist/index.mjs
97
97
  var BitSet = class BitSet {
98
98
  constructor(arg) {
99
99
  this.bits = arg instanceof BitSet ? arg.bits.slice() : [];
@@ -187,7 +187,7 @@ var Chunk = class Chunk {
187
187
  this.outro = "";
188
188
  this.end = index;
189
189
  if (this.edited) {
190
- newChunk.edit("", false);
190
+ newChunk.edit("", false, true);
191
191
  this.content = "";
192
192
  } else this.content = originalBefore;
193
193
  newChunk.next = this.next;
@@ -1149,7 +1149,7 @@ var MagicString = class MagicString {
1149
1149
  * Content appended or prepended at positions strictly inside the range is removed with it, while
1150
1150
  * content attached at `start` or `end` is preserved — use `s.overwrite(start, end, '')` to remove
1151
1151
  * the range including its edge inserts.
1152
- * Removing the same content twice, or making removals that partially overlap, will cause an error.
1152
+ * Repeated removals and removals with partially overlapping ranges are allowed.
1153
1153
  */
1154
1154
  remove(start, end) {
1155
1155
  start = start + this.offset;
@@ -1431,10 +1431,16 @@ var MagicString = class MagicString {
1431
1431
  * @internal
1432
1432
  */
1433
1433
  _hasRemovedContent(start, end) {
1434
- let chunk = this.firstChunk;
1435
- while (chunk) {
1436
- if (chunk.content === "" && chunk.start < end && chunk.end > start) return true;
1437
- chunk = chunk.next;
1434
+ let chunk = this.byStart.get(start);
1435
+ if (!chunk) {
1436
+ chunk = this.lastSearchedChunk;
1437
+ const searchForward = start >= chunk.end;
1438
+ while (chunk && !chunk.contains(start)) chunk = searchForward ? this.byStart.get(chunk.end) : this.byEnd.get(chunk.start);
1439
+ if (chunk) this.lastSearchedChunk = chunk;
1440
+ }
1441
+ while (chunk && chunk.start < end) {
1442
+ if (chunk.content === "") return true;
1443
+ chunk = this.byStart.get(chunk.end);
1438
1444
  }
1439
1445
  return false;
1440
1446
  }
@@ -1498,6 +1504,7 @@ var MagicString = class MagicString {
1498
1504
  const stringLength = string.length;
1499
1505
  if (stringLength === 0) {
1500
1506
  for (let index = 0; index <= original.length; index += 1) {
1507
+ if (this._hasRemovedContent(index, index)) continue;
1501
1508
  const _replacement = typeof replacement === "function" ? replacement("", index, original) : expandReplacement(replacement, "", index, original, [], void 0);
1502
1509
  if (_replacement !== "") this.appendRight(index, _replacement);
1503
1510
  }
@@ -0,0 +1,8 @@
1
+ import { tt as SessionPersistenceRdb } from "./log-CUjKQbOR.mjs";
2
+ import { Context } from "@deepseek-ai/cordis";
3
+ //#region src/export.d.ts
4
+ declare const SESSION_EXPORT_PATH = "/api/session.export";
5
+ /** 导出通道:直接把会话日志打成 zip 响应体,与导入通道读同一份 artifact。 */
6
+ declare function registerSessionExport(ctx: Context, persistence: SessionPersistenceRdb): void;
7
+ //#endregion
8
+ export { SESSION_EXPORT_PATH, registerSessionExport };
@@ -0,0 +1,76 @@
1
+ import { strToU8, zip } from "fflate";
2
+ //#region src/export.ts
3
+ const SESSION_EXPORT_PATH = "/api/session.export";
4
+ /** 文件名只保留安全字符:会话 id 来自请求体,会写进 Content-Disposition。 */
5
+ function safeFilename(sessionId) {
6
+ return sessionId.replace(/[^A-Za-z0-9._-]/gu, "_");
7
+ }
8
+ /** fflate 只给回调式异步 API(同步变体被 node/no-sync 禁止)。 */
9
+ function zipBytes(files) {
10
+ return new Promise((resolve, reject) => {
11
+ zip(files, (error, data) => {
12
+ if (error === null) resolve(data);
13
+ else reject(error);
14
+ });
15
+ });
16
+ }
17
+ /** 导出通道:直接把会话日志打成 zip 响应体,与导入通道读同一份 artifact。 */
18
+ function registerSessionExport(ctx, persistence) {
19
+ ctx.inject(["webServer", "connection"], (webCtx) => {
20
+ const webServer = webCtx.webServer;
21
+ const connection = webCtx.get("connection");
22
+ return webCtx.effect(() => webServer.register({
23
+ kind: "exact",
24
+ path: SESSION_EXPORT_PATH,
25
+ handler: async (req, res) => {
26
+ const rejection = connection.requestRejection(req);
27
+ if (rejection !== void 0) {
28
+ res.writeHead(rejection);
29
+ res.end(rejection === 401 ? "unauthorized" : "forbidden");
30
+ return;
31
+ }
32
+ if (req.method !== "POST") {
33
+ res.writeHead(405, { "content-type": "application/json" });
34
+ res.end(JSON.stringify({ error: "method not allowed" }));
35
+ return;
36
+ }
37
+ const chunks = [];
38
+ for await (const chunk of req) chunks.push(chunk);
39
+ let envelope;
40
+ try {
41
+ envelope = JSON.parse(Buffer.concat(chunks).toString("utf8"));
42
+ } catch {
43
+ res.writeHead(400, { "content-type": "application/json" });
44
+ res.end(JSON.stringify({ error: "request body is not JSON" }));
45
+ return;
46
+ }
47
+ if (typeof envelope.sessionId !== "string" || envelope.sessionId === "") {
48
+ res.writeHead(400, { "content-type": "application/json" });
49
+ res.end(JSON.stringify({ error: "missing sessionId field" }));
50
+ return;
51
+ }
52
+ const sessionId = envelope.sessionId;
53
+ try {
54
+ const raw = await persistence.readRaw(sessionId);
55
+ if (raw === void 0) {
56
+ res.writeHead(404, { "content-type": "application/json" });
57
+ res.end(JSON.stringify({ error: `session "${sessionId}" not found` }));
58
+ return;
59
+ }
60
+ const zipFile = await zipBytes({ [raw.filename]: strToU8(raw.content) });
61
+ res.writeHead(200, {
62
+ "content-type": "application/zip",
63
+ "content-length": String(zipFile.byteLength),
64
+ "content-disposition": `attachment; filename="${safeFilename(sessionId)}.zip"`
65
+ });
66
+ res.end(Buffer.from(zipFile));
67
+ } catch (error) {
68
+ res.writeHead(500, { "content-type": "application/json" });
69
+ res.end(JSON.stringify({ error: error instanceof Error ? error.message : "session export failed" }));
70
+ }
71
+ }
72
+ }), `session-rdb: ${SESSION_EXPORT_PATH} route`);
73
+ });
74
+ }
75
+ //#endregion
76
+ export { SESSION_EXPORT_PATH, registerSessionExport };
package/dist/gc.d.mts ADDED
@@ -0,0 +1,8 @@
1
+ import { tt as SessionPersistenceRdb } from "./log-CUjKQbOR.mjs";
2
+ import { Context } from "@deepseek-ai/cordis";
3
+ //#region src/gc.d.ts
4
+ declare const SESSION_GC_PATH = "/api/session.gc";
5
+ /** GC 通道:停 agent → 回收孤儿事件行 → VACUUM,一次请求完成。 */
6
+ declare function registerSessionGc(ctx: Context, persistence: SessionPersistenceRdb): void;
7
+ //#endregion
8
+ export { SESSION_GC_PATH, registerSessionGc };
package/dist/gc.mjs ADDED
@@ -0,0 +1,51 @@
1
+ //#region src/gc.ts
2
+ const SESSION_GC_PATH = "/api/session.gc";
3
+ /** 让所有运行中的 agent 先退场:GC 要重写事件表,运行中的写路径必须停下来。 */
4
+ async function stopRunningAgents(ctx) {
5
+ const running = ctx.get("agents")?.list() ?? [];
6
+ for (const agent of running) agent.cancel?.({ kind: "user" }, { keepInbox: true });
7
+ await Promise.all(running.map((agent) => agent.whenIdle()));
8
+ return running.length;
9
+ }
10
+ /** GC 通道:停 agent → 回收孤儿事件行 → VACUUM,一次请求完成。 */
11
+ function registerSessionGc(ctx, persistence) {
12
+ ctx.inject(["webServer", "connection"], (webCtx) => {
13
+ const webServer = webCtx.webServer;
14
+ const connection = webCtx.get("connection");
15
+ return webCtx.effect(() => webServer.register({
16
+ kind: "exact",
17
+ path: SESSION_GC_PATH,
18
+ handler: async (req, res) => {
19
+ const rejection = connection.requestRejection(req);
20
+ if (rejection !== void 0) {
21
+ res.writeHead(rejection);
22
+ res.end(rejection === 401 ? "unauthorized" : "forbidden");
23
+ return;
24
+ }
25
+ if (req.method !== "POST") {
26
+ res.writeHead(405, { "content-type": "application/json" });
27
+ res.end(JSON.stringify({ error: "method not allowed" }));
28
+ return;
29
+ }
30
+ try {
31
+ const stoppedAgents = await stopRunningAgents(webCtx);
32
+ const orphanSessions = await persistence.collectOrphanSessions();
33
+ const orphanEvents = await persistence.collectOrphans();
34
+ await persistence.vacuum();
35
+ res.writeHead(200, { "content-type": "application/json" });
36
+ res.end(JSON.stringify({
37
+ orphanSessions,
38
+ orphanEvents,
39
+ stoppedAgents,
40
+ vacuumed: true
41
+ }));
42
+ } catch (error) {
43
+ res.writeHead(500, { "content-type": "application/json" });
44
+ res.end(JSON.stringify({ error: error instanceof Error ? error.message : "gc failed" }));
45
+ }
46
+ }
47
+ }), `session-rdb: ${SESSION_GC_PATH} route`);
48
+ });
49
+ }
50
+ //#endregion
51
+ export { SESSION_GC_PATH, registerSessionGc };
package/dist/import.d.mts CHANGED
@@ -1,4 +1,4 @@
1
- import { p as SessionPersistenceRdb } from "./backend-E0bmTXpz.mjs";
1
+ import { tt as SessionPersistenceRdb } from "./log-CUjKQbOR.mjs";
2
2
  import { SessionStorageMetadata } from "@deepseek-ai/dsh-session-persistence";
3
3
  import { Session, SessionEvent, SessionId } from "@deepseek-ai/dsh-session";
4
4
  import { Context } from "@deepseek-ai/cordis";
package/dist/import.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { i as replaceLiveSessionLog, l as restoreCatalog } from "./branch-BjUR6wfe.mjs";
1
+ import { i as replaceLiveSessionLog, l as restoreCatalog } from "./branch-D4nVzQQM.mjs";
2
2
  import { randomUUID } from "node:crypto";
3
3
  import { SESSION_FORMAT_VERSION, SessionLogOffset } from "@deepseek-ai/dsh-session";
4
4
  import { parseSessionFormatLogFilename } from "@deepseek-ai/dsh-session-format";
package/dist/index.d.mts CHANGED
@@ -1,2 +1,2 @@
1
- import { D as SCHEMA_VERSION, _ as locateTurnEnd, b as SessionRowsItem, d as SessionDeletionError, f as SessionDeletionErrorCode, g as SessionBranchRdbProvider, h as SessionBranchRdb, l as Config, m as SessionPersistenceRdbInternals, p as SessionPersistenceRdb, u as ProjectionCacheOptions, x as SessionRowsValue, y as SESSION_ROWS_PATH } from "./backend-E0bmTXpz.mjs";
2
- export { Config, ProjectionCacheOptions, SCHEMA_VERSION, SESSION_ROWS_PATH, SessionBranchRdb, SessionBranchRdbProvider, SessionDeletionError, SessionDeletionErrorCode, SessionPersistenceRdb, SessionPersistenceRdb as default, SessionPersistenceRdbInternals, type SessionRowsItem, type SessionRowsValue, locateTurnEnd };
1
+ import { $ as SessionDeletionError, Q as ResolvedConfig, X as Config, Z as ProjectionCacheOptions, at as locateTurnEnd, ct as SessionRowsItem, et as SessionDeletionErrorCode, ht as SCHEMA_VERSION, it as SessionBranchRdbProvider, lt as SessionRowsValue, nt as SessionPersistenceRdbInternals, rt as SessionBranchRdb, st as SESSION_ROWS_PATH, tt as SessionPersistenceRdb } from "./log-CUjKQbOR.mjs";
2
+ export { Config, ProjectionCacheOptions, ResolvedConfig, SCHEMA_VERSION, SESSION_ROWS_PATH, SessionBranchRdb, SessionBranchRdbProvider, SessionDeletionError, SessionDeletionErrorCode, SessionPersistenceRdb, SessionPersistenceRdb as default, SessionPersistenceRdbInternals, type SessionRowsItem, type SessionRowsValue, locateTurnEnd };
package/dist/index.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { p as SCHEMA_VERSION } from "./sqlite-zcXVyMxr.mjs";
2
- import { n as SessionPersistenceRdb, r as SESSION_ROWS_PATH, t as SessionDeletionError } from "./src-B9X8BKyh.mjs";
3
- import { n as SessionBranchRdbProvider, r as locateTurnEnd, t as SessionBranchRdb } from "./branch-BjUR6wfe.mjs";
1
+ import { o as SCHEMA_VERSION } from "./sqlite-SCiSnwaz.mjs";
2
+ import { n as SessionPersistenceRdb, r as SESSION_ROWS_PATH, t as SessionDeletionError } from "./src-BZ1tffd5.mjs";
3
+ import { n as SessionBranchRdbProvider, r as locateTurnEnd, t as SessionBranchRdb } from "./branch-D4nVzQQM.mjs";
4
4
  export { SCHEMA_VERSION, SESSION_ROWS_PATH, SessionBranchRdb, SessionBranchRdbProvider, SessionDeletionError, SessionPersistenceRdb, SessionPersistenceRdb as default, locateTurnEnd };
@@ -1,3 +1,4 @@
1
+ import { localDayKey } from "./usage.mjs";
1
2
  import { SessionSeq } from "@deepseek-ai/dsh-session";
2
3
  import { sessionFormatCatalog } from "@deepseek-ai/dsh-session-format-catalog";
3
4
  //#region src/log.ts
@@ -354,15 +355,15 @@ function renameLegacyPtcEvents(events) {
354
355
  * (v4 起 system 消息只认这个 kind);
355
356
  * - `tool/result` 的消息:v3 是 user 角色、结果块包在 `content[0]`;v4 是 `role: 'tool'` + 顶层
356
357
  * `toolCallId` + `content` 就是结果块;
357
- * - 本仓库自己写过、上游已删除的事件类型(见 `LEGACY_OWN_EVENT_TYPES`)标 `ignorable: true`——v4 的
358
- * 校验对未知类型只接受可忽略信封。
358
+ * - **本仓库自造、上游不认识的事件类型**(见 `OWN_EVENT_TYPES`:历史遗留的与我们当前在写的)标
359
+ * `ignorable: true`——v4 的校验对未知类型只接受可忽略信封。
359
360
  *
360
361
  * 只认这张白名单:**其它未知类型保持 fail loud**(那是「数据来自更新的 harness」的信号,不能吞掉)。
361
362
  */
362
363
  function normalizeToCurrentShape(events) {
363
364
  for (const event of events) {
364
365
  const type = event.type;
365
- if (LEGACY_OWN_EVENT_TYPES.has(type)) {
366
+ if (OWN_EVENT_TYPES.has(type)) {
366
367
  event.ignorable = true;
367
368
  continue;
368
369
  }
@@ -405,16 +406,36 @@ function liftToolResultMessage(message) {
405
406
  };
406
407
  }
407
408
  /**
408
- * 本仓库写过、后来连同机制一起删除的事件类型:读的时候标 `ignorable: true` 让校验跳过它们。
409
- * 新增历史类型时登记到这里——**不要**用「所有未知类型」代替(那会掩盖更新版本的未知事件)。
409
+ * 落库前给**本仓库自造的事件类型**补上 `ignorable: true` 信封。
410
+ *
411
+ * 上游的持久化校验(`validateStoredEvents`)只放行「已知类型」或「带可忽略信封的未知类型」,我们的类型
412
+ * 按构造就在已知表之外——不补的话连**写入**都会被拒(`appendBatch` 里那道校验)。上游 `Session.append`
413
+ * 没有承载这个信封的口子(第三个参数只服务 surface 事件),所以这一笔由持久化层补:canonical log 里的
414
+ * 形状不变,落库的那一份带上信封。
415
+ * @param events - 即将落库的批次(`materializeAppendBatch` 的 JSON 快照副本,可写)。
416
+ */
417
+ function sealOwnEvents(events) {
418
+ for (const event of events) if (OWN_EVENT_TYPES.has(event.type)) event.ignorable = true;
419
+ }
420
+ /**
421
+ * 本仓库自造、上游不认识的事件类型:读的时候标 `ignorable: true`,v4 的持久化校验才肯跳过它们
422
+ * (`validateStoredEvents` 只放行「已知类型」或「带可忽略信封的未知类型」)。
423
+ *
424
+ * 两类都在这张表里:
425
+ * - **历史遗留**:写过、后来连同机制一起删除的(`session-branch/version`);
426
+ * - **当前在写**:本仓库的插件自己声明的事件类型(`session-mode/selected`)——上游的
427
+ * `KNOWN_SESSION_EVENT_TYPES` 由上游仓库的声明生成,外部插件的事件按构造就在它之外。
428
+ *
429
+ * 新增自造事件类型时**必须**登记到这里,否则读路径 fail loud(真回归:`session-mode/selected` 漏登记,
430
+ * 会话打不开)。**不要**用「所有未知类型」代替(那会掩盖更新版本的未知事件——那种必须拒读)。
410
431
  */
411
- const LEGACY_OWN_EVENT_TYPES = /* @__PURE__ */ new Set(["session-branch/version"]);
412
- /** 行里是否还带着旧代消息形状(`session-branch/version` 这类自造事件也算)——写路径曾把回退视图的结果
413
- * 以当前版本号落库,所以**版本号不可信**,读之前要按内容判一次。 */
414
- function hasLegacyShape(events) {
432
+ const OWN_EVENT_TYPES = /* @__PURE__ */ new Set(["session-branch/version", "session-mode/selected"]);
433
+ /** 这批事件是否需要按当前形状归一(旧代消息形状,或尚未标 `ignorable` 的自造事件类型)——写路径曾把回退
434
+ * 视图的结果以当前版本号落库,所以**版本号不可信**,读之前要按内容判一次。 */
435
+ function needsShapeAdoption(events) {
415
436
  for (const event of events) {
416
437
  const type = event.type;
417
- if (LEGACY_OWN_EVENT_TYPES.has(type)) return true;
438
+ if (OWN_EVENT_TYPES.has(type)) return true;
418
439
  if (type === "system/message") {
419
440
  if ((event.data["message"]?.["source"])?.["kind"] === "plugin") return true;
420
441
  continue;
@@ -555,10 +576,13 @@ function textField(value) {
555
576
  /**
556
577
  * 一条事件行的用量(只有带 `usage` 的 `assistant/message` 才有):写路径据此记录
557
578
  * `t_event_usage`,统计因此不必逐行解析 JSON。两种 `f_data` 结构(带信封 / 老格式)都认。
579
+ * 读侧维度一起物化:本地日由事件时间算,`f_referenced` 直接 1(这一批事件行与桥接行同事务,
580
+ * 提交即被引用),`f_subagent` 取写入它的会话是否子代理。
558
581
  * @param event - 待写入的事件行。
582
+ * @param subagent - 写入它的会话是否 `origin === 'subagent'`。
559
583
  * @returns 用量行,或该事件没有用量时的 undefined。
560
584
  */
561
- function usageRowOf(event) {
585
+ function usageRowOf(event, subagent) {
562
586
  if (event.fType !== "assistant/message") return void 0;
563
587
  let parsed;
564
588
  try {
@@ -573,8 +597,11 @@ function usageRowOf(event) {
573
597
  return {
574
598
  fEventId: event.fEventId,
575
599
  fCreatedAt: event.fCreatedAt,
600
+ fDay: localDayKey(event.fCreatedAt),
576
601
  fProvider: textField(source?.["provider"]),
577
602
  fModel: textField(source?.["model"]),
603
+ fReferenced: 1,
604
+ fSubagent: subagent ? 1 : 0,
578
605
  fInputTokens: numericField(usage["inputTokens"]),
579
606
  fOutputTokens: numericField(usage["outputTokens"]),
580
607
  fCacheReadTokens: numericField(usage["cacheReadTokens"]),
@@ -583,4 +610,4 @@ function usageRowOf(event) {
583
610
  };
584
611
  }
585
612
  //#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 };
613
+ export { usageRowOf as S, 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, 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, toJsonlArtifact as x, syncMeteringRanges as y };