@morlay/session-rdb 0.1.0 → 0.1.2
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 -166
- package/dist/artifact.d.mts +2 -2
- package/dist/artifact.mjs +3 -2
- package/dist/{branch-D4nVzQQM.mjs → branch-IQii6Muq.mjs} +1 -8
- package/dist/deletion.d.mts +1 -2
- package/dist/deletion.mjs +2 -1
- package/dist/export.d.mts +1 -3
- package/dist/export.mjs +1 -4
- package/dist/gc.d.mts +1 -3
- package/dist/gc.mjs +1 -3
- package/dist/import.d.mts +1 -2
- package/dist/import.mjs +2 -7
- package/dist/index.d.mts +1 -1
- package/dist/index.mjs +4 -3
- package/dist/{log-BWVFYAh5.mjs → log-D5C8-TGs.mjs} +0 -52
- package/dist/{log-CUjKQbOR.d.mts → log-tZMKFRKi.d.mts} +17 -168
- package/dist/routes-DGBlBE_I.mjs +10 -0
- package/dist/{sqlite-SCiSnwaz.mjs → sqlite-CMMSn5kE.mjs} +24 -62
- package/dist/{src-BZ1tffd5.mjs → src-dsd2H70D.mjs} +59 -97
- package/dist/storage.d.mts +2 -22
- package/dist/storage.mjs +1 -1
- package/dist/testing.d.mts +1 -1
- package/dist/testing.mjs +1 -1
- package/dist/usage.d.mts +1 -1
- package/dist/usage.mjs +1 -17
- package/package.json +19 -19
- package/src/backend.ts +35 -54
- package/src/branch.ts +3 -5
- package/src/deletion.ts +2 -1
- package/src/entities/v3/event-usage.ts +3 -6
- package/src/entities/v3/session-counts.ts +3 -5
- package/src/entities/v3/session-usage.ts +3 -5
- package/src/export.ts +5 -4
- package/src/gc.ts +4 -3
- package/src/import.ts +5 -6
- package/src/index.ts +47 -45
- package/src/legacy.ts +3 -5
- package/src/log.ts +24 -53
- package/src/postgres.ts +70 -61
- package/src/routes.ts +16 -0
- package/src/rows.ts +11 -15
- package/src/session-query.ts +3 -7
- package/src/sqlite.ts +70 -58
- package/src/storage-takeover/projection-cache.ts +6 -8
- package/src/storage-takeover/repository.ts +2 -2
- package/src/storage-takeover/types.ts +1 -1
- package/src/usage.ts +22 -34
- 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
|
-
|
|
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
|
-
|
|
93
|
+
// 写批次提交时递增 revision;给了 `lastEventAt` 就一并把「最后活动时间」往前推(只增不减)。
|
|
96
94
|
bumpRevision(id: SessionId, lastEventAt?: number): Promise<void>;
|
|
97
95
|
|
|
98
|
-
|
|
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
|
-
|
|
104
|
+
// 删除整条会话;返回被删桥接行引用的 event id(引用标记要跟着重算)。
|
|
105
|
+
deleteSession(id: SessionId): Promise<readonly string[]>;
|
|
106
106
|
|
|
107
|
-
|
|
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
|
-
|
|
119
|
+
// 该会话最后一个事件的时间(没有事件时回落 `createdAt`)。
|
|
124
120
|
updatedAt: number;
|
|
125
121
|
archived: boolean;
|
|
126
122
|
subagent: boolean;
|
|
127
|
-
|
|
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
|
-
|
|
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
|
-
|
|
183
|
+
// 父会话已不存在(或没有父)的 subagent 会话:父被删后它们成了孤儿。
|
|
188
184
|
listOrphanSubagentSessions(): Promise<SessionId[]>;
|
|
189
185
|
|
|
190
|
-
|
|
191
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
209
|
+
// 删掉一个会话的汇总行(会话删除时;外键 CASCADE 之外再显式清一次)。
|
|
229
210
|
deleteSessionStats(id: SessionId): Promise<void>;
|
|
230
211
|
|
|
231
|
-
|
|
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
|
-
//
|
|
315
|
-
|
|
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
|
-
//
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
5
|
-
|
|
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
|
|
7
|
+
export { SESSION_EXPORT_PATH };
|
|
7
8
|
|
|
8
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
104
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
624
|
-
//
|
|
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(
|
|
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
|
-
|
|
837
|
+
// GC 通道:回收已无桥接行引用的事件行(孤儿),返回删除行数。
|
|
844
838
|
async collectOrphans(): Promise<number> {
|
|
845
839
|
await this.ready;
|
|
846
840
|
return this.backend.collectOrphans();
|
|
847
841
|
}
|
|
848
842
|
|
|
849
|
-
|
|
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) =>
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1077
|
-
|
|
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
|
-
//
|
|
1140
|
-
//
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
49
|
-
|
|
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(
|