@morlay/session-rdb 0.0.11 → 0.0.12
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 +0 -16
- package/dist/index.d.mts +203 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +2030 -0
- package/dist/index.mjs.map +1 -0
- package/dist/invariant.d.mts +8 -0
- package/dist/invariant.d.mts.map +1 -0
- package/dist/invariant.mjs +10 -0
- package/dist/invariant.mjs.map +1 -0
- package/package.json +22 -22
- package/src/adapters/ddl.ts +77 -0
- package/src/adapters/index.ts +4 -0
- package/src/adapters/to-postgres.ts +91 -0
- package/src/adapters/to-sqlite.ts +79 -0
- package/src/backend.ts +112 -0
- package/src/branch.ts +508 -0
- package/src/entities/events.ts +27 -0
- package/src/entities/index.ts +30 -0
- package/src/entities/persistence-state.ts +10 -0
- package/src/entities/schema-meta.ts +9 -0
- package/src/entities/session-events.ts +29 -0
- package/src/entities/sessions.ts +20 -0
- package/src/entities/types.ts +48 -0
- package/src/import.ts +270 -0
- package/src/index.ts +543 -0
- package/src/invariant.ts +15 -0
- package/src/log.ts +370 -0
- package/src/migrate.ts +137 -0
- package/src/postgres.ts +369 -0
- package/src/schema.ts +234 -0
- package/src/sqlite.ts +412 -0
- package/src/write-guard.ts +27 -0
- package/lib/index.d.mts +0 -558
- package/lib/index.d.mts.map +0 -1
- package/lib/index.mjs +0 -2176
- package/lib/index.mjs.map +0 -1
- package/lib/invariant.d.mts +0 -15
- package/lib/invariant.d.mts.map +0 -1
- package/lib/invariant.mjs +0 -21
- package/lib/invariant.mjs.map +0 -1
package/src/branch.ts
ADDED
|
@@ -0,0 +1,508 @@
|
|
|
1
|
+
import {
|
|
2
|
+
SESSION_FORMAT_VERSION,
|
|
3
|
+
type Session,
|
|
4
|
+
type SessionEvent,
|
|
5
|
+
type SessionHeader,
|
|
6
|
+
type SessionId,
|
|
7
|
+
} from "@deepseek-ai/dsh-session";
|
|
8
|
+
import type { SessionPersistenceSnapshot } from "@deepseek-ai/dsh-session-persistence";
|
|
9
|
+
import {
|
|
10
|
+
SessionBranch,
|
|
11
|
+
SessionBranchError,
|
|
12
|
+
balanceRewindPrefix,
|
|
13
|
+
buildTimeline,
|
|
14
|
+
type BranchAnchorMode,
|
|
15
|
+
type BranchBoundary,
|
|
16
|
+
type ForkFromOptions,
|
|
17
|
+
type SessionBranchProvider,
|
|
18
|
+
} from "@morlay/session-branch";
|
|
19
|
+
import { randomUUID } from "node:crypto";
|
|
20
|
+
import type { SessionPersistenceRdb } from "./index.ts";
|
|
21
|
+
import { rowToMeta } from "./log.ts";
|
|
22
|
+
import { isPersistedEvent } from "./schema.ts";
|
|
23
|
+
|
|
24
|
+
export function locateTurnEnd(
|
|
25
|
+
events: readonly SessionEvent[],
|
|
26
|
+
atSeq?: number,
|
|
27
|
+
mode: BranchAnchorMode = "after",
|
|
28
|
+
): number {
|
|
29
|
+
const ends = events.filter((event) => event.type === "turn/end").map((event) => event.seq);
|
|
30
|
+
if (atSeq === undefined) {
|
|
31
|
+
const last = ends.at(-1);
|
|
32
|
+
if (last === undefined) throw new SessionBranchError("session has no closed turn", "OPEN_TURN");
|
|
33
|
+
return last;
|
|
34
|
+
}
|
|
35
|
+
if (mode === "before") {
|
|
36
|
+
let boundary = -1;
|
|
37
|
+
for (const seq of ends) {
|
|
38
|
+
if (seq < atSeq) boundary = seq;
|
|
39
|
+
else break;
|
|
40
|
+
}
|
|
41
|
+
return boundary;
|
|
42
|
+
}
|
|
43
|
+
const firstAfter = ends.find((seq) => seq >= atSeq);
|
|
44
|
+
if (firstAfter !== undefined) return firstAfter;
|
|
45
|
+
// atSeq 越过末尾:若其落在未闭合轮内则拒绝,否则回退最后一个闭合轮。
|
|
46
|
+
const lastStart = [...events].reverse().find((event) => event.type === "turn/start");
|
|
47
|
+
if (lastStart !== undefined && lastStart.seq <= atSeq) {
|
|
48
|
+
throw new SessionBranchError(`anchor ${atSeq} lies inside an open turn`, "OPEN_TURN");
|
|
49
|
+
}
|
|
50
|
+
const last = ends.at(-1);
|
|
51
|
+
if (last === undefined) throw new SessionBranchError("session has no closed turn", "OPEN_TURN");
|
|
52
|
+
return last;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function renumber(events: readonly SessionEvent[], offset: number): SessionEvent[] {
|
|
56
|
+
return events.map((event, index) => ({ ...event, seq: offset + index }) as SessionEvent);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function mintSessionId(): SessionId {
|
|
60
|
+
return `session-${randomUUID()}` as SessionId;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export interface LiveSessionHooks {
|
|
64
|
+
getSession(id: SessionId): Session | undefined;
|
|
65
|
+
|
|
66
|
+
getAgent(id: SessionId): LiveAgentLike | undefined;
|
|
67
|
+
|
|
68
|
+
flush(session: Session): Promise<boolean>;
|
|
69
|
+
|
|
70
|
+
setCoordinatorCursor(id: SessionId, cursor: number): void;
|
|
71
|
+
|
|
72
|
+
setCoordinatorState(id: SessionId, cursor: number, meta: SessionHeader): void;
|
|
73
|
+
|
|
74
|
+
setCoordinatorSeedLength(id: SessionId, seedLength: number): void;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export interface LiveAgentLike {
|
|
78
|
+
session: Session;
|
|
79
|
+
|
|
80
|
+
requestHeaderLogged?: boolean;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function truncateLiveSession(session: Session, newLength: number): void {
|
|
84
|
+
const s = session as unknown as {
|
|
85
|
+
log: SessionEvent[];
|
|
86
|
+
eventsSnapshot?: unknown;
|
|
87
|
+
headerFold?: unknown;
|
|
88
|
+
headerFoldSeq: number;
|
|
89
|
+
contextFold?: unknown;
|
|
90
|
+
contextFoldSeq: number;
|
|
91
|
+
derived: unknown[];
|
|
92
|
+
derivedNodes: number;
|
|
93
|
+
derivedGeneration: number;
|
|
94
|
+
surfaceManager: {
|
|
95
|
+
_state: { nodes: number[]; replaceGeneration: number };
|
|
96
|
+
_lastProcessedSeq: number;
|
|
97
|
+
_pendingPlan?: unknown;
|
|
98
|
+
baseSeq: number;
|
|
99
|
+
};
|
|
100
|
+
};
|
|
101
|
+
s.log.length = newLength;
|
|
102
|
+
s.eventsSnapshot = undefined;
|
|
103
|
+
s.headerFold = undefined;
|
|
104
|
+
s.headerFoldSeq = 0;
|
|
105
|
+
s.contextFold = undefined;
|
|
106
|
+
s.contextFoldSeq = 0;
|
|
107
|
+
s.derived = [];
|
|
108
|
+
s.derivedNodes = 0;
|
|
109
|
+
s.derivedGeneration = 0;
|
|
110
|
+
s.surfaceManager._state = { nodes: [], replaceGeneration: 0 };
|
|
111
|
+
s.surfaceManager._lastProcessedSeq = s.surfaceManager.baseSeq - 1;
|
|
112
|
+
s.surfaceManager._pendingPlan = undefined;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function replaceLiveSessionLog(session: Session, events: readonly SessionEvent[]): void {
|
|
116
|
+
const s = session as unknown as {
|
|
117
|
+
log: SessionEvent[];
|
|
118
|
+
eventsSnapshot?: unknown;
|
|
119
|
+
headerFold?: unknown;
|
|
120
|
+
headerFoldSeq: number;
|
|
121
|
+
contextFold?: unknown;
|
|
122
|
+
contextFoldSeq: number;
|
|
123
|
+
derived: unknown[];
|
|
124
|
+
derivedNodes: number;
|
|
125
|
+
derivedGeneration: number;
|
|
126
|
+
surfaceManager: {
|
|
127
|
+
_state: { nodes: number[]; replaceGeneration: number };
|
|
128
|
+
_lastProcessedSeq: number;
|
|
129
|
+
_pendingPlan?: unknown;
|
|
130
|
+
baseSeq: number;
|
|
131
|
+
};
|
|
132
|
+
};
|
|
133
|
+
s.log.length = 0;
|
|
134
|
+
s.log.push(...events);
|
|
135
|
+
s.eventsSnapshot = undefined;
|
|
136
|
+
s.headerFold = undefined;
|
|
137
|
+
s.headerFoldSeq = 0;
|
|
138
|
+
s.contextFold = undefined;
|
|
139
|
+
s.contextFoldSeq = 0;
|
|
140
|
+
s.derived = [];
|
|
141
|
+
s.derivedNodes = 0;
|
|
142
|
+
s.derivedGeneration = 0;
|
|
143
|
+
s.surfaceManager._state = { nodes: [], replaceGeneration: 0 };
|
|
144
|
+
s.surfaceManager._lastProcessedSeq = s.surfaceManager.baseSeq - 1;
|
|
145
|
+
s.surfaceManager._pendingPlan = undefined;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export class SessionBranchRdbProvider implements SessionBranchProvider {
|
|
149
|
+
readonly name = "session-rdb";
|
|
150
|
+
|
|
151
|
+
constructor(
|
|
152
|
+
private readonly persistence: SessionPersistenceRdb,
|
|
153
|
+
|
|
154
|
+
private readonly live: LiveSessionHooks = {
|
|
155
|
+
getSession: () => undefined,
|
|
156
|
+
getAgent: () => undefined,
|
|
157
|
+
flush: async () => true,
|
|
158
|
+
setCoordinatorCursor: () => {},
|
|
159
|
+
setCoordinatorState: () => {},
|
|
160
|
+
setCoordinatorSeedLength: () => {},
|
|
161
|
+
},
|
|
162
|
+
) {}
|
|
163
|
+
|
|
164
|
+
async readBranchPrefix(
|
|
165
|
+
id: SessionId,
|
|
166
|
+
atSeq?: number,
|
|
167
|
+
mode: BranchAnchorMode = "after",
|
|
168
|
+
signal?: AbortSignal,
|
|
169
|
+
): Promise<BranchBoundary> {
|
|
170
|
+
const { events } = await this.readRawEvents(id, signal);
|
|
171
|
+
const boundary = locateTurnEnd(events, atSeq, mode);
|
|
172
|
+
return { seq: boundary, events: events.slice(0, boundary + 1) };
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
async readRawEvents(
|
|
176
|
+
id: SessionId,
|
|
177
|
+
signal?: AbortSignal,
|
|
178
|
+
): Promise<{ meta: SessionHeader; events: readonly SessionEvent[] }> {
|
|
179
|
+
const stored = await this.persistence.loadStored(id, signal);
|
|
180
|
+
if (stored === undefined)
|
|
181
|
+
throw new SessionBranchError(`session "${id}" not found`, "SESSION_NOT_FOUND");
|
|
182
|
+
return { meta: stored.meta, events: stored.events };
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
async forkFrom(
|
|
186
|
+
sourceId: SessionId,
|
|
187
|
+
options: ForkFromOptions = {},
|
|
188
|
+
signal?: AbortSignal,
|
|
189
|
+
): Promise<SessionId> {
|
|
190
|
+
signal?.throwIfAborted();
|
|
191
|
+
const { atSeq, anchorMode = "after", seedSuffix = [], childSessionId, meta = {} } = options;
|
|
192
|
+
const source = await this.persistence.inspect(sourceId, signal);
|
|
193
|
+
const boundary = locateTurnEnd(source.events, atSeq, anchorMode);
|
|
194
|
+
const prefix = source.events.slice(0, boundary + 1);
|
|
195
|
+
const childId = childSessionId ?? mintSessionId();
|
|
196
|
+
const childMeta: SessionHeader = {
|
|
197
|
+
version: SESSION_FORMAT_VERSION,
|
|
198
|
+
id: childId,
|
|
199
|
+
createdAt: meta.createdAt ?? Date.now(),
|
|
200
|
+
...(meta.cwd !== undefined
|
|
201
|
+
? { cwd: meta.cwd }
|
|
202
|
+
: source.meta.cwd !== undefined
|
|
203
|
+
? { cwd: source.meta.cwd }
|
|
204
|
+
: {}),
|
|
205
|
+
parentSession: sourceId,
|
|
206
|
+
isSeeded: true,
|
|
207
|
+
...(meta.agentPreset !== undefined
|
|
208
|
+
? { agentPreset: meta.agentPreset }
|
|
209
|
+
: source.meta.agentPreset !== undefined
|
|
210
|
+
? { agentPreset: source.meta.agentPreset }
|
|
211
|
+
: {}),
|
|
212
|
+
...(meta.origin !== undefined ? { origin: meta.origin } : {}),
|
|
213
|
+
...(meta.delegationDepth !== undefined ? { delegationDepth: meta.delegationDepth } : {}),
|
|
214
|
+
};
|
|
215
|
+
const seed = [...renumber(prefix, 0), ...renumber(seedSuffix, prefix.length)];
|
|
216
|
+
// 事件行复用:前缀事件(上游 seq → 源会话已存在事件行 id)注册到写路径,
|
|
217
|
+
// appendBatch 消费时复用事件行、不复制。seedSuffix 的 manualTurn 事件是
|
|
218
|
+
// 新事件(无源行),不注册。
|
|
219
|
+
const internals = this.persistence.internals();
|
|
220
|
+
const sourceRows = await internals.backend.getEventRows(sourceId);
|
|
221
|
+
const sourceEventIds = new Map(sourceRows.map((row) => [row.fOriginalSeq, row.fEventId]));
|
|
222
|
+
const reuse = new Map<number, string>();
|
|
223
|
+
for (const event of prefix) {
|
|
224
|
+
const eventId = sourceEventIds.get(event.seq);
|
|
225
|
+
if (eventId !== undefined) reuse.set(event.seq, eventId);
|
|
226
|
+
}
|
|
227
|
+
internals.registerReuseEventIds(childId, reuse);
|
|
228
|
+
await this.persistence.create(childMeta, prefix.length);
|
|
229
|
+
if (seed.length > 0) await this.persistence.append(childId, seed);
|
|
230
|
+
return childId;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
async rewind(
|
|
234
|
+
id: SessionId,
|
|
235
|
+
toBoundary: number,
|
|
236
|
+
signal?: AbortSignal,
|
|
237
|
+
): Promise<SessionPersistenceSnapshot> {
|
|
238
|
+
signal?.throwIfAborted();
|
|
239
|
+
if (!Number.isSafeInteger(toBoundary) || toBoundary < -1) {
|
|
240
|
+
throw new SessionBranchError(
|
|
241
|
+
`rewind boundary must be a non-negative safe integer, got ${toBoundary}`,
|
|
242
|
+
"INVALID_BOUNDARY",
|
|
243
|
+
);
|
|
244
|
+
}
|
|
245
|
+
const live = this.live.getSession(id);
|
|
246
|
+
// live 会话先落盘 write-behind 缓冲,保证后续读取与后端事务同视图。
|
|
247
|
+
if (live !== undefined) await this.live.flush(live);
|
|
248
|
+
// 边界校验用原始事件(loadStored,不补 closers)——inspect 会给未闭合
|
|
249
|
+
// log 补合成 closers,把 user/message 边界掩盖成 turn/end,丢失 exclusive
|
|
250
|
+
// 语义。
|
|
251
|
+
const raw = live === undefined ? await this.readRawEvents(id, signal) : undefined;
|
|
252
|
+
const inspection = live === undefined ? undefined : await this.persistence.inspect(id, signal);
|
|
253
|
+
const events = live === undefined ? raw!.events : inspection!.events;
|
|
254
|
+
const meta = live === undefined ? raw!.meta : inspection!.meta;
|
|
255
|
+
const boundaryEvent = events[toBoundary];
|
|
256
|
+
if (toBoundary === -1) {
|
|
257
|
+
// 空前缀:清空整个 log(head 归 -1),与 commitRepair 的初始状态一致。
|
|
258
|
+
} else if (boundaryEvent === undefined) {
|
|
259
|
+
throw new SessionBranchError(
|
|
260
|
+
`rewind boundary ${toBoundary} does not exist in session "${id}"`,
|
|
261
|
+
"INVALID_BOUNDARY",
|
|
262
|
+
);
|
|
263
|
+
} else if (boundaryEvent.type !== "turn/end" && boundaryEvent.type !== "user/message") {
|
|
264
|
+
throw new SessionBranchError(
|
|
265
|
+
`rewind boundary ${toBoundary} is not a turn/end or user/message (${boundaryEvent.type})`,
|
|
266
|
+
"INVALID_BOUNDARY",
|
|
267
|
+
);
|
|
268
|
+
}
|
|
269
|
+
// 保留前缀长度:turn/end inclusive;user/message exclusive(边界消息由
|
|
270
|
+
// 编辑版替换)。exclusive 截断可能残留孤儿 step/start,经平衡化剔除。
|
|
271
|
+
const rawKeepLength =
|
|
272
|
+
toBoundary === -1 ? 0 : boundaryEvent!.type === "turn/end" ? toBoundary + 1 : toBoundary;
|
|
273
|
+
const keepLength = balanceRewindPrefix(events.slice(0, rawKeepLength)).length;
|
|
274
|
+
|
|
275
|
+
const internals = this.persistence.internals();
|
|
276
|
+
// live 视图是上游 seq(含被过滤的 delta),RDB head 是稠密 seq——live
|
|
277
|
+
// 边界须换算为前缀中 persisted 事件数 - 1;cold 视图本身已是稠密 seq。
|
|
278
|
+
const denseBoundary =
|
|
279
|
+
live === undefined
|
|
280
|
+
? keepLength - 1
|
|
281
|
+
: events.slice(0, keepLength).filter(isPersistedEvent).length - 1;
|
|
282
|
+
const newSeedLength = await internals.backend.transaction(async (tx) => {
|
|
283
|
+
signal?.throwIfAborted();
|
|
284
|
+
const head = await tx.getHead(id);
|
|
285
|
+
if (denseBoundary > head.fHeadSequence) {
|
|
286
|
+
throw new SessionBranchError(
|
|
287
|
+
`rewind boundary ${toBoundary} is beyond the stored head ${head.fHeadSequence}`,
|
|
288
|
+
"INVALID_BOUNDARY",
|
|
289
|
+
);
|
|
290
|
+
}
|
|
291
|
+
if (denseBoundary < head.fHeadSequence) {
|
|
292
|
+
await tx.deleteBridgeTail(id, denseBoundary + 1);
|
|
293
|
+
const prev = denseBoundary === -1 ? undefined : await tx.getPrevBridge(id, denseBoundary);
|
|
294
|
+
if (prev === undefined) {
|
|
295
|
+
await tx.updateHead(id, "", -1);
|
|
296
|
+
} else {
|
|
297
|
+
await tx.updateHead(id, prev.fEventId, prev.fSequence);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
// 截断进入继承前缀后收缩 f_seed_length(只收缩、不扩张),防止存储
|
|
301
|
+
// 出现「继承前缀超过存储事件数」的矛盾(上游 load 判损坏)。
|
|
302
|
+
const storedSeedLength = await tx.getSeedLength(id);
|
|
303
|
+
let shrunk = storedSeedLength;
|
|
304
|
+
if (storedSeedLength !== null && storedSeedLength > denseBoundary + 1) {
|
|
305
|
+
await tx.updateSeedLength(id, denseBoundary + 1);
|
|
306
|
+
shrunk = denseBoundary + 1;
|
|
307
|
+
}
|
|
308
|
+
await tx.bumpRevision(id);
|
|
309
|
+
return shrunk;
|
|
310
|
+
});
|
|
311
|
+
// 同步 coordinator 的 storage.inheritedEventCount——DB 已收缩而内存不
|
|
312
|
+
// 收缩的话,下一次 append 的 upsert 会把旧值覆盖回去。
|
|
313
|
+
if (newSeedLength !== null) {
|
|
314
|
+
this.live.setCoordinatorSeedLength(id, newSeedLength);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// 更新确认 head(下一次 append 的并发校验基准),与 appendBatch 同语义。
|
|
318
|
+
internals.writeGuard.confirmHead(id, denseBoundary);
|
|
319
|
+
|
|
320
|
+
if (live !== undefined) {
|
|
321
|
+
// live 分支:截断内存 log 并重置派生缓存;同步 coordinator cursor 与
|
|
322
|
+
// agent 轮次游标。不调用 load——live 时 load 会先 flush 把旧内存写回,
|
|
323
|
+
// 撤销本次截断。
|
|
324
|
+
truncateLiveSession(live, keepLength);
|
|
325
|
+
const agent = this.live.getAgent(id);
|
|
326
|
+
if (agent !== undefined) {
|
|
327
|
+
agent.requestHeaderLogged = false;
|
|
328
|
+
// 重置 agent 的轮次游标,使重放(followup)复用目标轮号而非递增。
|
|
329
|
+
const lastTurn =
|
|
330
|
+
live.snapshotEvents().findLast((e) => e.type === "turn/start")?.data.turn ?? 0;
|
|
331
|
+
const phase = (agent as unknown as { phase?: { lastTurn?: number } }).phase;
|
|
332
|
+
if (phase !== undefined) phase.lastTurn = lastTurn;
|
|
333
|
+
}
|
|
334
|
+
this.live.setCoordinatorCursor(id, keepLength);
|
|
335
|
+
} else {
|
|
336
|
+
// cold 分支:user/message 边界直接同步 ownerless states 条目(load 会
|
|
337
|
+
// 补 closers 撤销截断语义);turn/end 边界经 load 重新 adopt。
|
|
338
|
+
if (boundaryEvent?.type === "user/message") {
|
|
339
|
+
this.live.setCoordinatorState(id, keepLength, meta);
|
|
340
|
+
} else {
|
|
341
|
+
await this.persistence.load(id);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
const row = await internals.backend.getSession(id);
|
|
346
|
+
if (row === undefined) {
|
|
347
|
+
// 空会话(toBoundary = -1 且从未有行):返回「已确认缺席」快照。
|
|
348
|
+
return {
|
|
349
|
+
header: {
|
|
350
|
+
version: SESSION_FORMAT_VERSION,
|
|
351
|
+
id,
|
|
352
|
+
createdAt: meta.createdAt,
|
|
353
|
+
...(meta.cwd !== undefined ? { cwd: meta.cwd } : {}),
|
|
354
|
+
...(meta.parentSession !== undefined ? { parentSession: meta.parentSession } : {}),
|
|
355
|
+
isSeeded: meta.isSeeded,
|
|
356
|
+
...(meta.origin !== undefined ? { origin: meta.origin } : {}),
|
|
357
|
+
...(meta.delegationDepth !== undefined ? { delegationDepth: meta.delegationDepth } : {}),
|
|
358
|
+
...(meta.agentPreset !== undefined ? { agentPreset: meta.agentPreset } : {}),
|
|
359
|
+
},
|
|
360
|
+
revision:
|
|
361
|
+
(await internals.readStoredRevision(id)) ??
|
|
362
|
+
(await this.persistence.readStoredRevision(id))!,
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
return { header: rowToMeta(row), revision: (await internals.readStoredRevision(id))! };
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
export class SessionBranchRdb extends SessionBranch {
|
|
370
|
+
static inject = ["sessionPersistence", "sessions"];
|
|
371
|
+
|
|
372
|
+
constructor(ctx: import("@deepseek-ai/cordis").Context) {
|
|
373
|
+
super(ctx);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
private readonly provider = new SessionBranchRdbProvider(
|
|
377
|
+
this.ctx.sessionPersistence as SessionPersistenceRdb,
|
|
378
|
+
{
|
|
379
|
+
getSession: (id) => this.ctx.sessions.get(id),
|
|
380
|
+
getAgent: (id) => {
|
|
381
|
+
// agents 服务可选(纯持久化环境无 agent-loop):经 ctx.get 动态访问。
|
|
382
|
+
const agents = this.ctx.get("agents") as
|
|
383
|
+
| { get(id: SessionId): LiveAgentLike | undefined }
|
|
384
|
+
| undefined;
|
|
385
|
+
return agents?.get(id);
|
|
386
|
+
},
|
|
387
|
+
flush: (session) => this.ctx.sessions.flush(session),
|
|
388
|
+
setCoordinatorCursor: (id, cursor) => {
|
|
389
|
+
// states 是私有 Map;保留条目(owner 不能丢),仅对齐 cursor 到新尾部。
|
|
390
|
+
const persistence = this.ctx.sessionPersistence as unknown as {
|
|
391
|
+
coordinator?: {
|
|
392
|
+
states?: Map<SessionId, { cursor: number } | undefined>;
|
|
393
|
+
};
|
|
394
|
+
};
|
|
395
|
+
const state = persistence.coordinator?.states?.get(id);
|
|
396
|
+
if (state !== undefined) state.cursor = cursor;
|
|
397
|
+
},
|
|
398
|
+
setCoordinatorState: (id, cursor, meta) => {
|
|
399
|
+
// states 条目可能不存在(会话从未被 adopt);存在则仅对齐 cursor,
|
|
400
|
+
// 不存在则创建 ownerless 条目,使下一次 append 走标准路径而非 adopt
|
|
401
|
+
// (adopt 会经 prepareCore 补 closers 撤销截断)。
|
|
402
|
+
const persistence = this.ctx.sessionPersistence as unknown as {
|
|
403
|
+
coordinator?: {
|
|
404
|
+
states?: Map<
|
|
405
|
+
SessionId,
|
|
406
|
+
{ cursor: number; meta: SessionHeader; materialized: boolean } | undefined
|
|
407
|
+
>;
|
|
408
|
+
};
|
|
409
|
+
};
|
|
410
|
+
const states = persistence.coordinator?.states;
|
|
411
|
+
if (states === undefined) return;
|
|
412
|
+
const state = states.get(id);
|
|
413
|
+
if (state !== undefined) {
|
|
414
|
+
state.cursor = cursor;
|
|
415
|
+
} else {
|
|
416
|
+
states.set(id, { meta, cursor, materialized: true });
|
|
417
|
+
}
|
|
418
|
+
},
|
|
419
|
+
setCoordinatorSeedLength: (id, seedLength) => {
|
|
420
|
+
// 收缩 storage.inheritedEventCount,与 DB 事务内的收缩保持一致——
|
|
421
|
+
// 不同步的话下一次 append 的 upsert 会把旧值覆盖回去。storage 对象
|
|
422
|
+
// 可能被冻结,整体替换 state.storage 而非改字段。
|
|
423
|
+
const persistence = this.ctx.sessionPersistence as unknown as {
|
|
424
|
+
coordinator?: {
|
|
425
|
+
states?: Map<
|
|
426
|
+
SessionId,
|
|
427
|
+
| {
|
|
428
|
+
storage?: { meta: SessionHeader; inheritedEventCount: number };
|
|
429
|
+
}
|
|
430
|
+
| undefined
|
|
431
|
+
>;
|
|
432
|
+
};
|
|
433
|
+
};
|
|
434
|
+
const state = persistence.coordinator?.states?.get(id);
|
|
435
|
+
if (state?.storage !== undefined && state.storage.inheritedEventCount > seedLength) {
|
|
436
|
+
state.storage = { ...state.storage, inheritedEventCount: seedLength };
|
|
437
|
+
}
|
|
438
|
+
},
|
|
439
|
+
},
|
|
440
|
+
);
|
|
441
|
+
|
|
442
|
+
readBranchPrefix(
|
|
443
|
+
id: SessionId,
|
|
444
|
+
atSeq?: number,
|
|
445
|
+
mode?: BranchAnchorMode,
|
|
446
|
+
signal?: AbortSignal,
|
|
447
|
+
): Promise<BranchBoundary> {
|
|
448
|
+
return this.provider.readBranchPrefix(id, atSeq, mode, signal);
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
readRawEvents(
|
|
452
|
+
id: SessionId,
|
|
453
|
+
signal?: AbortSignal,
|
|
454
|
+
): Promise<{ meta: SessionHeader; events: readonly SessionEvent[] }> {
|
|
455
|
+
return this.provider.readRawEvents(id, signal);
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
forkFrom(
|
|
459
|
+
sourceId: SessionId,
|
|
460
|
+
options?: ForkFromOptions,
|
|
461
|
+
signal?: AbortSignal,
|
|
462
|
+
): Promise<SessionId> {
|
|
463
|
+
return this.provider.forkFrom(sourceId, options, signal);
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
rewind(
|
|
467
|
+
id: SessionId,
|
|
468
|
+
toBoundary: number,
|
|
469
|
+
signal?: AbortSignal,
|
|
470
|
+
): Promise<SessionPersistenceSnapshot> {
|
|
471
|
+
return this.provider.rewind(id, toBoundary, signal);
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
syncLiveCursor(sessionId: SessionId): void {
|
|
475
|
+
const live = this.ctx.sessions.get(sessionId);
|
|
476
|
+
if (live === undefined) return;
|
|
477
|
+
const persistence = this.ctx.sessionPersistence as unknown as {
|
|
478
|
+
coordinator?: {
|
|
479
|
+
states?: Map<SessionId, { cursor: number } | undefined>;
|
|
480
|
+
};
|
|
481
|
+
};
|
|
482
|
+
const state = persistence.coordinator?.states?.get(sessionId);
|
|
483
|
+
if (state === undefined) return;
|
|
484
|
+
let cursor = state.cursor;
|
|
485
|
+
// ignorable 是下游信封扩展(上游 SessionEvent 无此字段),结构化读取。
|
|
486
|
+
while (
|
|
487
|
+
(live.snapshotEvents()[cursor] as (SessionEvent & { ignorable?: unknown }) | undefined)
|
|
488
|
+
?.ignorable === true
|
|
489
|
+
)
|
|
490
|
+
cursor += 1;
|
|
491
|
+
state.cursor = cursor;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
async timeline(sessionId: SessionId, signal?: AbortSignal) {
|
|
495
|
+
const persistence = this.ctx.sessionPersistence as SessionPersistenceRdb;
|
|
496
|
+
const snapshots = await persistence.listSnapshots(signal);
|
|
497
|
+
// live 会话从内存 log 读自有后缀(含 ignorable 版本效果事件);cold 会话
|
|
498
|
+
// 走持久化 readFrom(版本效果不落 canonical log,timeline 为 lineage 骨架)。
|
|
499
|
+
const readOwnEvents = async (id: SessionId, fromSeq: number, s?: AbortSignal) => {
|
|
500
|
+
const live = this.ctx.sessions.get(id);
|
|
501
|
+
if (live !== undefined) return live.snapshotEvents().slice(fromSeq);
|
|
502
|
+
return (await persistence.readFrom(id, fromSeq, s)).events;
|
|
503
|
+
};
|
|
504
|
+
return buildTimeline(snapshots, readOwnEvents, sessionId, signal);
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
export default SessionBranchRdb;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { TableDef } from "./types.ts";
|
|
2
|
+
|
|
3
|
+
export const events: TableDef = {
|
|
4
|
+
name: "t_events",
|
|
5
|
+
columns: [
|
|
6
|
+
{ name: "f_id", type: "serial", primaryKey: true },
|
|
7
|
+
{ name: "f_event_id", type: "text", notNull: true, unique: true },
|
|
8
|
+
{ name: "f_parent_id", type: "text", notNull: true, default: "" },
|
|
9
|
+
{ name: "f_type", type: "text", notNull: true, default: "" },
|
|
10
|
+
{ name: "f_kind", type: "text", notNull: true, default: "" },
|
|
11
|
+
{ name: "f_role", type: "text", notNull: true, default: "" },
|
|
12
|
+
{ name: "f_name", type: "text", notNull: true, default: "" },
|
|
13
|
+
{ name: "f_action_id", type: "text", notNull: true, default: "" },
|
|
14
|
+
{ name: "f_encoding", type: "text", notNull: true, default: "" },
|
|
15
|
+
{ name: "f_data", type: "text", notNull: true },
|
|
16
|
+
{ name: "f_created_at", type: "bigint", notNull: true, default: 0 },
|
|
17
|
+
],
|
|
18
|
+
// 查询经 f_event_id(列级 UNIQUE 唯一索引)与 t_session_events 复合索引
|
|
19
|
+
// (按 session 过滤后回表);f_parent_id 仅写路径构造;维度列索引为
|
|
20
|
+
// 审计/UI 过滤预留。
|
|
21
|
+
indexes: [
|
|
22
|
+
{ name: "idx_events_kind", columns: ["f_kind"] },
|
|
23
|
+
{ name: "idx_events_role", columns: ["f_role"] },
|
|
24
|
+
{ name: "idx_events_name", columns: ["f_name"] },
|
|
25
|
+
{ name: "idx_events_action_id", columns: ["f_action_id"] },
|
|
26
|
+
],
|
|
27
|
+
};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { persistenceState } from "./persistence-state.ts";
|
|
2
|
+
import { schemaMeta } from "./schema-meta.ts";
|
|
3
|
+
import { sessions } from "./sessions.ts";
|
|
4
|
+
import { events } from "./events.ts";
|
|
5
|
+
import { sessionEvents } from "./session-events.ts";
|
|
6
|
+
|
|
7
|
+
export { persistenceState };
|
|
8
|
+
export { schemaMeta };
|
|
9
|
+
export { sessions };
|
|
10
|
+
export { events };
|
|
11
|
+
export { sessionEvents };
|
|
12
|
+
export type {
|
|
13
|
+
ColumnDef,
|
|
14
|
+
TableDef,
|
|
15
|
+
CheckDef,
|
|
16
|
+
UniqueDef,
|
|
17
|
+
IndexDef,
|
|
18
|
+
ColumnTypeName,
|
|
19
|
+
DeleteAction,
|
|
20
|
+
} from "./types.ts";
|
|
21
|
+
|
|
22
|
+
export const sqliteTableDefs = [persistenceState, sessions, events, sessionEvents] as const;
|
|
23
|
+
|
|
24
|
+
export const postgresTableDefs = [
|
|
25
|
+
persistenceState,
|
|
26
|
+
schemaMeta,
|
|
27
|
+
sessions,
|
|
28
|
+
events,
|
|
29
|
+
sessionEvents,
|
|
30
|
+
] as const;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { TableDef } from "./types.ts";
|
|
2
|
+
|
|
3
|
+
export const persistenceState: TableDef = {
|
|
4
|
+
name: "t_persistence_state",
|
|
5
|
+
columns: [
|
|
6
|
+
{ name: "f_singleton", type: "integer", primaryKey: true },
|
|
7
|
+
{ name: "f_store_id", type: "text", notNull: true },
|
|
8
|
+
],
|
|
9
|
+
checks: [{ name: "ck_persistence_state_singleton", expression: "f_singleton = 1" }],
|
|
10
|
+
};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { TableDef } from "./types.ts";
|
|
2
|
+
|
|
3
|
+
export const sessionEvents: TableDef = {
|
|
4
|
+
name: "t_session_events",
|
|
5
|
+
columns: [
|
|
6
|
+
{ name: "f_id", type: "serial", primaryKey: true },
|
|
7
|
+
{
|
|
8
|
+
name: "f_session_id",
|
|
9
|
+
type: "text",
|
|
10
|
+
notNull: true,
|
|
11
|
+
references: { table: "t_sessions", column: "f_session_id", onDelete: "cascade" },
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
name: "f_event_id",
|
|
15
|
+
type: "text",
|
|
16
|
+
notNull: true,
|
|
17
|
+
references: { table: "t_events", column: "f_event_id", onDelete: "cascade" },
|
|
18
|
+
},
|
|
19
|
+
{ name: "f_sequence", type: "integer", notNull: true },
|
|
20
|
+
{ name: "f_original_seq", type: "integer", notNull: true },
|
|
21
|
+
{ name: "f_surface_op", type: "text" },
|
|
22
|
+
],
|
|
23
|
+
uniques: [
|
|
24
|
+
{ name: "uq_session_events_session_sequence", columns: ["f_session_id", "f_sequence"] },
|
|
25
|
+
],
|
|
26
|
+
// UNIQUE(f_session_id, f_sequence) 自动建唯一索引(按 session 过滤 + seq
|
|
27
|
+
// 范围/排序/取尾);f_event_id 索引覆盖反向查找(孤儿事件行清理)。
|
|
28
|
+
indexes: [{ name: "idx_session_events_event_id", columns: ["f_event_id"] }],
|
|
29
|
+
};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { TableDef } from "./types.ts";
|
|
2
|
+
|
|
3
|
+
export const sessions: TableDef = {
|
|
4
|
+
name: "t_sessions",
|
|
5
|
+
columns: [
|
|
6
|
+
{ name: "f_id", type: "serial", primaryKey: true },
|
|
7
|
+
{ name: "f_session_id", type: "text", notNull: true, unique: true },
|
|
8
|
+
{ name: "f_head_event_id", type: "text", notNull: true, default: "" },
|
|
9
|
+
{ name: "f_head_sequence", type: "integer", notNull: true, default: -1 },
|
|
10
|
+
{ name: "f_version", type: "integer", notNull: true },
|
|
11
|
+
{ name: "f_created_at", type: "bigint", notNull: true },
|
|
12
|
+
{ name: "f_cwd", type: "text" },
|
|
13
|
+
{ name: "f_parent_session", type: "text" },
|
|
14
|
+
{ name: "f_seed_length", type: "integer" },
|
|
15
|
+
{ name: "f_origin", type: "text" },
|
|
16
|
+
{ name: "f_delegation_depth", type: "integer" },
|
|
17
|
+
{ name: "f_incarnation", type: "text", notNull: true },
|
|
18
|
+
{ name: "f_revision", type: "integer", notNull: true },
|
|
19
|
+
],
|
|
20
|
+
};
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
export type ColumnTypeName = "serial" | "integer" | "bigint" | "text";
|
|
2
|
+
|
|
3
|
+
export type DeleteAction = "cascade" | "set null" | "restrict" | "no action";
|
|
4
|
+
|
|
5
|
+
export interface ColumnDef {
|
|
6
|
+
name: string;
|
|
7
|
+
type: ColumnTypeName;
|
|
8
|
+
notNull?: boolean;
|
|
9
|
+
|
|
10
|
+
primaryKey?: boolean;
|
|
11
|
+
|
|
12
|
+
default?: string | number;
|
|
13
|
+
|
|
14
|
+
unique?: boolean;
|
|
15
|
+
|
|
16
|
+
references?: {
|
|
17
|
+
table: string;
|
|
18
|
+
column: string;
|
|
19
|
+
onDelete?: DeleteAction;
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface CheckDef {
|
|
24
|
+
name: string;
|
|
25
|
+
expression: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface UniqueDef {
|
|
29
|
+
name?: string;
|
|
30
|
+
columns: string[];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface IndexDef {
|
|
34
|
+
name: string;
|
|
35
|
+
columns: string[];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface TableDef {
|
|
39
|
+
name: string;
|
|
40
|
+
columns: ColumnDef[];
|
|
41
|
+
checks?: CheckDef[];
|
|
42
|
+
uniques?: UniqueDef[];
|
|
43
|
+
indexes?: IndexDef[];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function toProperty(name: string): string {
|
|
47
|
+
return name.replace(/_([a-z])/g, (_match, char: string) => char.toUpperCase());
|
|
48
|
+
}
|