@morlay/session-rdb 0.1.1 → 0.1.3

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 (50) hide show
  1. package/README.md +17 -166
  2. package/dist/artifact.d.mts +2 -2
  3. package/dist/artifact.mjs +3 -2
  4. package/dist/{branch-D4nVzQQM.mjs → branch-aiXPmnKC.mjs} +1 -8
  5. package/dist/deletion.d.mts +3 -4
  6. package/dist/deletion.mjs +2 -1
  7. package/dist/export.d.mts +3 -5
  8. package/dist/export.mjs +1 -4
  9. package/dist/gc.d.mts +3 -5
  10. package/dist/gc.mjs +1 -3
  11. package/dist/import.d.mts +7 -8
  12. package/dist/import.mjs +2 -7
  13. package/dist/index.d.mts +1 -1
  14. package/dist/index.mjs +4 -3
  15. package/dist/invariant.d.mts +4 -5
  16. package/dist/{log-BWVFYAh5.mjs → log-DYxdjJ-l.mjs} +1 -52
  17. package/dist/{log-CUjKQbOR.d.mts → log-DYyxRh7j.d.mts} +2216 -294
  18. package/dist/rolldown-runtime-EAsCxrDo.mjs +14 -0
  19. package/dist/routes-DGBlBE_I.mjs +10 -0
  20. package/dist/{sqlite-SCiSnwaz.mjs → sqlite-YCr2-Fov.mjs} +24 -62
  21. package/dist/{src-BZ1tffd5.mjs → src-BmogreEz.mjs} +59 -97
  22. package/dist/storage.d.mts +6 -26
  23. package/dist/storage.mjs +1 -1
  24. package/dist/testing.d.mts +7 -7
  25. package/dist/testing.mjs +2 -4
  26. package/dist/usage.d.mts +1 -1
  27. package/dist/usage.mjs +1 -17
  28. package/package.json +2 -2
  29. package/src/backend.ts +35 -54
  30. package/src/branch.ts +3 -5
  31. package/src/deletion.ts +2 -1
  32. package/src/entities/v3/event-usage.ts +3 -6
  33. package/src/entities/v3/session-counts.ts +3 -5
  34. package/src/entities/v3/session-usage.ts +3 -5
  35. package/src/export.ts +5 -4
  36. package/src/gc.ts +4 -3
  37. package/src/import.ts +5 -6
  38. package/src/index.ts +47 -45
  39. package/src/legacy.ts +3 -5
  40. package/src/log.ts +28 -53
  41. package/src/postgres.ts +70 -61
  42. package/src/routes.ts +16 -0
  43. package/src/rows.ts +11 -15
  44. package/src/session-query.ts +3 -7
  45. package/src/sqlite.ts +70 -58
  46. package/src/storage-takeover/projection-cache.ts +6 -8
  47. package/src/storage-takeover/repository.ts +2 -2
  48. package/src/storage-takeover/types.ts +1 -1
  49. package/src/usage.ts +22 -34
  50. package/src/write-guard.ts +1 -1
