@morlay/session-rdb 0.0.21-alpha.4 → 0.0.21-alpha.6
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 +53 -27
- package/dist/artifact.d.mts +27 -2
- package/dist/artifact.mjs +2 -2
- package/dist/{backend-a0xDrUFG.d.mts → backend-E0bmTXpz.d.mts} +233 -27
- package/dist/{branch-Dlmbkfjj.mjs → branch-BjUR6wfe.mjs} +31 -15
- package/dist/deletion.d.mts +1 -1
- package/dist/deletion.mjs +1 -1
- package/dist/import.d.mts +1 -1
- package/dist/import.mjs +2 -3
- package/dist/index.d.mts +2 -2
- package/dist/index.mjs +4 -4
- package/dist/{log-C-rbdFwK.mjs → log-qCNkDjh5.mjs} +93 -22
- package/dist/{sqlite-C3LHLzYW.mjs → sqlite-zcXVyMxr.mjs} +85 -10
- package/dist/{src-Bn0038Sp.mjs → src-B9X8BKyh.mjs} +210 -38
- package/dist/storage.d.mts +2 -1
- package/dist/storage.mjs +1 -1
- package/dist/testing.d.mts +2 -11
- package/dist/testing.mjs +2 -19
- package/drizzle/postgres/20260921075900_v3_agent_preset/migration.sql +1 -0
- package/drizzle/postgres/20260922090001_v3_pinned_seq/migration.sql +2 -0
- package/drizzle/postgres/20260922093001_v3_last_event_at/migration.sql +10 -0
- package/drizzle/sqlite/20260921075855_v3_agent_preset/migration.sql +7 -0
- package/drizzle/sqlite/20260921075855_v3_agent_preset/snapshot.json +1041 -0
- package/drizzle/sqlite/20260922090000_v3_pinned_seq/migration.sql +4 -0
- package/drizzle/sqlite/20260922093000_v3_last_event_at/migration.sql +13 -0
- package/package.json +21 -21
- package/src/backend.ts +46 -1
- package/src/branch.ts +28 -11
- package/src/drizzle/postgres-v3.ts +1 -0
- package/src/drizzle/sqlite-v3.ts +1 -0
- package/src/entities/v3/event-usage.ts +5 -5
- package/src/entities/v3/sessions.ts +5 -0
- package/src/import.ts +3 -3
- package/src/index.ts +56 -22
- package/src/legacy.ts +14 -3
- package/src/log.ts +107 -22
- package/src/postgres.ts +65 -5
- package/src/rows.ts +125 -0
- package/src/schema.ts +3 -2
- package/src/session-query.ts +27 -1
- package/src/sqlite.ts +71 -3
- package/src/storage-takeover/projection-cache.ts +40 -15
- package/src/storage-takeover/repository.ts +34 -0
- package/src/storage-takeover/storage-backend.ts +3 -1
- package/src/testing/coordinator-contract.ts +2 -1
- package/src/testing.ts +0 -1
- package/src/write-guard.ts +5 -0
- package/src/testing/helpers.ts +0 -20
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
-- 「最后活动时间」物化到会话行:管理面的会话列表要按它排序 + 分页,原先每行一次「该会话最后事件时间」的
|
|
2
|
+
-- 相关子查询(真实库 243 行 = 4.2s)在分页下也救不了——排序要求全表先把值算出来。加列后排序与分页都只
|
|
3
|
+
-- 碰 t_sessions(毫秒级),写路径负责维护;这里一次性回填,没有事件的会话留 NULL(读时回落 createdAt)。
|
|
4
|
+
-- 类型写 `integer`(不是 `bigint`):STRICT 表只认固定类型名,`bigint` 会被拒。
|
|
5
|
+
ALTER TABLE `t_sessions` ADD `f_last_event_at` integer;
|
|
6
|
+
--> statement-breakpoint
|
|
7
|
+
UPDATE `t_sessions`
|
|
8
|
+
SET `f_last_event_at` = (
|
|
9
|
+
SELECT MAX(e.f_created_at)
|
|
10
|
+
FROM t_session_events se
|
|
11
|
+
JOIN t_events e ON e.f_event_id = se.f_event_id
|
|
12
|
+
WHERE se.f_session_id = t_sessions.f_session_id
|
|
13
|
+
);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@morlay/session-rdb",
|
|
3
|
-
"version": "0.0.21-alpha.
|
|
3
|
+
"version": "0.0.21-alpha.6",
|
|
4
4
|
"description": "RDB durable session backend for DeepSeek Harness: implements both session-persistence and session-branch (rewind / retry / fork) providers.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"branch",
|
|
@@ -39,35 +39,35 @@
|
|
|
39
39
|
"./cordis.patch.yml": "./cordis.patch.yml"
|
|
40
40
|
},
|
|
41
41
|
"dependencies": {
|
|
42
|
-
"@morlay/session-branch": "0.0.10-alpha.
|
|
42
|
+
"@morlay/session-branch": "0.0.10-alpha.6",
|
|
43
43
|
"drizzle-orm": "^1.0.0-rc.5-ab785fc",
|
|
44
44
|
"fflate": "^0.8.2",
|
|
45
45
|
"pg": "^8.23.0"
|
|
46
46
|
},
|
|
47
47
|
"devDependencies": {
|
|
48
|
-
"@deepseek-ai/dsh-
|
|
49
|
-
"@deepseek-ai/dsh-session-projection
|
|
50
|
-
"@deepseek-ai/dsh-session-
|
|
51
|
-
"@deepseek-ai/dsh-
|
|
52
|
-
"@deepseek-ai/dsh-storage
|
|
53
|
-
"@deepseek-ai/dsh-
|
|
54
|
-
"@deepseek-ai/dsh-
|
|
48
|
+
"@deepseek-ai/dsh-compaction": "0.1.7-alpha.1",
|
|
49
|
+
"@deepseek-ai/dsh-session-projection": "0.1.7-alpha.1",
|
|
50
|
+
"@deepseek-ai/dsh-session-projection-cache": "0.1.7-alpha.1",
|
|
51
|
+
"@deepseek-ai/dsh-session-turn-outline": "0.1.7-alpha.1",
|
|
52
|
+
"@deepseek-ai/dsh-storage": "0.1.7-alpha.1",
|
|
53
|
+
"@deepseek-ai/dsh-storage-domain": "0.1.7-alpha.1",
|
|
54
|
+
"@deepseek-ai/dsh-token-meter": "0.1.7-alpha.1",
|
|
55
|
+
"@deepseek-ai/dsh-workspace": "0.1.7-alpha.1",
|
|
55
56
|
"@types/pg": "^8.23.1",
|
|
56
57
|
"drizzle-kit": "1.0.0-rc.5-ab785fc"
|
|
57
58
|
},
|
|
58
59
|
"peerDependencies": {
|
|
59
|
-
"@deepseek-ai/cordis": "4.0.
|
|
60
|
-
"@deepseek-ai/dsh-invariants": "0.1.
|
|
61
|
-
"@deepseek-ai/dsh-llm": "0.1.
|
|
62
|
-
"@deepseek-ai/dsh-scope": "0.1.
|
|
63
|
-
"@deepseek-ai/dsh-session": "0.1.
|
|
64
|
-
"@deepseek-ai/dsh-session-format": "0.1.
|
|
65
|
-
"@deepseek-ai/dsh-session-format-catalog": "0.1.
|
|
66
|
-
"@deepseek-ai/dsh-session-persistence": "0.1.
|
|
67
|
-
"@deepseek-ai/dsh-session-query": "0.1.
|
|
68
|
-
"@deepseek-ai/dsh-
|
|
69
|
-
"@deepseek-ai/
|
|
70
|
-
"@deepseek-ai/schemastery": "3.18.2"
|
|
60
|
+
"@deepseek-ai/cordis": "4.0.3",
|
|
61
|
+
"@deepseek-ai/dsh-invariants": "0.1.7-alpha.1",
|
|
62
|
+
"@deepseek-ai/dsh-llm": "0.1.7-alpha.1",
|
|
63
|
+
"@deepseek-ai/dsh-scope": "0.1.7-alpha.1",
|
|
64
|
+
"@deepseek-ai/dsh-session": "0.1.7-alpha.1",
|
|
65
|
+
"@deepseek-ai/dsh-session-format": "0.1.7-alpha.1",
|
|
66
|
+
"@deepseek-ai/dsh-session-format-catalog": "0.1.7-alpha.1",
|
|
67
|
+
"@deepseek-ai/dsh-session-persistence": "0.1.7-alpha.1",
|
|
68
|
+
"@deepseek-ai/dsh-session-query": "0.1.7-alpha.1",
|
|
69
|
+
"@deepseek-ai/dsh-storage": "0.1.7-alpha.1",
|
|
70
|
+
"@deepseek-ai/schemastery": "3.18.3"
|
|
71
71
|
},
|
|
72
72
|
"dsh": {
|
|
73
73
|
"bundle": {
|
package/src/backend.ts
CHANGED
|
@@ -15,12 +15,17 @@ export interface SessionRow {
|
|
|
15
15
|
fSeedLength: number | null;
|
|
16
16
|
fOrigin: string | null;
|
|
17
17
|
fDelegationDepth: number | null;
|
|
18
|
+
fAgentPreset: string | null;
|
|
18
19
|
|
|
19
20
|
fIncarnation: string;
|
|
20
21
|
|
|
21
22
|
fRevision: number;
|
|
22
23
|
|
|
23
24
|
fArchivedAt: number | null;
|
|
25
|
+
|
|
26
|
+
fPinnedSeq: number | null;
|
|
27
|
+
|
|
28
|
+
fLastEventAt: number | null;
|
|
24
29
|
}
|
|
25
30
|
|
|
26
31
|
export interface EventInsert {
|
|
@@ -82,7 +87,8 @@ export interface BackendTx {
|
|
|
82
87
|
|
|
83
88
|
updateHead(id: SessionId, headEventId: string, headSequence: number): Promise<void>;
|
|
84
89
|
|
|
85
|
-
|
|
90
|
+
/** 写批次提交时递增 revision;给了 `lastEventAt` 就一并把「最后活动时间」往前推(只增不减)。 */
|
|
91
|
+
bumpRevision(id: SessionId, lastEventAt?: number): Promise<void>;
|
|
86
92
|
|
|
87
93
|
deleteBridgeTail(id: SessionId, fromSequence: number): Promise<void>;
|
|
88
94
|
|
|
@@ -97,6 +103,42 @@ export interface BackendTx {
|
|
|
97
103
|
deleteSessions(ids: SessionId[]): Promise<number>;
|
|
98
104
|
}
|
|
99
105
|
|
|
106
|
+
/**
|
|
107
|
+
* 「会话行」列表项:管理面自己的列表用(完整语料,含归档)+ 标题 + 最后活动时间。
|
|
108
|
+
*
|
|
109
|
+
* 与官方列表(`session/list` → `ctx.sessionQuery.listSessions`)的区别:那条按部署策略**默认排除归档**,
|
|
110
|
+
* 这里给的是完整集合,标题也直接带出来(官方那条的标题走投影缓存)。
|
|
111
|
+
*/
|
|
112
|
+
export interface SessionListRowRecord {
|
|
113
|
+
sessionId: string;
|
|
114
|
+
title: string | null;
|
|
115
|
+
origin: string | null;
|
|
116
|
+
cwd: string | null;
|
|
117
|
+
createdAt: number;
|
|
118
|
+
/** 该会话最后一个事件的时间(没有事件时回落 `createdAt`)。 */
|
|
119
|
+
updatedAt: number;
|
|
120
|
+
archived: boolean;
|
|
121
|
+
subagent: boolean;
|
|
122
|
+
/** 所属工作区标题;不属于任何工作区时为 null。 */
|
|
123
|
+
workspace: string | null;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** 会话行列表的查询条件:搜索、子代理过滤与分页都在后端做(前端分页等于每次拉全量)。 */
|
|
127
|
+
export interface SessionListRowsQuery {
|
|
128
|
+
/** 匹配标题或所属工作区标题(大小写不敏感);空串即不过滤。 */
|
|
129
|
+
query?: string;
|
|
130
|
+
/** 是否连子代理派生会话一起返回(默认不含)。 */
|
|
131
|
+
includeSubagents?: boolean;
|
|
132
|
+
limit?: number;
|
|
133
|
+
offset?: number;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export interface SessionListRowsPage {
|
|
137
|
+
items: SessionListRowRecord[];
|
|
138
|
+
/** 过滤后的总数(分页前的),前端据此算页数。 */
|
|
139
|
+
total: number;
|
|
140
|
+
}
|
|
141
|
+
|
|
100
142
|
/** 活动计数的一个桶:本地日 + 事件类型 + 计数。 */
|
|
101
143
|
export interface EventCountBucket {
|
|
102
144
|
day: string;
|
|
@@ -142,6 +184,9 @@ export interface Backend {
|
|
|
142
184
|
*/
|
|
143
185
|
usageReport(sinceMs?: number): Promise<UsageAggregate>;
|
|
144
186
|
|
|
187
|
+
/** 管理面的会话行列表:完整语料(含归档)+ 标题 + 最后活动时间,按活动倒序分页。 */
|
|
188
|
+
listSessionRows(query?: SessionListRowsQuery): Promise<SessionListRowsPage>;
|
|
189
|
+
|
|
145
190
|
/**
|
|
146
191
|
* 活动计数**旁路累加**(派生表 `t_event_counts`,不参与写事务、失败可丢——表可销毁重建):
|
|
147
192
|
* 每个 (会话, 本地日, 事件类型) 记一行计数。
|
package/src/branch.ts
CHANGED
|
@@ -278,21 +278,38 @@ export class SessionBranchRdbProvider implements SessionBranchProvider {
|
|
|
278
278
|
const seed = [...renumber(prefix, 0), ...renumber(seedSuffix, prefix.length)];
|
|
279
279
|
|
|
280
280
|
const internals = this.persistence.internals();
|
|
281
|
-
|
|
282
|
-
|
|
281
|
+
// 复用父会话的事件行有个前提:**读取视图的 seq 与存储行的 f_sequence 一一对应**。legacy 会话的读视图
|
|
282
|
+
// 由迁移链重建(可能重新编号、增删事件),按 seq 复用会把桥接行挂到语义不符的行上——读取时按 `f_data`
|
|
283
|
+
// 解析,子会话前缀读到的内容就会和它自己的 seed 事件对不上。那种情况下复制事件行(多几行存储,正确优先)。
|
|
284
|
+
const reusable = !source.migrated && source.events.length === source.storedCount;
|
|
283
285
|
const reuse = new Map<number, string>();
|
|
284
|
-
|
|
285
|
-
const
|
|
286
|
-
|
|
286
|
+
if (reusable) {
|
|
287
|
+
const sourceRows = await internals.backend.getEventRows(sourceId);
|
|
288
|
+
const sourceEventIds = new Map(sourceRows.map((row) => [row.fSequence, row.fEventId]));
|
|
289
|
+
for (const event of prefix) {
|
|
290
|
+
const eventId = sourceEventIds.get(event.seq);
|
|
291
|
+
if (eventId !== undefined) reuse.set(event.seq, eventId);
|
|
292
|
+
}
|
|
293
|
+
} else if (prefix.length > 0) {
|
|
294
|
+
this.live.warn?.(
|
|
295
|
+
`session-rdb: fork "${sourceId}" copies event rows instead of reusing them — its read view comes from a migration, so stored seqs need not line up`,
|
|
296
|
+
);
|
|
287
297
|
}
|
|
288
298
|
internals.registerReuseEventIds(childId, reuse);
|
|
289
|
-
const handle = await this.persistence.create(childMeta, {
|
|
290
|
-
inheritedEventCount: SessionLogOffset(prefix.length),
|
|
291
|
-
});
|
|
292
299
|
try {
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
300
|
+
const handle = await this.persistence.create(childMeta, {
|
|
301
|
+
inheritedEventCount: SessionLogOffset(prefix.length),
|
|
302
|
+
});
|
|
303
|
+
try {
|
|
304
|
+
if (seed.length > 0) await handle.append(seed);
|
|
305
|
+
} finally {
|
|
306
|
+
await handle.close();
|
|
307
|
+
}
|
|
308
|
+
} catch (error) {
|
|
309
|
+
// fork 没成(id 已存在、append 失败等):别把复用映射留在进程里——同一个 childId 之后的落写会按它
|
|
310
|
+
// 去复用父会话的事件行,而那条会话可能完全无关。
|
|
311
|
+
internals.dropReuseEventIds(childId);
|
|
312
|
+
throw error;
|
|
296
313
|
}
|
|
297
314
|
// 子会话继承了前缀事件:活动计数按子会话重算(派生表,best-effort)。
|
|
298
315
|
await this.persistence.rebuildEventCounts(childId);
|
|
@@ -13,3 +13,4 @@ export const tWorkspaces = tables["t_workspaces"]!;
|
|
|
13
13
|
export const tWorkspaceSessions = tables["t_workspace_sessions"]!;
|
|
14
14
|
export const tWorkspaceState = tables["t_workspace_state"]!;
|
|
15
15
|
export const tSessionProjcacheRows = tables["t_session_projcache_row"]!;
|
|
16
|
+
export const tEventUsage = tables["t_event_usage"]!;
|
package/src/drizzle/sqlite-v3.ts
CHANGED
|
@@ -13,3 +13,4 @@ export const tWorkspaces = tables["t_workspaces"]!;
|
|
|
13
13
|
export const tWorkspaceSessions = tables["t_workspace_sessions"]!;
|
|
14
14
|
export const tWorkspaceState = tables["t_workspace_state"]!;
|
|
15
15
|
export const tSessionProjcacheRows = tables["t_session_projcache_row"]!;
|
|
16
|
+
export const tEventUsage = tables["t_event_usage"]!;
|
|
@@ -16,11 +16,11 @@ export const eventUsage: TableDef = {
|
|
|
16
16
|
f_created_at: { type: "bigint", notNull: true },
|
|
17
17
|
f_provider: { type: "text" },
|
|
18
18
|
f_model: { type: "text" },
|
|
19
|
-
f_input_tokens: { type: "integer", notNull: true },
|
|
20
|
-
f_output_tokens: { type: "integer", notNull: true },
|
|
21
|
-
f_cache_read_tokens: { type: "integer", notNull: true },
|
|
22
|
-
f_reasoning_tokens: { type: "integer", notNull: true },
|
|
23
|
-
f_total_tokens: { type: "integer", notNull: true },
|
|
19
|
+
f_input_tokens: { type: "integer", notNull: true, default: 0 },
|
|
20
|
+
f_output_tokens: { type: "integer", notNull: true, default: 0 },
|
|
21
|
+
f_cache_read_tokens: { type: "integer", notNull: true, default: 0 },
|
|
22
|
+
f_reasoning_tokens: { type: "integer", notNull: true, default: 0 },
|
|
23
|
+
f_total_tokens: { type: "integer", notNull: true, default: 0 },
|
|
24
24
|
},
|
|
25
25
|
indexes: { idx_event_usage_created_at: { columns: ["f_created_at"] } },
|
|
26
26
|
};
|
|
@@ -14,11 +14,16 @@ export const sessions: TableDef = {
|
|
|
14
14
|
f_seed_length: { type: "integer" },
|
|
15
15
|
f_origin: { type: "text" },
|
|
16
16
|
f_delegation_depth: { type: "integer" },
|
|
17
|
+
f_agent_preset: { type: "text" },
|
|
17
18
|
f_incarnation: { type: "text", notNull: true },
|
|
18
19
|
f_revision: { type: "integer", notNull: true },
|
|
19
20
|
|
|
20
21
|
f_archived_at: { type: "bigint" },
|
|
21
22
|
|
|
23
|
+
f_pinned_seq: { type: "integer" },
|
|
24
|
+
|
|
25
|
+
f_last_event_at: { type: "bigint" },
|
|
26
|
+
|
|
22
27
|
f_title: { type: "text" },
|
|
23
28
|
|
|
24
29
|
f_title_seq: { type: "integer" },
|
package/src/import.ts
CHANGED
|
@@ -3,11 +3,11 @@ import type { Context } from "@deepseek-ai/cordis";
|
|
|
3
3
|
import { SESSION_FORMAT_VERSION, SessionLogOffset } from "@deepseek-ai/dsh-session";
|
|
4
4
|
import type { Session, SessionEvent, SessionId, SessionHeader } from "@deepseek-ai/dsh-session";
|
|
5
5
|
import { parseSessionFormatLogFilename } from "@deepseek-ai/dsh-session-format";
|
|
6
|
-
import { sessionFormatCatalog } from "@deepseek-ai/dsh-session-format-catalog";
|
|
7
6
|
import type { SessionStorageMetadata } from "@deepseek-ai/dsh-session-persistence";
|
|
8
7
|
import { balanceRewindPrefix } from "@morlay/session-branch";
|
|
9
8
|
import { unzip } from "fflate";
|
|
10
9
|
import { replaceLiveSessionLog } from "./branch.ts";
|
|
10
|
+
import { restoreCatalog } from "./legacy.ts";
|
|
11
11
|
import type { SessionPersistenceRdb } from "./index.ts";
|
|
12
12
|
|
|
13
13
|
export const SESSION_LOG_ARTIFACT_FILENAME = "session.jsonl";
|
|
@@ -30,9 +30,9 @@ export function parseJsonlArtifact(content: string): SessionStorageMetadata & {
|
|
|
30
30
|
throw new Error("imported session log has an unparsable header line");
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
-
let restore: ReturnType<typeof
|
|
33
|
+
let restore: ReturnType<typeof restoreCatalog.createRestore>;
|
|
34
34
|
try {
|
|
35
|
-
restore =
|
|
35
|
+
restore = restoreCatalog.createRestore(header, {
|
|
36
36
|
recovery: "strict",
|
|
37
37
|
validation: "transformed",
|
|
38
38
|
});
|
package/src/index.ts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { Context } from "@deepseek-ai/cordis";
|
|
2
2
|
import z from "@deepseek-ai/schemastery";
|
|
3
|
-
import type { SettingsProvider } from "@deepseek-ai/dsh-settings";
|
|
4
3
|
import { randomUUID } from "node:crypto";
|
|
5
4
|
import { Pool } from "pg";
|
|
6
5
|
import { drizzle as drizzlePg } from "drizzle-orm/node-postgres";
|
|
@@ -61,16 +60,31 @@ import { registerSessionImport } from "./import.ts";
|
|
|
61
60
|
import { registerSessionDeletion } from "./deletion.ts";
|
|
62
61
|
import { registerSessionExport } from "./export.ts";
|
|
63
62
|
import { registerSessionGc } from "./gc.ts";
|
|
63
|
+
import { registerSessionRows } from "./rows.ts";
|
|
64
64
|
import { COUNTED_EVENT_TYPES, localDayKey, registerSessionUsage } from "./usage.ts";
|
|
65
65
|
import type { UsageAggregate } from "./usage.ts";
|
|
66
66
|
import { SessionQueryRdb } from "./session-query.ts";
|
|
67
67
|
import { adoptLegacyRows, convertLegacyRows, isLegacyVersion } from "./legacy.ts";
|
|
68
|
+
import { hasLegacyShape } from "./log.ts";
|
|
68
69
|
import { installStorageTakeover } from "./storage-takeover/index.ts";
|
|
69
70
|
|
|
71
|
+
/** 一批事件里的最大时间:写路径据此推进会话行的「最后活动时间」。 */
|
|
72
|
+
function maxEventTime(events: readonly { readonly time?: number }[]): number | undefined {
|
|
73
|
+
let max: number | undefined;
|
|
74
|
+
for (const event of events) {
|
|
75
|
+
const time = event.time;
|
|
76
|
+
if (typeof time !== "number") continue;
|
|
77
|
+
if (max === undefined || time > max) max = time;
|
|
78
|
+
}
|
|
79
|
+
return max;
|
|
80
|
+
}
|
|
81
|
+
|
|
70
82
|
const DEFAULT_PROJECTION_WRITE_EVERY_EVENTS = 200;
|
|
71
83
|
const DEFAULT_PROJECTION_WRITE_INTERVAL_MS = 5000;
|
|
72
84
|
|
|
73
85
|
export { SCHEMA_VERSION } from "./schema.ts";
|
|
86
|
+
export { SESSION_ROWS_PATH } from "./rows.ts";
|
|
87
|
+
export type { SessionRowsItem, SessionRowsValue } from "./rows.ts";
|
|
74
88
|
export { SessionBranchRdb, SessionBranchRdbProvider, locateTurnEnd } from "./branch.ts";
|
|
75
89
|
|
|
76
90
|
export interface SessionPersistenceRdbInternals {
|
|
@@ -95,6 +109,8 @@ export interface SessionPersistenceRdbInternals {
|
|
|
95
109
|
): Promise<import("@deepseek-ai/dsh-session-persistence").SessionPersistenceRevision | undefined>;
|
|
96
110
|
|
|
97
111
|
registerReuseEventIds(childId: SessionId, map: ReadonlyMap<number, string>): void;
|
|
112
|
+
/** fork 失败时丢弃还没被消费的映射。 */
|
|
113
|
+
dropReuseEventIds(childId: SessionId): void;
|
|
98
114
|
}
|
|
99
115
|
|
|
100
116
|
export interface ProjectionCacheOptions {
|
|
@@ -469,7 +485,7 @@ class RdbSessionHandle implements SessionHandle {
|
|
|
469
485
|
}
|
|
470
486
|
|
|
471
487
|
export class SessionPersistenceRdb extends SessionPersistence {
|
|
472
|
-
static inject = ["sessions"
|
|
488
|
+
static inject = ["sessions"];
|
|
473
489
|
|
|
474
490
|
static Config: z<Config> = z.union([
|
|
475
491
|
z.object({
|
|
@@ -503,8 +519,6 @@ export class SessionPersistenceRdb extends SessionPersistence {
|
|
|
503
519
|
}),
|
|
504
520
|
]);
|
|
505
521
|
|
|
506
|
-
static readonly settingsNs = "session-rdb";
|
|
507
|
-
|
|
508
522
|
override readonly name = "session-rdb";
|
|
509
523
|
|
|
510
524
|
readonly tracker = new RdbBackendTracker(this.name);
|
|
@@ -526,23 +540,11 @@ export class SessionPersistenceRdb extends SessionPersistence {
|
|
|
526
540
|
|
|
527
541
|
injectedBackend?: Backend,
|
|
528
542
|
) {
|
|
529
|
-
let resolved: Config = config;
|
|
530
|
-
const settings = ctx.reflect.get("settings") as unknown as SettingsProvider | undefined;
|
|
531
|
-
if (settings !== undefined) {
|
|
532
|
-
const scope = settings.register(
|
|
533
|
-
SessionPersistenceRdb.settingsNs,
|
|
534
|
-
SessionPersistenceRdb.Config,
|
|
535
|
-
{ base: config },
|
|
536
|
-
);
|
|
537
|
-
resolved = scope.get();
|
|
538
|
-
scope.watch(() => {
|
|
539
|
-
ctx.logger.warn("session-rdb: settings changed; restart to apply the new configuration");
|
|
540
|
-
});
|
|
541
|
-
}
|
|
542
543
|
super(ctx);
|
|
543
544
|
|
|
544
|
-
|
|
545
|
-
|
|
545
|
+
// 配置就是这一行的 config(cordis.patch.yml / profile patch,或设置页改它);上游 0.1.7 的 settings
|
|
546
|
+
// 不再提供 namespace section 覆盖,旧 `settings.yaml` 的 `session-rdb` 段由上游一次性导进同 id 的行。
|
|
547
|
+
this.backend = injectedBackend ?? createBackend(config);
|
|
546
548
|
this.ready = this.init();
|
|
547
549
|
this.installLiveRouting(ctx);
|
|
548
550
|
|
|
@@ -560,6 +562,8 @@ export class SessionPersistenceRdb extends SessionPersistence {
|
|
|
560
562
|
|
|
561
563
|
registerSessionUsage(this.ctx, this);
|
|
562
564
|
|
|
565
|
+
registerSessionRows(this.ctx, this.backend);
|
|
566
|
+
|
|
563
567
|
installStorageTakeover(this.ctx, {
|
|
564
568
|
repository: this.backend.storage,
|
|
565
569
|
ready: this.ready,
|
|
@@ -853,7 +857,6 @@ export class SessionPersistenceRdb extends SessionPersistence {
|
|
|
853
857
|
validateStoredEvents(meta, [...events]);
|
|
854
858
|
|
|
855
859
|
const reuse = this.reuseEventIds.get(meta.id);
|
|
856
|
-
if (reuse !== undefined) this.reuseEventIds.delete(meta.id);
|
|
857
860
|
let confirmedHead = -1;
|
|
858
861
|
await this.backend.transaction(async (tx) => {
|
|
859
862
|
if (tornTruncateTo !== undefined) {
|
|
@@ -877,9 +880,12 @@ export class SessionPersistenceRdb extends SessionPersistence {
|
|
|
877
880
|
reuse,
|
|
878
881
|
);
|
|
879
882
|
await tx.updateHead(meta.id, headEventId, headSequence);
|
|
880
|
-
await tx.bumpRevision(meta.id);
|
|
883
|
+
await tx.bumpRevision(meta.id, maxEventTime(events));
|
|
881
884
|
confirmedHead = headSequence;
|
|
882
885
|
});
|
|
886
|
+
// 事务**成功之后**才丢掉复用映射:失败(并发写者校验、唯一键冲突等)时这份映射还没被消费,
|
|
887
|
+
// 重试同一个 append 还要靠它复用父会话的事件行——提前删掉会让前缀事件行被重新插入一遍。
|
|
888
|
+
if (reuse !== undefined) this.reuseEventIds.delete(meta.id);
|
|
883
889
|
|
|
884
890
|
this.writeGuard.confirmHead(meta.id, confirmedHead);
|
|
885
891
|
this.tracker.materialized(meta.id);
|
|
@@ -951,6 +957,21 @@ export class SessionPersistenceRdb extends SessionPersistence {
|
|
|
951
957
|
}
|
|
952
958
|
}
|
|
953
959
|
const { preserved, tornFrom } = scanRows(eventRows, options.fromSeq ?? 0);
|
|
960
|
+
// 版本号不完全可信:写路径曾把回退视图的结果以当前版本号落库,于是库里存在「v4 标记 + 旧代形状」
|
|
961
|
+
// 的行——在扫干净的边界之内再按内容判一次(撕裂尾部与坏行已经被 `scanRows` 丢掉了)。
|
|
962
|
+
if (hasLegacyShape(preserved)) {
|
|
963
|
+
const adopted = adoptLegacyRows(row, eventRows);
|
|
964
|
+
return {
|
|
965
|
+
meta: adopted.meta,
|
|
966
|
+
inheritedEventCount: adopted.inheritedEventCount,
|
|
967
|
+
events: adopted.events,
|
|
968
|
+
incarnation: row.fIncarnation,
|
|
969
|
+
revision: row.fRevision,
|
|
970
|
+
storedCount: eventRows.length,
|
|
971
|
+
migrated: false,
|
|
972
|
+
...(adopted.tornFrom !== undefined ? { tornFrom: adopted.tornFrom } : {}),
|
|
973
|
+
};
|
|
974
|
+
}
|
|
954
975
|
return {
|
|
955
976
|
meta,
|
|
956
977
|
inheritedEventCount: row.fSeedLength ?? 0,
|
|
@@ -1006,6 +1027,13 @@ export class SessionPersistenceRdb extends SessionPersistence {
|
|
|
1006
1027
|
log: { meta: SessionHeader; inheritedEventCount: number; events: SessionEvent[] },
|
|
1007
1028
|
): Promise<void> {
|
|
1008
1029
|
await this.backend.transaction(async (tx) => {
|
|
1030
|
+
// 这段重写会删光该会话的桥接行、再按迁移视图重建——与 `appendBatch` 同一个理由,先做并发写者校验:
|
|
1031
|
+
// 否则另一实例在 open 之前 / 期间提交的事件会被这段重写静默丢掉。调用方刚读过这份日志(重写只发生在
|
|
1032
|
+
// write open 的读路径之后),所以 guard 里没有该会话时就把当前磁盘 head 记成本实例已确认的值;
|
|
1033
|
+
// 有记录时严格比较——那能抓住"读完之后另一个实例又写了"。
|
|
1034
|
+
const head = await tx.getHead(id);
|
|
1035
|
+
if (!this.writeGuard.has(id)) this.writeGuard.confirmHead(id, head.fHeadSequence);
|
|
1036
|
+
this.writeGuard.assertNoConcurrentWriter(id, head.fHeadSequence);
|
|
1009
1037
|
await tx.deleteBridgeTail(id, 0);
|
|
1010
1038
|
await tx.upsertSession(
|
|
1011
1039
|
{ meta: log.meta, inheritedEventCount: SessionLogOffset(log.inheritedEventCount) },
|
|
@@ -1016,7 +1044,7 @@ export class SessionPersistenceRdb extends SessionPersistence {
|
|
|
1016
1044
|
nextSeq: 0,
|
|
1017
1045
|
});
|
|
1018
1046
|
await tx.updateHead(id, headEventId, headSequence);
|
|
1019
|
-
await tx.bumpRevision(id);
|
|
1047
|
+
await tx.bumpRevision(id, maxEventTime(log.events));
|
|
1020
1048
|
});
|
|
1021
1049
|
}
|
|
1022
1050
|
|
|
@@ -1146,6 +1174,11 @@ export class SessionPersistenceRdb extends SessionPersistence {
|
|
|
1146
1174
|
this.reuseEventIds.set(childId, new Map(map));
|
|
1147
1175
|
}
|
|
1148
1176
|
|
|
1177
|
+
/** 丢弃一个还没被消费的复用映射(fork 失败时):留着会让同一个 childId 之后的落写复用父会话的事件行。 */
|
|
1178
|
+
dropReuseEventIds(childId: SessionId): void {
|
|
1179
|
+
this.reuseEventIds.delete(childId);
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1149
1182
|
internals(): SessionPersistenceRdbInternals {
|
|
1150
1183
|
return {
|
|
1151
1184
|
backend: this.backend,
|
|
@@ -1164,6 +1197,7 @@ export class SessionPersistenceRdb extends SessionPersistence {
|
|
|
1164
1197
|
listSnapshots: (signal) => this.listSnapshots(signal),
|
|
1165
1198
|
readStoredRevision: (id, signal) => this.readStoredRevision(id, signal),
|
|
1166
1199
|
registerReuseEventIds: (childId, map) => this.registerReuseEventIds(childId, map),
|
|
1200
|
+
dropReuseEventIds: (childId) => this.dropReuseEventIds(childId),
|
|
1167
1201
|
};
|
|
1168
1202
|
}
|
|
1169
1203
|
|
package/src/legacy.ts
CHANGED
|
@@ -4,9 +4,9 @@ import {
|
|
|
4
4
|
type SessionHeader,
|
|
5
5
|
type SessionId,
|
|
6
6
|
} from "@deepseek-ai/dsh-session";
|
|
7
|
-
import {
|
|
7
|
+
import { createSessionFormatCatalogWithChildren } from "@deepseek-ai/dsh-session-format-catalog";
|
|
8
8
|
import type { EventRow, SessionRow } from "./backend.ts";
|
|
9
|
-
import { repairRequestHeaders, rowToMeta, scanRows } from "./log.ts";
|
|
9
|
+
import { normalizeToCurrentShape, repairRequestHeaders, rowToMeta, scanRows } from "./log.ts";
|
|
10
10
|
|
|
11
11
|
function physicalHeader(row: SessionRow): Record<string, unknown> {
|
|
12
12
|
const common = {
|
|
@@ -18,6 +18,7 @@ function physicalHeader(row: SessionRow): Record<string, unknown> {
|
|
|
18
18
|
...(row.fParentSession !== null ? { parentSession: row.fParentSession } : {}),
|
|
19
19
|
...(row.fOrigin !== null ? { origin: row.fOrigin } : {}),
|
|
20
20
|
delegationDepth: row.fDelegationDepth ?? 0,
|
|
21
|
+
...(row.fAgentPreset !== null ? { agentPreset: row.fAgentPreset } : {}),
|
|
21
22
|
};
|
|
22
23
|
|
|
23
24
|
return row.fVersion < 2
|
|
@@ -43,11 +44,18 @@ function physicalEvent(row: EventRow): Record<string, unknown> {
|
|
|
43
44
|
};
|
|
44
45
|
}
|
|
45
46
|
|
|
47
|
+
/**
|
|
48
|
+
* 读旧格式用的 catalog:v3→v4 这条边要求显式子会话证据(上游 0.1.7 起),本仓库声明
|
|
49
|
+
* **无子会话证据**——v3 的目录事实由父会话自己在成功路径上写成 `subagent/catalog` 事件,
|
|
50
|
+
* 迁移的 child evidence 只是补偿入口;我们没有更权威的子集合来源可给(见 ADR-跟随上游session-format-v4)。
|
|
51
|
+
*/
|
|
52
|
+
export const restoreCatalog = createSessionFormatCatalogWithChildren([]);
|
|
53
|
+
|
|
46
54
|
export function convertLegacyRows(
|
|
47
55
|
row: SessionRow,
|
|
48
56
|
eventRows: readonly EventRow[],
|
|
49
57
|
): { meta: SessionHeader; inheritedEventCount: number; events: SessionEvent[] } {
|
|
50
|
-
const restore =
|
|
58
|
+
const restore = restoreCatalog.createRestore(physicalHeader(row), {
|
|
51
59
|
recovery: "strict",
|
|
52
60
|
validation: "transformed",
|
|
53
61
|
});
|
|
@@ -77,7 +85,10 @@ export function adoptLegacyRows(
|
|
|
77
85
|
} {
|
|
78
86
|
const { preserved, tornFrom } = scanRows(eventRows, 0);
|
|
79
87
|
|
|
88
|
+
// 回退视图要独立扛住 v4 的校验:迁移链(严格)拒绝的那些旧形状在这里按字段归一
|
|
89
|
+
// (system 消息的 source、tool/result 的消息形状、自造事件类型的 ignorable 信封)。
|
|
80
90
|
repairRequestHeaders(preserved);
|
|
91
|
+
normalizeToCurrentShape(preserved);
|
|
81
92
|
return {
|
|
82
93
|
meta: { ...rowToMeta(row), version: SESSION_FORMAT_VERSION },
|
|
83
94
|
|