@morlay/session-rdb 0.0.20 → 0.0.21-alpha.0
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 +59 -31
- package/dist/artifact.d.mts +25 -65
- package/dist/artifact.mjs +2 -2
- package/dist/{schema-BkQN5GyT.d.mts → backend-DpdtxYpz.d.mts} +346 -93
- package/dist/{branch-Co6xlbi0.mjs → branch-5JzX9rUq.mjs} +163 -52
- package/dist/deletion.d.mts +7 -0
- package/dist/deletion.mjs +2 -0
- package/dist/dist-vIVO6bA-.mjs +1524 -0
- package/dist/import.d.mts +4 -4
- package/dist/import.mjs +28 -11
- package/dist/index.d.mts +2 -3
- package/dist/index.mjs +4 -1631
- package/dist/{log-DO69NQnn.mjs → log-CnYct2Dv.mjs} +37 -82
- package/dist/{sqlite-DYExtbLo.mjs → sqlite-fpvm5Dzs.mjs} +205 -75
- package/dist/src-CWTWV7vx.mjs +1904 -0
- package/dist/storage.d.mts +14 -5
- package/dist/storage.mjs +2 -2
- package/dist/testing.d.mts +1 -26
- package/dist/testing.mjs +10503 -9614
- package/drizzle/postgres/20260918120000_v3_event_usage/migration.sql +14 -0
- package/drizzle/postgres/20260918120000_v3_event_usage/snapshot.json +1309 -0
- package/drizzle/sqlite/20260918120000_v3_event_usage/migration.sql +14 -0
- package/drizzle/sqlite/20260918120000_v3_event_usage/snapshot.json +1031 -0
- package/package.json +28 -29
- package/src/adapters/to-postgres.ts +1 -3
- package/src/adapters/to-sqlite.ts +0 -2
- package/src/adapters/types.ts +0 -3
- package/src/artifact.ts +0 -2
- package/src/backend.ts +31 -2
- package/src/branch.ts +223 -135
- package/src/deletion.ts +87 -0
- package/src/drizzle/postgres-v2.ts +0 -1
- package/src/drizzle/postgres-v3.ts +0 -1
- package/src/drizzle/sqlite-v2.ts +0 -1
- package/src/drizzle/sqlite-v3.ts +0 -1
- package/src/entities/v2/session-events.ts +0 -1
- package/src/entities/v3/event-usage.ts +25 -0
- package/src/entities/v3/events.ts +1 -3
- package/src/entities/v3/index.ts +4 -0
- package/src/entities/v3/session-events.ts +1 -2
- package/src/entities/v3/session-projcache-rows.ts +0 -8
- package/src/entities/v3/sessions.ts +3 -3
- package/src/entities/v3/storage-units.ts +0 -1
- package/src/entities/v3/workspace-sessions.ts +0 -5
- package/src/entities/v3/workspace-state.ts +0 -6
- package/src/entities/v3/workspaces.ts +0 -5
- package/src/export.ts +103 -0
- package/src/gc.ts +76 -0
- package/src/import-storages.ts +4 -37
- package/src/import.ts +54 -31
- package/src/index.ts +150 -114
- package/src/legacy.ts +4 -42
- package/src/log.ts +63 -106
- package/src/postgres.ts +249 -22
- package/src/schema.ts +6 -6
- package/src/session-query.ts +0 -4
- package/src/sqlite.ts +231 -55
- package/src/storage-takeover/index.ts +2 -28
- package/src/storage-takeover/projection-cache.ts +30 -134
- package/src/storage-takeover/repository.ts +20 -59
- package/src/storage-takeover/storage-backend.ts +3 -33
- package/src/storage-takeover/types.ts +14 -56
- package/src/storage.ts +0 -2
- package/src/testing/contract.ts +19 -50
- package/src/testing/coordinator-contract.ts +10 -25
- package/src/testing.ts +1 -4
- package/src/usage.ts +191 -0
- package/dist/index-D55G9n4k.d.mts +0 -271
- package/dist/magic-string.es-BgJoa-3K.mjs +0 -1017
package/src/branch.ts
CHANGED
|
@@ -12,15 +12,45 @@ import {
|
|
|
12
12
|
SessionBranchError,
|
|
13
13
|
balanceRewindPrefix,
|
|
14
14
|
buildTimeline,
|
|
15
|
+
rewindKeepLength,
|
|
15
16
|
type BranchAnchorMode,
|
|
16
17
|
type BranchBoundary,
|
|
17
18
|
type ForkFromOptions,
|
|
18
19
|
type SessionBranchProvider,
|
|
19
20
|
} from "@morlay/session-branch";
|
|
20
21
|
import { randomUUID } from "node:crypto";
|
|
22
|
+
import type { Backend } from "./backend.ts";
|
|
21
23
|
import type { SessionPersistenceRdb } from "./index.ts";
|
|
24
|
+
import { isLegacyVersion } from "./legacy.ts";
|
|
22
25
|
import { rowToMeta } from "./log.ts";
|
|
23
26
|
|
|
27
|
+
function assertRewindBoundary(
|
|
28
|
+
id: SessionId,
|
|
29
|
+
toBoundary: number,
|
|
30
|
+
boundaryType: string | undefined,
|
|
31
|
+
head: number,
|
|
32
|
+
): void {
|
|
33
|
+
if (toBoundary === -1) return;
|
|
34
|
+
if (toBoundary > head) {
|
|
35
|
+
throw new SessionBranchError(
|
|
36
|
+
`rewind boundary ${toBoundary} is beyond the stored head ${head}`,
|
|
37
|
+
"INVALID_BOUNDARY",
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
if (boundaryType === undefined) {
|
|
41
|
+
throw new SessionBranchError(
|
|
42
|
+
`rewind boundary ${toBoundary} does not exist in session "${id}"`,
|
|
43
|
+
"INVALID_BOUNDARY",
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
if (boundaryType !== "turn/end" && boundaryType !== "user/message") {
|
|
47
|
+
throw new SessionBranchError(
|
|
48
|
+
`rewind boundary ${toBoundary} is not a turn/end or user/message (${boundaryType})`,
|
|
49
|
+
"INVALID_BOUNDARY",
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
24
54
|
export function locateTurnEnd(
|
|
25
55
|
events: readonly SessionEvent[],
|
|
26
56
|
atSeq?: number,
|
|
@@ -42,7 +72,7 @@ export function locateTurnEnd(
|
|
|
42
72
|
}
|
|
43
73
|
const firstAfter = ends.find((seq) => seq >= atSeq);
|
|
44
74
|
if (firstAfter !== undefined) return firstAfter;
|
|
45
|
-
|
|
75
|
+
|
|
46
76
|
const lastStart = [...events].reverse().find((event) => event.type === "turn/start");
|
|
47
77
|
if (lastStart !== undefined && lastStart.seq <= atSeq) {
|
|
48
78
|
throw new SessionBranchError(`anchor ${atSeq} lies inside an open turn`, "OPEN_TURN");
|
|
@@ -67,46 +97,63 @@ export interface LiveSessionHooks {
|
|
|
67
97
|
|
|
68
98
|
flush(session: Session): Promise<boolean>;
|
|
69
99
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
// 跳过,重放输入永远进不了 inbox 投影。可选:纯持久化环境没有投影服务。
|
|
100
|
+
warn?(message: string): void;
|
|
101
|
+
|
|
73
102
|
resetProjections?(session: Session): void;
|
|
74
103
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
// store 的 higher-seq-wins 规则覆盖(rewind 后的正确值 seq 更小),表现为
|
|
78
|
-
// 轮次导航残留被删除的旧轮次。可选:纯持久化环境没有投影缓存服务。
|
|
104
|
+
resetTokenMeter?(session: Session): void;
|
|
105
|
+
|
|
79
106
|
refreshProjectionCache?(session: ProjectionCacheSession): Promise<void>;
|
|
80
107
|
}
|
|
81
108
|
|
|
82
|
-
/**
|
|
83
|
-
* 投影检查点刷新所需的最小会话面:cold rewind 没有 live Session 可复用,
|
|
84
|
-
* 而截断后的前缀未必是合法的独立会话(surface 引用可能悬空),不能走
|
|
85
|
-
* `Session.create` 的校验;缓存服务只读 id / header / inheritedEventCount
|
|
86
|
-
* 与事件前缀,用普通对象承载即可。
|
|
87
|
-
*/
|
|
88
109
|
export interface ProjectionCacheSession {
|
|
89
110
|
readonly id: SessionId;
|
|
90
111
|
readonly header: SessionHeader;
|
|
91
112
|
readonly inheritedEventCount: SessionLogOffset;
|
|
113
|
+
|
|
114
|
+
// 没有 live 会话时(cold rewind)缓存按这个新水位截断,不 fold 日志
|
|
115
|
+
readonly headSeq?: number;
|
|
92
116
|
snapshotEvents(): readonly SessionEvent[];
|
|
93
117
|
}
|
|
94
118
|
|
|
95
|
-
// 投影 registry 的失效面(上游私有结构,duck-type 读取)。
|
|
96
119
|
interface ProjectionRegistryLike {
|
|
97
120
|
registrations?: Map<string, { cells: WeakMap<object, unknown> }>;
|
|
98
121
|
}
|
|
99
122
|
|
|
123
|
+
interface TokenMeterLike {
|
|
124
|
+
states?: WeakMap<object, unknown>;
|
|
125
|
+
}
|
|
126
|
+
|
|
100
127
|
export interface LiveAgentLike {
|
|
101
128
|
session: Session;
|
|
102
129
|
|
|
103
130
|
requestHeaderLogged?: boolean;
|
|
104
131
|
|
|
105
|
-
// 上游 Agent 的 inbox 契约面(ReactLoopInbox.clear):rewind 后残留的排队
|
|
106
|
-
// 输入必须 durable 取消,否则 agent 会继续处理它们。可选:测试替身可能没有。
|
|
107
132
|
inbox?: { clear(): void };
|
|
108
133
|
}
|
|
109
134
|
|
|
135
|
+
interface SurfaceManagerLike {
|
|
136
|
+
_state: {
|
|
137
|
+
nodes: number[];
|
|
138
|
+
replaceGeneration: number;
|
|
139
|
+
contentGeneration: number;
|
|
140
|
+
projectedMessages: Map<unknown, unknown>;
|
|
141
|
+
};
|
|
142
|
+
_lastProcessedSeq: number;
|
|
143
|
+
_pendingPlan?: unknown;
|
|
144
|
+
baseSeq: number;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function resetSurfaceManager(surfaceManager: SurfaceManagerLike): void {
|
|
148
|
+
const state = surfaceManager._state;
|
|
149
|
+
state.nodes = [];
|
|
150
|
+
state.replaceGeneration = 0;
|
|
151
|
+
state.contentGeneration = 0;
|
|
152
|
+
state.projectedMessages = new Map();
|
|
153
|
+
surfaceManager._lastProcessedSeq = surfaceManager.baseSeq - 1;
|
|
154
|
+
surfaceManager._pendingPlan = undefined;
|
|
155
|
+
}
|
|
156
|
+
|
|
110
157
|
export function truncateLiveSession(session: Session, newLength: number): void {
|
|
111
158
|
const s = session as unknown as {
|
|
112
159
|
log: SessionEvent[];
|
|
@@ -118,12 +165,7 @@ export function truncateLiveSession(session: Session, newLength: number): void {
|
|
|
118
165
|
derived: unknown[];
|
|
119
166
|
derivedNodes: number;
|
|
120
167
|
derivedGeneration: number;
|
|
121
|
-
surfaceManager:
|
|
122
|
-
_state: { nodes: number[]; replaceGeneration: number };
|
|
123
|
-
_lastProcessedSeq: number;
|
|
124
|
-
_pendingPlan?: unknown;
|
|
125
|
-
baseSeq: number;
|
|
126
|
-
};
|
|
168
|
+
surfaceManager: SurfaceManagerLike;
|
|
127
169
|
};
|
|
128
170
|
s.log.length = newLength;
|
|
129
171
|
s.eventsSnapshot = undefined;
|
|
@@ -134,9 +176,7 @@ export function truncateLiveSession(session: Session, newLength: number): void {
|
|
|
134
176
|
s.derived = [];
|
|
135
177
|
s.derivedNodes = 0;
|
|
136
178
|
s.derivedGeneration = 0;
|
|
137
|
-
s.surfaceManager
|
|
138
|
-
s.surfaceManager._lastProcessedSeq = s.surfaceManager.baseSeq - 1;
|
|
139
|
-
s.surfaceManager._pendingPlan = undefined;
|
|
179
|
+
resetSurfaceManager(s.surfaceManager);
|
|
140
180
|
}
|
|
141
181
|
|
|
142
182
|
export function replaceLiveSessionLog(session: Session, events: readonly SessionEvent[]): void {
|
|
@@ -150,12 +190,7 @@ export function replaceLiveSessionLog(session: Session, events: readonly Session
|
|
|
150
190
|
derived: unknown[];
|
|
151
191
|
derivedNodes: number;
|
|
152
192
|
derivedGeneration: number;
|
|
153
|
-
surfaceManager:
|
|
154
|
-
_state: { nodes: number[]; replaceGeneration: number };
|
|
155
|
-
_lastProcessedSeq: number;
|
|
156
|
-
_pendingPlan?: unknown;
|
|
157
|
-
baseSeq: number;
|
|
158
|
-
};
|
|
193
|
+
surfaceManager: SurfaceManagerLike;
|
|
159
194
|
};
|
|
160
195
|
s.log.length = 0;
|
|
161
196
|
s.log.push(...events);
|
|
@@ -167,9 +202,7 @@ export function replaceLiveSessionLog(session: Session, events: readonly Session
|
|
|
167
202
|
s.derived = [];
|
|
168
203
|
s.derivedNodes = 0;
|
|
169
204
|
s.derivedGeneration = 0;
|
|
170
|
-
s.surfaceManager
|
|
171
|
-
s.surfaceManager._lastProcessedSeq = s.surfaceManager.baseSeq - 1;
|
|
172
|
-
s.surfaceManager._pendingPlan = undefined;
|
|
205
|
+
resetSurfaceManager(s.surfaceManager);
|
|
173
206
|
}
|
|
174
207
|
|
|
175
208
|
export class SessionBranchRdbProvider implements SessionBranchProvider {
|
|
@@ -217,7 +250,12 @@ export class SessionBranchRdbProvider implements SessionBranchProvider {
|
|
|
217
250
|
if (source === undefined)
|
|
218
251
|
throw new SessionBranchError(`session "${sourceId}" not found`, "SESSION_NOT_FOUND");
|
|
219
252
|
const boundary = locateTurnEnd(source.events, atSeq, anchorMode);
|
|
220
|
-
const prefix = source.events.slice(0, boundary + 1);
|
|
253
|
+
const prefix = balanceRewindPrefix(source.events.slice(0, boundary + 1));
|
|
254
|
+
if (prefix.length <= boundary) {
|
|
255
|
+
this.live.warn?.(
|
|
256
|
+
`session-rdb: fork "${sourceId}" dropped ${boundary + 1 - prefix.length} trailing event(s) from seq ${prefix.length} to keep the seed's step pairs balanced`,
|
|
257
|
+
);
|
|
258
|
+
}
|
|
221
259
|
const childId = childSessionId ?? mintSessionId();
|
|
222
260
|
const childMeta: SessionHeader = {
|
|
223
261
|
version: SESSION_FORMAT_VERSION,
|
|
@@ -239,9 +277,7 @@ export class SessionBranchRdbProvider implements SessionBranchProvider {
|
|
|
239
277
|
...(meta.delegationDepth !== undefined ? { delegationDepth: meta.delegationDepth } : {}),
|
|
240
278
|
};
|
|
241
279
|
const seed = [...renumber(prefix, 0), ...renumber(seedSuffix, prefix.length)];
|
|
242
|
-
|
|
243
|
-
// appendBatch 消费时复用事件行、不复制。seedSuffix 的 manualTurn 事件是
|
|
244
|
-
// 新事件(无源行),不注册。
|
|
280
|
+
|
|
245
281
|
const internals = this.persistence.internals();
|
|
246
282
|
const sourceRows = await internals.backend.getEventRows(sourceId);
|
|
247
283
|
const sourceEventIds = new Map(sourceRows.map((row) => [row.fSequence, row.fEventId]));
|
|
@@ -275,44 +311,49 @@ export class SessionBranchRdbProvider implements SessionBranchProvider {
|
|
|
275
311
|
);
|
|
276
312
|
}
|
|
277
313
|
const live = this.live.getSession(id);
|
|
278
|
-
|
|
314
|
+
|
|
279
315
|
if (live !== undefined) await this.live.flush(live);
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
//
|
|
284
|
-
|
|
285
|
-
|
|
316
|
+
|
|
317
|
+
const internals = this.persistence.internals();
|
|
318
|
+
|
|
319
|
+
// SessionBranch 是「先停止、再操作」的排他面(ADR-rewind绕过handle模型直接截断):rewind 直连 DB 做最小工作,
|
|
320
|
+
// 不走持久化抽象的全量读路径(readLog 会拉全部事件 + legacy 转换 + 读视图修复)。
|
|
321
|
+
const row = await internals.backend.getSession(id);
|
|
322
|
+
if (row === undefined) {
|
|
286
323
|
throw new SessionBranchError(`session "${id}" not found`, "SESSION_NOT_FOUND");
|
|
287
324
|
}
|
|
288
|
-
const
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
);
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
325
|
+
const meta = rowToMeta(row);
|
|
326
|
+
|
|
327
|
+
let rawKeepLength: number;
|
|
328
|
+
let kept: readonly SessionEvent[] | undefined;
|
|
329
|
+
if (isLegacyVersion(row.fVersion)) {
|
|
330
|
+
// 旧格式的持久化坐标与当前视图 seq 不一致:只有这条路径需要读日志(含格式转换与读视图修复)
|
|
331
|
+
const log = await this.persistence.readLog(id, {}, signal);
|
|
332
|
+
if (log === undefined) {
|
|
333
|
+
throw new SessionBranchError(`session "${id}" not found`, "SESSION_NOT_FOUND");
|
|
334
|
+
}
|
|
335
|
+
const boundaryType = log.events[toBoundary]?.type;
|
|
336
|
+
assertRewindBoundary(id, toBoundary, boundaryType, log.events.length - 1);
|
|
337
|
+
rawKeepLength =
|
|
338
|
+
toBoundary === -1 ? 0 : boundaryType === "turn/end" ? toBoundary + 1 : toBoundary;
|
|
339
|
+
kept = balanceRewindPrefix(log.events.slice(0, rawKeepLength));
|
|
340
|
+
} else {
|
|
341
|
+
const boundaryType =
|
|
342
|
+
toBoundary === -1 ? undefined : await internals.backend.getEventTypeAt(id, toBoundary);
|
|
343
|
+
assertRewindBoundary(id, toBoundary, boundaryType, row.fHeadSequence);
|
|
344
|
+
rawKeepLength =
|
|
345
|
+
toBoundary === -1 ? 0 : boundaryType === "turn/end" ? toBoundary + 1 : toBoundary;
|
|
346
|
+
}
|
|
347
|
+
const keepLength =
|
|
348
|
+
kept === undefined
|
|
349
|
+
? await this.rewindKeepLength(id, rawKeepLength, internals.backend)
|
|
350
|
+
: kept.length;
|
|
351
|
+
if (keepLength < rawKeepLength) {
|
|
352
|
+
this.live.warn?.(
|
|
353
|
+
`session-rdb: rewind "${id}" dropped ${rawKeepLength - keepLength} trailing event(s) from seq ${keepLength} to keep the retained prefix's step pairs balanced`,
|
|
304
354
|
);
|
|
305
355
|
}
|
|
306
|
-
// 保留前缀长度:turn/end inclusive;user/message exclusive(边界消息由
|
|
307
|
-
// 编辑版替换)。exclusive 截断可能残留孤儿 step/start,经平衡化剔除。
|
|
308
|
-
const rawKeepLength =
|
|
309
|
-
toBoundary === -1 ? 0 : boundaryEvent!.type === "turn/end" ? toBoundary + 1 : toBoundary;
|
|
310
|
-
const kept = balanceRewindPrefix(events.slice(0, rawKeepLength));
|
|
311
|
-
const keepLength = kept.length;
|
|
312
356
|
|
|
313
|
-
const internals = this.persistence.internals();
|
|
314
|
-
// 原样存储(无过滤):live 视图与 RDB head 同空间(上游 seq),
|
|
315
|
-
// 边界即保留前缀长度 - 1。
|
|
316
357
|
const denseBoundary = keepLength - 1;
|
|
317
358
|
const newSeedLength = await internals.backend.transaction(async (tx) => {
|
|
318
359
|
signal?.throwIfAborted();
|
|
@@ -332,140 +373,188 @@ export class SessionBranchRdbProvider implements SessionBranchProvider {
|
|
|
332
373
|
await tx.updateHead(id, prev.fEventId, prev.fSequence);
|
|
333
374
|
}
|
|
334
375
|
}
|
|
335
|
-
|
|
336
|
-
// 出现「继承前缀超过存储事件数」的矛盾(上游 load 判损坏)。
|
|
376
|
+
|
|
337
377
|
const storedSeedLength = await tx.getSeedLength(id);
|
|
338
378
|
let shrunk = storedSeedLength;
|
|
339
379
|
if (storedSeedLength !== null && storedSeedLength > denseBoundary + 1) {
|
|
340
380
|
await tx.updateSeedLength(id, denseBoundary + 1);
|
|
341
381
|
shrunk = denseBoundary + 1;
|
|
342
382
|
}
|
|
343
|
-
|
|
383
|
+
|
|
344
384
|
await tx.refreshTitle(id);
|
|
345
385
|
await tx.bumpRevision(id);
|
|
346
386
|
return shrunk;
|
|
347
387
|
});
|
|
348
388
|
|
|
349
|
-
// 更新确认 head(下一次 append 的并发校验基准),与 appendBatch 同语义。
|
|
350
389
|
internals.writeGuard.confirmHead(id, denseBoundary);
|
|
351
390
|
|
|
352
391
|
if (live !== undefined) {
|
|
353
|
-
// live 分支:截断内存 log 并重置派生缓存;同步 handle cursor 与
|
|
354
|
-
// agent 轮次游标。不调用 load——live 时 load 会先 flush 把旧内存写回,
|
|
355
|
-
// 撤销本次截断。
|
|
356
392
|
truncateLiveSession(live, keepLength);
|
|
357
|
-
|
|
358
|
-
// (seq 回退)不会进入投影。
|
|
393
|
+
|
|
359
394
|
this.live.resetProjections?.(live);
|
|
395
|
+
|
|
396
|
+
this.live.resetTokenMeter?.(live);
|
|
360
397
|
const agent = this.live.getAgent(id);
|
|
361
398
|
if (agent !== undefined) {
|
|
362
399
|
agent.requestHeaderLogged = false;
|
|
363
|
-
|
|
400
|
+
|
|
364
401
|
const lastTurn =
|
|
365
402
|
live.snapshotEvents().findLast((e) => e.type === "turn/start")?.data.turn ?? 0;
|
|
366
403
|
const phase = (agent as unknown as { phase?: { lastTurn?: number } }).phase;
|
|
367
404
|
if (phase !== undefined) phase.lastTurn = lastTurn;
|
|
368
405
|
}
|
|
369
|
-
|
|
370
|
-
// append 从截断后的位置续接。handle cursor 是**上游空间**(live 内存
|
|
371
|
-
// log 截断后的长度,与 drainBuffered 的过滤/contiguity 校验同空间)。
|
|
406
|
+
|
|
372
407
|
const handle = this.persistence.tracker.writerOf(id);
|
|
373
408
|
if (handle !== undefined) {
|
|
374
409
|
handle.resetAfterRewind(keepLength, newSeedLength === null ? undefined : newSeedLength);
|
|
375
410
|
}
|
|
376
|
-
|
|
377
|
-
// 仍会被 agent 处理,必须 durable 取消。cursor 已对齐,取消事件从截断点续接。
|
|
411
|
+
|
|
378
412
|
agent?.inbox?.clear();
|
|
379
|
-
|
|
380
|
-
// 可能在 rewind 返回后直接读后端)。
|
|
413
|
+
|
|
381
414
|
await this.live.flush(live);
|
|
382
|
-
|
|
383
|
-
// 会停在截断前;超前行锁死前端(higher-seq-wins),轮次导航残留旧轮次。
|
|
415
|
+
|
|
384
416
|
await this.refreshProjectionCache(live);
|
|
385
417
|
} else {
|
|
386
|
-
//
|
|
387
|
-
// 的 log 重写检查点(没有 live 会话可复用,也不为此 resume 一个 agent)。
|
|
418
|
+
// rewind 不读日志:缓存行按新水位截断(保留下来的投影单元仍在截断前缀内)
|
|
388
419
|
await this.refreshProjectionCache({
|
|
389
420
|
id,
|
|
390
421
|
header: meta,
|
|
391
|
-
inheritedEventCount: SessionLogOffset(
|
|
392
|
-
|
|
422
|
+
inheritedEventCount: SessionLogOffset(newSeedLength ?? row.fSeedLength ?? 0),
|
|
423
|
+
headSeq: keepLength - 1,
|
|
424
|
+
snapshotEvents: () => [],
|
|
393
425
|
});
|
|
394
426
|
}
|
|
395
427
|
|
|
396
|
-
const row = await internals.backend.getSession(id);
|
|
397
|
-
if (row === undefined) {
|
|
398
|
-
// 空会话(toBoundary = -1 且从未有行):返回「已确认缺席」快照。
|
|
399
|
-
return {
|
|
400
|
-
header: {
|
|
401
|
-
version: SESSION_FORMAT_VERSION,
|
|
402
|
-
id,
|
|
403
|
-
createdAt: meta.createdAt,
|
|
404
|
-
...(meta.cwd !== undefined ? { cwd: meta.cwd } : {}),
|
|
405
|
-
...(meta.parentSession !== undefined ? { parentSession: meta.parentSession } : {}),
|
|
406
|
-
isSeeded: meta.isSeeded,
|
|
407
|
-
...(meta.origin !== undefined ? { origin: meta.origin } : {}),
|
|
408
|
-
...(meta.delegationDepth !== undefined ? { delegationDepth: meta.delegationDepth } : {}),
|
|
409
|
-
...(meta.agentPreset !== undefined ? { agentPreset: meta.agentPreset } : {}),
|
|
410
|
-
},
|
|
411
|
-
revision:
|
|
412
|
-
(await internals.readStoredRevision(id)) ??
|
|
413
|
-
(await this.persistence.readStoredRevision(id))!,
|
|
414
|
-
};
|
|
415
|
-
}
|
|
416
428
|
return { header: rowToMeta(row), revision: (await internals.readStoredRevision(id))! };
|
|
417
429
|
}
|
|
418
430
|
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
431
|
+
// 保留长度只依赖尾部窗口:从 rawKeepLength 往回读类型(有界),窗口没覆盖到最近一个 turn/end
|
|
432
|
+
// 就翻倍重读,直到覆盖或到达前缀开头。
|
|
433
|
+
private async rewindKeepLength(
|
|
434
|
+
id: SessionId,
|
|
435
|
+
rawKeepLength: number,
|
|
436
|
+
backend: Backend,
|
|
437
|
+
): Promise<number> {
|
|
438
|
+
if (rawKeepLength === 0) return 0;
|
|
439
|
+
let limit = 64;
|
|
440
|
+
for (;;) {
|
|
441
|
+
const rows = await backend.getEventTypesBefore(id, rawKeepLength, limit);
|
|
442
|
+
const types = [...rows].reverse().map((row) => row.fType);
|
|
443
|
+
const windowStart = rawKeepLength - types.length;
|
|
444
|
+
if (types.includes("turn/end") || windowStart === 0 || types.length >= rawKeepLength) {
|
|
445
|
+
return rewindKeepLength(types, rawKeepLength);
|
|
446
|
+
}
|
|
447
|
+
limit *= 4;
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
|
|
423
451
|
private async refreshProjectionCache(session: ProjectionCacheSession): Promise<void> {
|
|
424
452
|
if (this.live.refreshProjectionCache === undefined) return;
|
|
425
453
|
try {
|
|
426
454
|
await this.live.refreshProjectionCache(session);
|
|
427
|
-
} catch {
|
|
428
|
-
// 实现方(SessionBranchRdb)已记日志;此处只保证 rewind 不因派生数据失败。
|
|
429
|
-
}
|
|
455
|
+
} catch {}
|
|
430
456
|
}
|
|
431
457
|
}
|
|
432
458
|
|
|
433
459
|
export class SessionBranchRdb extends SessionBranch {
|
|
434
460
|
static inject = ["sessionPersistence", "sessions"];
|
|
435
461
|
|
|
462
|
+
private readonly warned = new Set<string>();
|
|
463
|
+
|
|
436
464
|
constructor(ctx: import("@deepseek-ai/cordis").Context) {
|
|
437
465
|
super(ctx);
|
|
438
466
|
}
|
|
439
467
|
|
|
468
|
+
// 覆盖导入等整段替换内存 log 的路径复用 rewind 的失效钩子(token-meter 水位是位置不是事件身份)
|
|
469
|
+
resetLiveDerivedState(session: Session): void {
|
|
470
|
+
this.resetProjectionCells(session);
|
|
471
|
+
this.resetTokenMeterFold(session);
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
private warnOnce(key: string, message: string): void {
|
|
475
|
+
if (this.warned.has(key)) return;
|
|
476
|
+
this.warned.add(key);
|
|
477
|
+
this.ctx.logger.warn(message);
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
private resetProjectionCells(session: Session): void {
|
|
481
|
+
const registry = (this.ctx as unknown as { get(name: string): unknown }).get(
|
|
482
|
+
"sessionProjections",
|
|
483
|
+
) as ProjectionRegistryLike | undefined;
|
|
484
|
+
const registrations = registry?.registrations;
|
|
485
|
+
if (!(registrations instanceof Map)) {
|
|
486
|
+
this.warnOnce(
|
|
487
|
+
"sessionProjections.registrations",
|
|
488
|
+
`session-rdb: ctx.sessionProjections.registrations is not a Map (upstream field shape changed); rewind leaves the projection cell caches of "${session.id}" stale, so replayed events can be skipped`,
|
|
489
|
+
);
|
|
490
|
+
return;
|
|
491
|
+
}
|
|
492
|
+
for (const registration of registrations.values()) {
|
|
493
|
+
const cells = registration?.cells;
|
|
494
|
+
if (!(cells instanceof WeakMap)) {
|
|
495
|
+
this.warnOnce(
|
|
496
|
+
"sessionProjections.cells",
|
|
497
|
+
`session-rdb: ctx.sessionProjections registration cells are not a WeakMap (upstream field shape changed); rewind leaves the projection cell caches of "${session.id}" stale, so replayed events can be skipped`,
|
|
498
|
+
);
|
|
499
|
+
continue;
|
|
500
|
+
}
|
|
501
|
+
cells.delete(session);
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
private resetTokenMeterFold(session: Session): void {
|
|
506
|
+
const meter = this.ctx.get("tokenMeter") as TokenMeterLike | undefined;
|
|
507
|
+
if (meter === undefined) {
|
|
508
|
+
this.warnOnce(
|
|
509
|
+
"tokenMeter",
|
|
510
|
+
`session-rdb: ctx.tokenMeter is not mounted; after rewind the token-meter fold watermark of "${session.id}" may stay stale, so compaction can report "step/end ... has no matching step/start event"`,
|
|
511
|
+
);
|
|
512
|
+
return;
|
|
513
|
+
}
|
|
514
|
+
const states = meter.states;
|
|
515
|
+
if (!(states instanceof WeakMap)) {
|
|
516
|
+
this.warnOnce(
|
|
517
|
+
"tokenMeter.states",
|
|
518
|
+
`session-rdb: ctx.tokenMeter.states is not a WeakMap (upstream field shape changed); after rewind the token-meter fold watermark of "${session.id}" may stay stale, so compaction can report "step/end ... has no matching step/start event"`,
|
|
519
|
+
);
|
|
520
|
+
return;
|
|
521
|
+
}
|
|
522
|
+
states.delete(session);
|
|
523
|
+
}
|
|
524
|
+
|
|
440
525
|
private readonly provider = new SessionBranchRdbProvider(
|
|
441
526
|
this.ctx.sessionPersistence as SessionPersistenceRdb,
|
|
442
527
|
{
|
|
443
528
|
getSession: (id) => this.ctx.sessions.get(id),
|
|
444
529
|
getAgent: (id) => {
|
|
445
|
-
// agents 服务可选(纯持久化环境无 agent-loop):经 ctx.get 动态访问。
|
|
446
530
|
const agents = this.ctx.get("agents") as
|
|
447
531
|
| { get(id: SessionId): LiveAgentLike | undefined }
|
|
448
532
|
| undefined;
|
|
449
533
|
return agents?.get(id);
|
|
450
534
|
},
|
|
451
535
|
flush: (session) => this.ctx.sessions.flush(session),
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
const registry = (this.ctx as unknown as { get(name: string): unknown }).get(
|
|
455
|
-
"sessionProjections",
|
|
456
|
-
) as ProjectionRegistryLike | undefined;
|
|
457
|
-
if (registry?.registrations === undefined) return;
|
|
458
|
-
for (const registration of registry.registrations.values()) {
|
|
459
|
-
registration.cells.delete(session);
|
|
460
|
-
}
|
|
536
|
+
warn: (message) => {
|
|
537
|
+
this.ctx.logger.warn(message);
|
|
461
538
|
},
|
|
539
|
+
resetProjections: (session) => this.resetProjectionCells(session),
|
|
540
|
+
resetTokenMeter: (session) => this.resetTokenMeterFold(session),
|
|
462
541
|
refreshProjectionCache: async (session) => {
|
|
463
|
-
// sessionProjectionCache 是可选服务(纯持久化环境无投影缓存)。
|
|
464
542
|
const cache = this.ctx.get("sessionProjectionCache") as
|
|
465
|
-
| {
|
|
543
|
+
| {
|
|
544
|
+
write(session: ProjectionCacheSession): Promise<void>;
|
|
545
|
+
truncateTo?(
|
|
546
|
+
header: SessionHeader,
|
|
547
|
+
inheritedEventCount: SessionLogOffset,
|
|
548
|
+
headSeq: number,
|
|
549
|
+
): Promise<void>;
|
|
550
|
+
}
|
|
466
551
|
| undefined;
|
|
467
552
|
if (cache === undefined) return;
|
|
468
553
|
try {
|
|
554
|
+
if (session.headSeq !== undefined && cache.truncateTo !== undefined) {
|
|
555
|
+
await cache.truncateTo(session.header, session.inheritedEventCount, session.headSeq);
|
|
556
|
+
return;
|
|
557
|
+
}
|
|
469
558
|
await cache.write(session);
|
|
470
559
|
} catch (error: unknown) {
|
|
471
560
|
this.ctx.logger.warn(
|
|
@@ -511,8 +600,7 @@ export class SessionBranchRdb extends SessionBranch {
|
|
|
511
600
|
async timeline(sessionId: SessionId, signal?: AbortSignal) {
|
|
512
601
|
const persistence = this.ctx.sessionPersistence as SessionPersistenceRdb;
|
|
513
602
|
const snapshots = await persistence.listSnapshots(signal);
|
|
514
|
-
|
|
515
|
-
// 走持久化 readFrom(版本效果不落 canonical log,timeline 为 lineage 骨架)。
|
|
603
|
+
|
|
516
604
|
const readOwnEvents = async (id: SessionId, fromSeq: number, s?: AbortSignal) => {
|
|
517
605
|
const live = this.ctx.sessions.get(id);
|
|
518
606
|
if (live !== undefined) return live.snapshotEvents().slice(fromSeq);
|
package/src/deletion.ts
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import type { Context } from "@deepseek-ai/cordis";
|
|
2
|
+
import type { SessionId } from "@deepseek-ai/dsh-session";
|
|
3
|
+
import { SessionDeletionError, type SessionPersistenceRdb } from "./index.ts";
|
|
4
|
+
|
|
5
|
+
export const SESSION_DELETE_PATH = "/api/session.delete";
|
|
6
|
+
|
|
7
|
+
const STATUS_OF_DELETION_ERROR: Record<SessionDeletionError["code"], number> = {
|
|
8
|
+
SESSION_NOT_FOUND: 404,
|
|
9
|
+
SESSION_NOT_ARCHIVED: 409,
|
|
10
|
+
SESSION_LIVE: 409,
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export function registerSessionDeletion(ctx: Context, persistence: SessionPersistenceRdb): void {
|
|
14
|
+
ctx.inject(["webServer", "connection"] as const, (webCtx) => {
|
|
15
|
+
const webServer = webCtx.webServer as unknown as {
|
|
16
|
+
register(route: {
|
|
17
|
+
kind: "exact";
|
|
18
|
+
path: string;
|
|
19
|
+
handler: (
|
|
20
|
+
req: import("node:http").IncomingMessage,
|
|
21
|
+
res: import("node:http").ServerResponse,
|
|
22
|
+
) => void | Promise<void>;
|
|
23
|
+
}): () => void;
|
|
24
|
+
};
|
|
25
|
+
const connection = webCtx.get("connection") as unknown as {
|
|
26
|
+
requestRejection(request: {
|
|
27
|
+
headers: import("node:http").IncomingHttpHeaders;
|
|
28
|
+
}): number | undefined;
|
|
29
|
+
};
|
|
30
|
+
return webCtx.effect(() =>
|
|
31
|
+
webServer.register({
|
|
32
|
+
kind: "exact",
|
|
33
|
+
path: SESSION_DELETE_PATH,
|
|
34
|
+
handler: async (req, res) => {
|
|
35
|
+
const rejection = connection.requestRejection(req);
|
|
36
|
+
if (rejection !== undefined) {
|
|
37
|
+
res.writeHead(rejection);
|
|
38
|
+
res.end(rejection === 401 ? "unauthorized" : "forbidden");
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
if (req.method !== "POST") {
|
|
42
|
+
res.writeHead(405, { "content-type": "application/json" });
|
|
43
|
+
res.end(JSON.stringify({ error: "method not allowed" }));
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
const chunks: Buffer[] = [];
|
|
47
|
+
for await (const chunk of req) chunks.push(chunk as Buffer);
|
|
48
|
+
let envelope: { sessionId?: unknown };
|
|
49
|
+
try {
|
|
50
|
+
envelope = JSON.parse(Buffer.concat(chunks).toString("utf8")) as {
|
|
51
|
+
sessionId?: unknown;
|
|
52
|
+
};
|
|
53
|
+
} catch {
|
|
54
|
+
res.writeHead(400, { "content-type": "application/json" });
|
|
55
|
+
res.end(JSON.stringify({ error: "request body is not JSON" }));
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
if (typeof envelope.sessionId !== "string" || envelope.sessionId === "") {
|
|
59
|
+
res.writeHead(400, { "content-type": "application/json" });
|
|
60
|
+
res.end(JSON.stringify({ error: "missing sessionId field" }));
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
try {
|
|
64
|
+
await persistence.deleteSession(envelope.sessionId as SessionId);
|
|
65
|
+
} catch (error: unknown) {
|
|
66
|
+
if (error instanceof SessionDeletionError) {
|
|
67
|
+
res.writeHead(STATUS_OF_DELETION_ERROR[error.code], {
|
|
68
|
+
"content-type": "application/json",
|
|
69
|
+
});
|
|
70
|
+
res.end(JSON.stringify({ error: error.message, code: error.code }));
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
res.writeHead(500, { "content-type": "application/json" });
|
|
74
|
+
res.end(
|
|
75
|
+
JSON.stringify({
|
|
76
|
+
error: error instanceof Error ? error.message : "session deletion failed",
|
|
77
|
+
}),
|
|
78
|
+
);
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
res.writeHead(200, { "content-type": "application/json" });
|
|
82
|
+
res.end(JSON.stringify({ deleted: envelope.sessionId }));
|
|
83
|
+
},
|
|
84
|
+
}),
|
|
85
|
+
);
|
|
86
|
+
});
|
|
87
|
+
}
|
package/src/drizzle/sqlite-v2.ts
CHANGED
package/src/drizzle/sqlite-v3.ts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import type { TableDef } from "../../adapters/types.ts";
|
|
2
2
|
import { sessionEvents as v3SessionEvents } from "../v3/session-events.ts";
|
|
3
3
|
|
|
4
|
-
/** v2 派生:v3 规格 + f_original_seq 列(原样存储前记录上游 seq 的遗留列)。 */
|
|
5
4
|
export const sessionEvents: TableDef = {
|
|
6
5
|
...v3SessionEvents,
|
|
7
6
|
columns: {
|