package/src/backend.ts CHANGED
@@ -75,10 +75,8 @@ export interface BackendTx {
75
75
 
76
76
  refreshTitle(id: SessionId): Promise<void>;
77
77
 
78
- /**
79
- * 插入事件行与它们的用量行(`usageRows` 由写路径一次折好,读侧维度已物化在行上)。
80
- * 这一批事件与桥接行同事务,提交即「被引用」。
81
- */
78
+ // 插入事件行与它们的用量行(`usageRows` 由写路径一次折好,读侧维度已物化在行上)。
79
+ // 这一批事件与桥接行同事务,提交即「被引用」。
82
80
  insertEvents(events: EventInsert[], usageRows: readonly EventUsageRow[]): Promise<void>;
83
81
 
84
82
  insertBridges(
@@ -92,47 +90,45 @@ export interface BackendTx {
92
90
 
93
91
  updateHead(id: SessionId, headEventId: string, headSequence: number): Promise<void>;
94
92
 
95
- /** 写批次提交时递增 revision;给了 `lastEventAt` 就一并把「最后活动时间」往前推(只增不减)。 */
93
+ // 写批次提交时递增 revision;给了 `lastEventAt` 就一并把「最后活动时间」往前推(只增不减)。
96
94
  bumpRevision(id: SessionId, lastEventAt?: number): Promise<void>;
97
95
 
98
- deleteBridgeTail(id: SessionId, fromSequence: number): Promise<void>;
96
+ // 删掉 fromSequence 起的桥接行,返回被删行引用的 event id(调用方据此重算这些用量行的引用标记)。
97
+ deleteBridgeTail(id: SessionId, fromSequence: number): Promise<readonly string[]>;
99
98
 
100
99
  getPrevBridge(
101
100
  id: SessionId,
102
101
  sequence: number,
103
102
  ): Promise<{ fEventId: string; fSequence: number } | undefined>;
104
103
 
105
- deleteSession(id: SessionId): Promise<void>;
104
+ // 删除整条会话;返回被删桥接行引用的 event id(引用标记要跟着重算)。
105
+ deleteSession(id: SessionId): Promise<readonly string[]>;
106
106
 
107
- /** 批量删除整条会话:桥接行、workspace 归属行、投影行与会话行。 */
108
- deleteSessions(ids: SessionId[]): Promise<number>;
107
+ // 批量删除整条会话:桥接行、workspace 归属行、投影行与会话行;返回删除行数与被删桥接行引用的 event id。
108
+ deleteSessions(ids: SessionId[]): Promise<{ deleted: number; eventIds: readonly string[] }>;
109
109
  }
110
110
 
111
- /**
112
- * 「会话行」列表项:管理面自己的列表用(完整语料,含归档)+ 标题 + 最后活动时间。
113
- *
114
- * 与官方列表(`session/list` → `ctx.sessionQuery.listSessions`)的区别:那条按部署策略**默认排除归档**,
115
- * 这里给的是完整集合,标题也直接带出来(官方那条的标题走投影缓存)。
116
- */
111
+ // 「会话行」列表项:管理面自己的列表(完整语料含归档 + 标题 + 最后活动时间);为何与官方 `session/list`
112
+ // 分开见 ADR-会话列表两条路。
117
113
  export interface SessionListRowRecord {
118
114
  sessionId: string;
119
115
  title: string | null;
120
116
  origin: string | null;
121
117
  cwd: string | null;
122
118
  createdAt: number;
123
- /** 该会话最后一个事件的时间(没有事件时回落 `createdAt`)。 */
119
+ // 该会话最后一个事件的时间(没有事件时回落 `createdAt`)。
124
120
  updatedAt: number;
125
121
  archived: boolean;
126
122
  subagent: boolean;
127
- /** 所属工作区标题;不属于任何工作区时为 null。 */
123
+ // 所属工作区标题;不属于任何工作区时为 null。
128
124
  workspace: string | null;
129
125
  }
130
126
 
131
- /** 会话行列表的查询条件:搜索、子代理过滤与分页都在后端做(前端分页等于每次拉全量)。 */
127
+ // 会话行列表的查询条件:搜索、子代理过滤与分页都在后端做(前端分页等于每次拉全量)。
132
128
  export interface SessionListRowsQuery {
133
- /** 匹配标题或所属工作区标题(大小写不敏感);空串即不过滤。 */
129
+ // 匹配标题或所属工作区标题(大小写不敏感);空串即不过滤。
134
130
  query?: string;
135
- /** 是否连子代理派生会话一起返回(默认不含)。 */
131
+ // 是否连子代理派生会话一起返回(默认不含)。
136
132
  includeSubagents?: boolean;
137
133
  limit?: number;
138
134
  offset?: number;
@@ -140,16 +136,16 @@ export interface SessionListRowsQuery {
140
136
 
141
137
  export interface SessionListRowsPage {
142
138
  items: SessionListRowRecord[];
143
- /** 过滤后的总数(分页前的),前端据此算页数。 */
139
+ // 过滤后的总数(分页前的),前端据此算页数。
144
140
  total: number;
145
141
  }
146
142
 
147
- /** 活动计数的一个桶:本地日 + 四项计数(轮次 / 步骤 / 用户输入 / 工具调用)。 */
143
+ // 活动计数的一个桶:本地日 + 四项计数(轮次 / 步骤 / 用户输入 / 工具调用)。
148
144
  export interface SessionCountBucket extends UsageActivityTotals {
149
145
  day: string;
150
146
  }
151
147
 
152
- /** 用量累加的一个桶:本地日 + 模型归属 + 五个 token 列。 */
148
+ // 用量累加的一个桶:本地日 + 模型归属 + 五个 token 列。
153
149
  export interface SessionUsageBucket extends UsageTokenTotals {
154
150
  day: string;
155
151
  provider: string | null;
@@ -181,54 +177,39 @@ export interface Backend {
181
177
 
182
178
  transaction<T>(fn: (tx: BackendTx) => Promise<T>): Promise<T>;
183
179
 
184
- /** 删除已无桥接行引用的事件行(孤儿),返回删除行数。 */
180
+ // 删除已无桥接行引用的事件行(孤儿),返回删除行数。
185
181
  collectOrphans(): Promise<number>;
186
182
 
187
- /** 父会话已不存在(或没有父)的 subagent 会话:父被删后它们成了孤儿。 */
183
+ // 父会话已不存在(或没有父)的 subagent 会话:父被删后它们成了孤儿。
188
184
  listOrphanSubagentSessions(): Promise<SessionId[]>;
189
185
 
190
- /**
191
- * 用量统计的原始聚合:读三张派生统计表(`t_event_usage` + 两张会话汇总表),
192
- * 给出总量 / subagent 拆分、按天×模型的桶与按会话的行(事件行去重、排除孤儿)。
193
- * @param sinceMs - 只算该时刻(含)之后的用量;省略即全量。会话汇总表按本地日过滤,
194
- * 与毫秒过滤等价的前提是调用方把起点对齐到本地零点(见 `resolveUsageSince`)。
195
- */
186
+ // 用量统计的原始聚合:读三张派生统计表(`t_event_usage` + 两张会话汇总表),给出总量 / subagent 拆分、
187
+ // 按天×模型的桶与按会话的行(事件行去重、排除孤儿);`sinceMs` 起算(省略即全量),起点须对齐本地零点。
196
188
  usageReport(sinceMs?: number): Promise<UsageAggregate>;
197
189
 
198
- /** 管理面的会话行列表:完整语料(含归档)+ 标题 + 最后活动时间,按活动倒序分页。 */
190
+ // 管理面的会话行列表:完整语料(含归档)+ 标题 + 最后活动时间,按活动倒序分页。
199
191
  listSessionRows(query?: SessionListRowsQuery): Promise<SessionListRowsPage>;
200
192
 
201
- /**
202
- * token 用量**旁路累加**(派生表 `t_session_usage`,不参与写事务、失败可丢——表可销毁重建):
203
- * 每个 (会话, 本地日, 模型) 记一行累加。
204
- * @param id - 会话 id。
205
- * @param buckets - 本批要累加的 (day, provider, model, tokens)。
206
- */
193
+ // token 用量**旁路累加**(派生表 `t_session_usage`,不参与写事务、失败可丢——表可销毁重建):每个
194
+ // (会话, 本地日, 模型) 记一行;`buckets` 是本批要累加的 (day, provider, model, tokens)。
207
195
  incrementSessionUsage(id: SessionId, buckets: readonly SessionUsageBucket[]): Promise<void>;
208
196
 
209
- /**
210
- * 活动计数**旁路累加**(派生表 `t_session_counts`,同上):每个 (会话, 本地日) 累加四项计数。
211
- * @param id - 会话 id。
212
- * @param buckets - 本批要累加的 (day, turns, steps, userInputs, toolCalls)。
213
- */
197
+ // 活动计数**旁路累加**(派生表 `t_session_counts`,同上):每个 (会话, 本地日) 累加四项计数,
198
+ // `buckets` 是本批要累加的 (day, turns, steps, userInputs, toolCalls)。
214
199
  incrementSessionCounts(id: SessionId, buckets: readonly SessionCountBucket[]): Promise<void>;
215
200
 
216
- /**
217
- * 按会话从事件表重算两张会话汇总表(rewind / fork 之后调用):先删该会话的行再重算。
218
- * @param id - 会话 id。
219
- */
201
+ // 按会话从事件表重算两张汇总表(旧日志重写之后调用:迁移进来的会话没有旁路累加的历史,得补算)。
220
202
  rebuildSessionStats(id: SessionId): Promise<void>;
221
203
 
222
- /**
223
- * 全量重算 `t_event_usage` 的引用标记(`f_referenced` / `f_subagent`):rewind / fork / 会话删除之后调用。
224
- * 引用可能跨会话消失(截断、删除),只按本会话判定不够,故这里是全量。
225
- */
226
- refreshEventUsageFlags(): Promise<void>;
204
+ // 重算这些用量行的引用标记(`f_referenced` / `f_subagent`):会话删除 / 日志重写之后调用,
205
+ // `eventIds` 是被删掉的那批桥接行引用的事件(只有它们的引用可能消失)。
206
+ // fork 与撤回(rewind)都**不**走这里——它们不改变已发生过的消耗(见 ADR-fork不继承统计、ADR-撤回不回退统计)。
207
+ refreshEventUsageFlags(eventIds: readonly string[]): Promise<void>;
227
208
 
228
- /** 删掉一个会话的汇总行(会话删除时;外键 CASCADE 之外再显式清一次)。 */
209
+ // 删掉一个会话的汇总行(会话删除时;外键 CASCADE 之外再显式清一次)。
229
210
  deleteSessionStats(id: SessionId): Promise<void>;
230
211
 
231
- /** 回收空间与统计(含 VACUUM);不得在事务内执行。 */
212
+ // 回收空间与统计(含 VACUUM);不得在事务内执行。
232
213
  vacuum(): Promise<void>;
233
214
 
234
215
  close(): Promise<void>;
package/src/branch.ts CHANGED
@@ -311,8 +311,8 @@ export class SessionBranchRdbProvider implements SessionBranchProvider {
311
311
  internals.dropReuseEventIds(childId);
312
312
  throw error;
313
313
  }
314
- // 子会话继承了前缀事件:统计按子会话重算(派生表,best-effort)。
315
- await this.persistence.rebuildSessionStats(childId);
314
+ // fork 不动统计:继承前缀的消耗是父会话真实发生的那一次,子会话只记自己新产生的
315
+ // (写路径旁路累加,见 ADR-统计只记真实发生的用量)。
316
316
  return childId;
317
317
  }
318
318
 
@@ -443,9 +443,7 @@ export class SessionBranchRdbProvider implements SessionBranchProvider {
443
443
  });
444
444
  }
445
445
 
446
- // 截断后该会话的事件集合变了:统计跟着重算(派生表,best-effort)。
447
- await this.persistence.rebuildSessionStats(id);
448
-
446
+ // 撤回不改统计:请求已发起就已经消耗过(见 ADR-撤回不回退统计)。
449
447
  return { header: rowToMeta(row), revision: (await internals.readStoredRevision(id))! };
450
448
  }
451
449
 
package/src/deletion.ts CHANGED
@@ -1,8 +1,9 @@
1
1
  import type { Context } from "@deepseek-ai/cordis";
2
2
  import type { SessionId } from "@deepseek-ai/dsh-session";
3
3
  import { SessionDeletionError, type SessionPersistenceRdb } from "./index.ts";
4
+ import { SESSION_DELETE_PATH } from "./routes.ts";
4
5
 
5
- export const SESSION_DELETE_PATH = "/api/session.delete";
6
+ export { SESSION_DELETE_PATH };
6
7
 
7
8
  const STATUS_OF_DELETION_ERROR: Record<SessionDeletionError["code"], number> = {
8
9
  SESSION_NOT_FOUND: 404,
@@ -1,11 +1,8 @@
1
1
  import type { TableDef } from "../../adapters/types.ts";
2
2
 
3
- /**
4
- * 用量日志:一条 `assistant/message` 事件一行(按事件行唯一,fork 共享行不重复)。
5
- * 读侧维度全部物化在行上——本地日、是否被会话引用、是否被子代理会话引用——统计因此不做
6
- * `EXISTS` 判定、也不回连事件表;事件行本身仍留在 `t_events`。
7
- * 取舍见 ADR-统计衍生表物化归属与汇总。
8
- */
3
+ // 用量日志:一条 `assistant/message` 事件一行(按事件行唯一,fork 共享行不重复);读侧维度全部物化在行上
4
+ // (本地日、是否被会话引用、是否被子代理会话引用),统计因此不做 `EXISTS` 判定、也不回连事件表。
5
+ // 取舍见 ADR-统计衍生表物化归属与汇总。
9
6
  export const eventUsage: TableDef = {
10
7
  name: "t_event_usage",
11
8
  columns: {
@@ -1,10 +1,8 @@
1
1
  import type { TableDef } from "../../adapters/types.ts";
2
2
 
3
- /**
4
- * 会话 × 本地日的活动计数(派生统计表):轮次 / 步骤 / 用户输入 / 工具调用四列直接读,
5
- * 不再存事件类型、也不在读取时折。旁路累加、可按会话重建,读侧只读;
6
- * 取舍见 ADR-统计衍生表物化归属与汇总。
7
- */
3
+ // 会话 × 本地日的活动计数(派生统计表):轮次 / 步骤 / 用户输入 / 工具调用四列直接读,
4
+ // 不再存事件类型、也不在读取时折。旁路累加、可按会话重建,读侧只读;
5
+ // 取舍见 ADR-统计衍生表物化归属与汇总。
8
6
  export const sessionCounts: TableDef = {
9
7
  name: "t_session_counts",
10
8
  columns: {
@@ -1,10 +1,8 @@
1
1
  import type { TableDef } from "../../adapters/types.ts";
2
2
 
3
- /**
4
- * 会话 × 本地日 × 模型的 token 汇总(派生统计表):**按会话的行**读它,因此保住
5
- * 「fork 子会话含继承前缀」的口径(各会话行之和 ≥ 总量)而不回连事件表。
6
- * 旁路累加、可按会话重建,读侧只读;取舍见 ADR-统计衍生表物化归属与汇总。
7
- */
3
+ // 会话 × 本地日 × 模型的 token 汇总(派生统计表):**按会话的行**读它,因此保住
4
+ // 「fork 子会话含继承前缀」的口径(各会话行之和 ≥ 总量)而不回连事件表。
5
+ // 旁路累加、可按会话重建,读侧只读;取舍见 ADR-统计衍生表物化归属与汇总。
8
6
  export const sessionUsage: TableDef = {
9
7
  name: "t_session_usage",
10
8
  columns: {
package/src/export.ts CHANGED
@@ -2,15 +2,16 @@ import { strToU8, zip } from "fflate";
2
2
  import type { Context } from "@deepseek-ai/cordis";
3
3
  import type { SessionId } from "@deepseek-ai/dsh-session";
4
4
  import type { SessionPersistenceRdb } from "./index.ts";
5
+ import { SESSION_EXPORT_PATH } from "./routes.ts";
5
6
 
6
- export const SESSION_EXPORT_PATH = "/api/session.export";
7
+ export { SESSION_EXPORT_PATH };
7
8
 
8
- /** 文件名只保留安全字符:会话 id 来自请求体,会写进 Content-Disposition。 */
9
+ // 文件名只保留安全字符:会话 id 来自请求体,会写进 Content-Disposition。
9
10
  function safeFilename(sessionId: string): string {
10
11
  return sessionId.replace(/[^A-Za-z0-9._-]/gu, "_");
11
12
  }
12
13
 
13
- /** fflate 只给回调式异步 API(同步变体被 node/no-sync 禁止)。 */
14
+ // fflate 只给回调式异步 API(同步变体被 node/no-sync 禁止)。
14
15
  function zipBytes(files: Record<string, Uint8Array>): Promise<Uint8Array> {
15
16
  return new Promise((resolve, reject) => {
16
17
  zip(files, (error, data) => {
@@ -20,7 +21,7 @@ function zipBytes(files: Record<string, Uint8Array>): Promise<Uint8Array> {
20
21
  });
21
22
  }
22
23
 
23
- /** 导出通道:直接把会话日志打成 zip 响应体,与导入通道读同一份 artifact。 */
24
+ // 导出通道:直接把会话日志打成 zip 响应体,与导入通道读同一份 artifact。
24
25
  export function registerSessionExport(ctx: Context, persistence: SessionPersistenceRdb): void {
25
26
  ctx.inject(["webServer", "connection"] as const, (webCtx) => {
26
27
  const webServer = webCtx.webServer as unknown as {
package/src/gc.ts CHANGED
@@ -1,14 +1,15 @@
1
1
  import type { Context } from "@deepseek-ai/cordis";
2
+ import { SESSION_GC_PATH } from "./routes.ts";
2
3
  import type { SessionPersistenceRdb } from "./index.ts";
3
4
 
4
- export const SESSION_GC_PATH = "/api/session.gc";
5
+ export { SESSION_GC_PATH };
5
6
 
6
7
  interface GcAgentLike {
7
8
  cancel?(cause: { kind: "user" }, options?: { keepInbox?: boolean }): void;
8
9
  whenIdle(): Promise<void>;
9
10
  }
10
11
 
11
- /** 让所有运行中的 agent 先退场:GC 要重写事件表,运行中的写路径必须停下来。 */
12
+ // 让所有运行中的 agent 先退场:GC 要重写事件表,运行中的写路径必须停下来。
12
13
  async function stopRunningAgents(ctx: Context): Promise<number> {
13
14
  const agents = ctx.get("agents") as { list(): GcAgentLike[] } | undefined;
14
15
  const running = agents?.list() ?? [];
@@ -17,7 +18,7 @@ async function stopRunningAgents(ctx: Context): Promise<number> {
17
18
  return running.length;
18
19
  }
19
20
 
20
- /** GC 通道:停 agent → 回收孤儿事件行 → VACUUM,一次请求完成。 */
21
+ // GC 通道:停 agent → 回收孤儿事件行 → VACUUM,一次请求完成。
21
22
  export function registerSessionGc(ctx: Context, persistence: SessionPersistenceRdb): void {
22
23
  ctx.inject(["webServer", "connection"] as const, (webCtx) => {
23
24
  const webServer = webCtx.webServer as unknown as {
package/src/import.ts CHANGED
@@ -9,10 +9,11 @@ import { unzip } from "fflate";
9
9
  import { replaceLiveSessionLog } from "./branch.ts";
10
10
  import { restoreCatalog } from "./legacy.ts";
11
11
  import type { SessionPersistenceRdb } from "./index.ts";
12
+ import { SESSION_IMPORT_PATH } from "./routes.ts";
12
13
 
13
14
  export const SESSION_LOG_ARTIFACT_FILENAME = "session.jsonl";
14
15
 
15
- export const SESSION_IMPORT_PATH = "/api/session.import";
16
+ export { SESSION_IMPORT_PATH };
16
17
 
17
18
  const MAX_IMPORT_ZIP_BYTES = 64 * 1024 * 1024;
18
19
 
@@ -99,11 +100,9 @@ export function parseJsonlArtifact(content: string): SessionStorageMetadata & {
99
100
  };
100
101
  }
101
102
 
102
- /**
103
- * fflate 的 `unzip` 是回调式异步 API(node 侧内部走 worker_threads,浏览器侧走 Web Worker),
104
- * 这里桥成 Promise。它与 `unzipSync` 的差异只在同步/异步:非法 zip 走的是同一段校验代码,
105
- * 拒绝的错误对象与同步版一致,因此上层文案无需区分。
106
- */
103
+ // fflate 的 `unzip` 是回调式异步 API(node 侧内部走 worker_threads,浏览器侧走 Web Worker),
104
+ // 这里桥成 Promise。它与 `unzipSync` 的差异只在同步/异步:非法 zip 走的是同一段校验代码,
105
+ // 拒绝的错误对象与同步版一致,因此上层文案无需区分。
107
106
  function unzipAsync(data: Uint8Array): Promise<Record<string, Uint8Array>> {
108
107
  return new Promise((resolve, reject) => {
109
108
  unzip(data, (error, entries) => {
package/src/index.ts CHANGED
@@ -76,7 +76,7 @@ import { adoptLegacyRows, convertLegacyRows, isLegacyVersion } from "./legacy.ts
76
76
  import { needsShapeAdoption, sealOwnEvents } from "./log.ts";
77
77
  import { installStorageTakeover } from "./storage-takeover/index.ts";
78
78
 
79
- /** 一批事件里的最大时间:写路径据此推进会话行的「最后活动时间」。 */
79
+ // 一批事件里的最大时间:写路径据此推进会话行的「最后活动时间」。
80
80
  function maxEventTime(events: readonly { readonly time?: number }[]): number | undefined {
81
81
  let max: number | undefined;
82
82
  for (const event of events) {
@@ -117,7 +117,7 @@ export interface SessionPersistenceRdbInternals {
117
117
  ): Promise<import("@deepseek-ai/dsh-session-persistence").SessionPersistenceRevision | undefined>;
118
118
 
119
119
  registerReuseEventIds(childId: SessionId, map: ReadonlyMap<number, string>): void;
120
- /** fork 失败时丢弃还没被消费的映射。 */
120
+ // fork 失败时丢弃还没被消费的映射。
121
121
  dropReuseEventIds(childId: SessionId): void;
122
122
  }
123
123
 
@@ -142,7 +142,7 @@ export type Config =
142
142
  projectionCache?: ProjectionCacheOptions | undefined;
143
143
  };
144
144
 
145
- /** schema 解析之后的形状:整段 Config 是 **volatile 稳定引用**(页面可编辑),读它要过 `.get()`。 */
145
+ // schema 解析之后的形状:整段 Config 是 **volatile 稳定引用**(页面可编辑),读它要过 `.get()`。
146
146
  export type ResolvedConfig = Volatile<Config>;
147
147
 
148
148
  export type SessionDeletionErrorCode =
@@ -488,13 +488,11 @@ class RdbSessionHandle implements SessionHandle {
488
488
  static readonly LIVE_WRITE_BATCH_MAX_DELAY_MS = 200;
489
489
  }
490
490
 
491
- /**
492
- * 本地化说明:`description()` 的类型签名只声明 `string`,而 meta 本身接受 `Dict<string>`
493
- * (`vendor/schemastery/src/index.ts` 的 `mergeDesc` 就是按字典合并的),所以这里只做一次类型放行。
494
- */
491
+ // 本地化说明:`description()` 的类型签名只声明 `string`,而 meta 本身接受 `Dict<string>`
492
+ // (`vendor/schemastery/src/index.ts` 的 `mergeDesc` 就是按字典合并的),所以这里只做一次类型放行。
495
493
  const localized = (text: { zh: string; en: string }): string => text as unknown as string;
496
494
 
497
- /** 投影 checkpoint 的写回节流:两个分支共用。 */
495
+ // 投影 checkpoint 的写回节流:两个分支共用。
498
496
  const projectionCacheSchema = z
499
497
  .object({
500
498
  writeEveryEvents: z
@@ -609,7 +607,7 @@ export class SessionPersistenceRdb extends SessionPersistence {
609
607
  private readonly liveBuffers = new Map<SessionId, SessionEvent[]>();
610
608
  private readonly liveReady = new Map<SessionId, Promise<void>>();
611
609
 
612
- /** 本次装配用的配置快照(整段 Config 是 volatile,重挂会给新的)。 */
610
+ // 本次装配用的配置快照(整段 Config 是 volatile,重挂会给新的)。
613
611
  readonly config: Config;
614
612
 
615
613
  constructor(
@@ -620,12 +618,9 @@ export class SessionPersistenceRdb extends SessionPersistence {
620
618
  ) {
621
619
  super(ctx);
622
620
 
623
- // 配置就是这一行的 config(cordis.patch.yml / profile patch,或设置页改它);上游 0.1.7 的 settings
624
- // 不再提供 namespace section 覆盖,旧 `settings.yaml` 的 `session-rdb` 段由上游一次性导进同 id 的行。
625
- //
626
- // 整段 Config 标了 volatile(页面可编辑),而生效靠 **Loader 重挂这一行**(settings 写完经
627
- // `reconcileProfilePatches` 让 Loader 重装受影响的 entry):重挂会构造新实例、拿到新快照,所以这里取一次
628
- // 就够,运行期不必反复读引用。
621
+ // 配置就是这一行的 config(cordis.patch.yml / profile patch,或设置页改它);整段 Config 标了 volatile,
622
+ // 生效靠 **Loader 重挂这一行**(settings 写完经 `reconcileProfilePatches` 重装受影响的 entry)——重挂构造
623
+ // 新实例、拿到新快照,所以这里取一次就够,运行期不必反复读引用。
629
624
  this.config = config.get();
630
625
  this.backend = injectedBackend ?? createBackend(this.config);
631
626
  this.ready = this.init();
@@ -829,24 +824,23 @@ export class SessionPersistenceRdb extends SessionPersistence {
829
824
  );
830
825
  }
831
826
  await this.unarchiveBeforeDeletion(id);
832
- await this.backend.transaction(async (tx) => {
833
- await tx.deleteSession(id);
834
- });
827
+ const referenced = await this.backend.transaction((tx) => tx.deleteSession(id));
835
828
  this.liveBuffers.delete(id);
836
829
  this.liveReady.delete(id);
837
830
  this.liveFailures.delete(id);
838
831
  this.liveDropWarned.delete(id);
839
832
  this.reuseEventIds.delete(id);
840
833
  await this.dropSessionStats(id);
834
+ await this.refreshEventUsageFlags(referenced);
841
835
  }
842
836
 
843
- /** GC 通道:回收已无桥接行引用的事件行(孤儿),返回删除行数。 */
837
+ // GC 通道:回收已无桥接行引用的事件行(孤儿),返回删除行数。
844
838
  async collectOrphans(): Promise<number> {
845
839
  await this.ready;
846
840
  return this.backend.collectOrphans();
847
841
  }
848
842
 
849
- /** GC 通道:回收父已不存在的 subagent 会话(live 的跳过),返回删除的会话数。 */
843
+ // GC 通道:回收父已不存在的 subagent 会话(live 的跳过),返回删除的会话数。
850
844
  async collectOrphanSessions(): Promise<number> {
851
845
  await this.ready;
852
846
  const orphans = await this.backend.listOrphanSubagentSessions();
@@ -854,7 +848,9 @@ export class SessionPersistenceRdb extends SessionPersistence {
854
848
  (id) => !this.tracker.hasPending(id) && !this.tracker.hasOpenHandle(id),
855
849
  );
856
850
  if (deletable.length === 0) return 0;
857
- const deleted = await this.backend.transaction((tx) => tx.deleteSessions(deletable));
851
+ const { deleted, eventIds } = await this.backend.transaction((tx) =>
852
+ tx.deleteSessions(deletable),
853
+ );
858
854
  for (const id of deletable) {
859
855
  this.liveBuffers.delete(id);
860
856
  this.liveReady.delete(id);
@@ -863,16 +859,17 @@ export class SessionPersistenceRdb extends SessionPersistence {
863
859
  this.reuseEventIds.delete(id);
864
860
  await this.dropSessionStats(id);
865
861
  }
862
+ await this.refreshEventUsageFlags(eventIds);
866
863
  return deleted;
867
864
  }
868
865
 
869
- /** 用量统计:SQL 聚合的按天 × 模型桶与按会话行(事件行去重、排除孤儿行)。 */
866
+ // 用量统计:SQL 聚合的按天 × 模型桶与按会话行(事件行去重、排除孤儿行)。
870
867
  async usageReport(sinceMs?: number): Promise<UsageAggregate> {
871
868
  await this.ready;
872
869
  return this.backend.usageReport(sinceMs);
873
870
  }
874
871
 
875
- /** GC 通道:VACUUM;调用方需先停止运行中的写路径(见 `registerSessionGc`)。 */
872
+ // GC 通道:VACUUM;调用方需先停止运行中的写路径(见 `registerSessionGc`)。
876
873
  async vacuum(): Promise<void> {
877
874
  await this.ready;
878
875
  await this.backend.vacuum();
@@ -1072,11 +1069,9 @@ export class SessionPersistenceRdb extends SessionPersistence {
1072
1069
  };
1073
1070
  }
1074
1071
 
1075
- /**
1076
- * 统计衍生表的**旁路累加**:不在写事务里、失败只 warn——两张会话汇总表都是可销毁重建的
1077
- * 派生表,丢几次累加不影响可用性(需要时用 `rebuildSessionStats` 重算)。
1078
- * token 走写路径已折好的用量行,活动计数走本批事件类型。
1079
- */
1072
+ // 统计衍生表的**旁路累加**:不在写事务里、失败只 warn——两张会话汇总表都是可销毁重建的
1073
+ // 派生表,丢几次累加不影响可用性(需要时用 `rebuildSessionStats` 重算)。
1074
+ // token 走写路径已折好的用量行,活动计数走本批事件类型。
1080
1075
  private async recordSessionStats(
1081
1076
  id: SessionId,
1082
1077
  events: readonly SessionEvent[],
@@ -1104,14 +1099,10 @@ export class SessionPersistenceRdb extends SessionPersistence {
1104
1099
  }
1105
1100
  }
1106
1101
 
1107
- /**
1108
- * 重算一个会话的统计(rewind / fork 之后;best-effort,同 `recordSessionStats`):
1109
- * 两张汇总表按会话重算,用量行的引用标记全量重算(引用可能跨会话消失)。
1110
- */
1102
+ // 重算一个会话的两张汇总表(fork 之后;best-effort,同 `recordSessionStats`)。
1111
1103
  async rebuildSessionStats(id: SessionId): Promise<void> {
1112
1104
  try {
1113
1105
  await this.backend.rebuildSessionStats(id);
1114
- await this.backend.refreshEventUsageFlags();
1115
1106
  } catch (error: unknown) {
1116
1107
  this.ctx.logger.warn(
1117
1108
  `session-rdb: session stats for "${id}" not rebuilt (${describeError(error)})`,
@@ -1119,11 +1110,23 @@ export class SessionPersistenceRdb extends SessionPersistence {
1119
1110
  }
1120
1111
  }
1121
1112
 
1122
- /** 会话删除时清掉它的汇总行,并重算用量行的引用标记(被删会话引用的行要立刻退出统计)。 */
1113
+ // 重算这些用量行的引用标记(会话删除 / 日志重写之后:消失的引用要让孤儿行退出统计)。
1114
+ private async refreshEventUsageFlags(eventIds: readonly string[]): Promise<void> {
1115
+ const unique = [...new Set(eventIds)];
1116
+ if (unique.length === 0) return;
1117
+ try {
1118
+ await this.backend.refreshEventUsageFlags(unique);
1119
+ } catch (error: unknown) {
1120
+ this.ctx.logger.warn(
1121
+ `session-rdb: usage reference flags not refreshed (${describeError(error)})`,
1122
+ );
1123
+ }
1124
+ }
1125
+
1126
+ // 会话删除时清掉它的汇总行(引用标记由调用方在拿到被删事件后一并重算)。
1123
1127
  private async dropSessionStats(id: SessionId): Promise<void> {
1124
1128
  try {
1125
1129
  await this.backend.deleteSessionStats(id);
1126
- await this.backend.refreshEventUsageFlags();
1127
1130
  } catch (error: unknown) {
1128
1131
  this.ctx.logger.warn(
1129
1132
  `session-rdb: session stats for "${id}" not deleted (${describeError(error)})`,
@@ -1135,15 +1138,14 @@ export class SessionPersistenceRdb extends SessionPersistence {
1135
1138
  id: SessionId,
1136
1139
  log: { meta: SessionHeader; inheritedEventCount: number; events: SessionEvent[] },
1137
1140
  ): Promise<void> {
1141
+ let removed: readonly string[] = [];
1138
1142
  await this.backend.transaction(async (tx) => {
1139
- // 这段重写会删光该会话的桥接行、再按迁移视图重建——与 `appendBatch` 同一个理由,先做并发写者校验:
1140
- // 否则另一实例在 open 之前 / 期间提交的事件会被这段重写静默丢掉。调用方刚读过这份日志(重写只发生在
1141
- // write open 的读路径之后),所以 guard 里没有该会话时就把当前磁盘 head 记成本实例已确认的值;
1142
- // 有记录时严格比较——那能抓住"读完之后另一个实例又写了"。
1143
+ // 重写会删光该会话的桥接行再重建,先做并发写者校验(与 `appendBatch` 同一个理由):否则另一实例在
1144
+ // open 前 / 期间提交的事件会被静默丢掉;调用方刚读过这份日志,guard 没有该会话时记下当前磁盘 head。
1143
1145
  const head = await tx.getHead(id);
1144
1146
  if (!this.writeGuard.has(id)) this.writeGuard.confirmHead(id, head.fHeadSequence);
1145
1147
  this.writeGuard.assertNoConcurrentWriter(id, head.fHeadSequence);
1146
- await tx.deleteBridgeTail(id, 0);
1148
+ removed = await tx.deleteBridgeTail(id, 0);
1147
1149
  await tx.upsertSession(
1148
1150
  { meta: log.meta, inheritedEventCount: SessionLogOffset(log.inheritedEventCount) },
1149
1151
  randomUUID(),
@@ -1157,6 +1159,7 @@ export class SessionPersistenceRdb extends SessionPersistence {
1157
1159
  });
1158
1160
  // 重写换了事件行:被删掉的那批成了孤儿(引用标记要落回 0),本会话汇总按新事件重算。
1159
1161
  await this.rebuildSessionStats(id);
1162
+ await this.refreshEventUsageFlags(removed);
1160
1163
  }
1161
1164
 
1162
1165
  async listSnapshots(
@@ -1225,8 +1228,7 @@ export class SessionPersistenceRdb extends SessionPersistence {
1225
1228
  signal?.throwIfAborted();
1226
1229
  await this.ready;
1227
1230
  signal?.throwIfAborted();
1228
- // 一条读取路径:load 的语义就是「读一次完整快照」,不再经 handle 面再读一遍
1229
- // (原先 open(read) 读一次 + handle.read 又读一次,等于把最贵的读付两遍)。
1231
+ // 一条读取路径:load 的语义是「读一次完整快照」,不经 handle 面再读一遍。
1230
1232
  const log = await this.readLog(id, {}, signal);
1231
1233
  if (log === undefined) {
1232
1234
  // 未落库的 live 会话没有磁盘视图:与 handle.read 的 detached 口径一致,返回空日志。
@@ -1285,7 +1287,7 @@ export class SessionPersistenceRdb extends SessionPersistence {
1285
1287
  this.reuseEventIds.set(childId, new Map(map));
1286
1288
  }
1287
1289
 
1288
- /** 丢弃一个还没被消费的复用映射(fork 失败时):留着会让同一个 childId 之后的落写复用父会话的事件行。 */
1290
+ // 丢弃一个还没被消费的复用映射(fork 失败时):留着会让同一个 childId 之后的落写复用父会话的事件行。
1289
1291
  dropReuseEventIds(childId: SessionId): void {
1290
1292
  this.reuseEventIds.delete(childId);
1291
1293
  }
@@ -1485,7 +1487,7 @@ function seedCoversPrefix(seed: readonly SessionEvent[], prefix: readonly Sessio
1485
1487
  );
1486
1488
  }
1487
1489
 
1488
- /** 一批事件折成活动计数的桶:只数四种类型,按本地日聚合成四项。 */
1490
+ // 一批事件折成活动计数的桶:只数四种类型,按本地日聚合成四项。
1489
1491
  function sessionCountBuckets(events: readonly SessionEvent[]): SessionCountBucket[] {
1490
1492
  const counted = new Map<string, SessionCountBucket>();
1491
1493
  for (const event of events) {
@@ -1498,7 +1500,7 @@ function sessionCountBuckets(events: readonly SessionEvent[]): SessionCountBucke
1498
1500
  return [...counted.values()];
1499
1501
  }
1500
1502
 
1501
- /** 一批用量行折成 token 桶:按「本地日 × 模型」聚合(模型未知落空串,与表口径一致)。 */
1503
+ // 一批用量行折成 token 桶:按「本地日 × 模型」聚合(模型未知落空串,与表口径一致)。
1502
1504
  function sessionUsageBuckets(usageRows: readonly EventUsageRow[]): SessionUsageBucket[] {
1503
1505
  const counted = new Map<string, SessionUsageBucket>();
1504
1506
  for (const row of usageRows) {
package/src/legacy.ts CHANGED
@@ -44,11 +44,9 @@ function physicalEvent(row: EventRow): Record<string, unknown> {
44
44
  };
45
45
  }
46
46
 
47
- /**
48
- * 读旧格式用的 catalog:v3→v4 这条边要求显式子会话证据(上游 0.1.7 起),本仓库声明
49
- * **无子会话证据**——v3 的目录事实由父会话自己在成功路径上写成 `subagent/catalog` 事件,
50
- * 迁移的 child evidence 只是补偿入口;我们没有更权威的子集合来源可给(见 ADR-跟随上游session-format-v4)。
51
- */
47
+ // 读旧格式用的 catalog:v3→v4 这条边要求显式子会话证据,本仓库声明**无子会话证据**——v3 的目录事实由父会话
48
+ // 自己在成功路径上写成 `subagent/catalog` 事件,迁移的 child evidence 只是补偿入口;见
49
+ // ADR-跟随上游session-format-v4。
52
50
  export const restoreCatalog = createSessionFormatCatalogWithChildren([]);
53
51
 
54
52
  export function convertLegacyRows(