@morlay/session-rdb 0.0.14 → 0.0.16-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 +11 -13
- package/dist/artifact.d.mts +6 -16
- package/dist/artifact.mjs +3 -3
- package/dist/{import-B-tf5vQn.mjs → import-CKrzjXVB.mjs} +73 -83
- package/dist/import.d.mts +2 -3
- package/dist/import.mjs +2 -2
- package/dist/index-CgGDHQSK.d.mts +225 -0
- package/dist/index.d.mts +3 -3
- package/dist/index.mjs +694 -169
- package/dist/log-DTJsxMud.mjs +314 -0
- package/dist/{schema-CFXZURX7.d.mts → schema-COK7-wRV.d.mts} +3 -15
- package/dist/sqlite-DCy4VTD8.mjs +698 -0
- package/dist/storage.d.mts +2 -7
- package/dist/storage.mjs +2 -3
- package/dist/testing.d.mts +30 -1
- package/dist/testing.mjs +676 -1991
- package/drizzle/postgres/20260908072805_v2_initial/migration.sql +58 -0
- package/drizzle/postgres/20260908072805_v2_initial/snapshot.json +686 -0
- package/drizzle/postgres/20260908072806_v3_drop_original_seq/migration.sql +1 -0
- package/drizzle/postgres/20260908072806_v3_drop_original_seq/snapshot.json +673 -0
- package/drizzle/sqlite/20260908072751_v2_initial/migration.sql +53 -0
- package/drizzle/sqlite/20260908072751_v2_initial/snapshot.json +492 -0
- package/drizzle/sqlite/20260908072752_v3_drop_original_seq/migration.sql +6 -0
- package/drizzle/sqlite/20260908072752_v3_drop_original_seq/snapshot.json +513 -0
- package/package.json +17 -12
- package/src/adapters/index.ts +10 -2
- package/src/adapters/to-postgres.ts +19 -15
- package/src/adapters/to-sqlite.ts +20 -14
- package/src/{entities → adapters}/types.ts +7 -8
- package/src/backend.ts +0 -7
- package/src/branch.ts +29 -110
- package/src/drizzle/postgres-v2.ts +11 -0
- package/src/drizzle/postgres-v3.ts +11 -0
- package/src/drizzle/sqlite-v2.ts +10 -0
- package/src/drizzle/sqlite-v3.ts +11 -0
- package/src/entities/index.ts +2 -30
- package/src/entities/v2/index.ts +20 -0
- package/src/entities/v2/session-events.ts +11 -0
- package/src/entities/v3/events.ts +27 -0
- package/src/entities/v3/index.ts +27 -0
- package/src/entities/v3/persistence-state.ts +10 -0
- package/src/entities/v3/schema-meta.ts +9 -0
- package/src/entities/v3/session-events.ts +26 -0
- package/src/entities/v3/sessions.ts +20 -0
- package/src/import.ts +65 -57
- package/src/index.ts +868 -223
- package/src/invariant.ts +0 -2
- package/src/legacy.ts +67 -0
- package/src/log.ts +41 -81
- package/src/postgres.ts +75 -93
- package/src/schema.ts +3 -14
- package/src/sqlite.ts +59 -46
- package/src/testing/contract.ts +460 -375
- package/src/testing/coordinator-contract.ts +108 -1374
- package/src/testing.ts +1 -6
- package/dist/index-uUvn6gYp.d.mts +0 -111
- package/dist/schema-BfPVmr1X.mjs +0 -875
- package/dist/sqlite-BUWnS0so.mjs +0 -356
- package/src/adapters/ddl.ts +0 -77
- package/src/entities/events.ts +0 -27
- package/src/entities/persistence-state.ts +0 -10
- package/src/entities/schema-meta.ts +0 -9
- package/src/entities/session-events.ts +0 -29
- package/src/entities/sessions.ts +0 -20
- package/src/migrate.ts +0 -137
package/src/index.ts
CHANGED
|
@@ -5,27 +5,41 @@ import { randomUUID } from "node:crypto";
|
|
|
5
5
|
import { Pool } from "pg";
|
|
6
6
|
import { drizzle as drizzlePg } from "drizzle-orm/node-postgres";
|
|
7
7
|
import {
|
|
8
|
+
SessionAlreadyExistsError,
|
|
9
|
+
SessionAlreadyOwnedError,
|
|
10
|
+
SessionHandleClosedError,
|
|
8
11
|
SessionPersistence,
|
|
12
|
+
SessionPersistenceNotFoundError,
|
|
9
13
|
SessionPersistenceRevision,
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
14
|
+
SessionReadOnlyError,
|
|
15
|
+
assertContiguous,
|
|
16
|
+
assertVersion,
|
|
17
|
+
materializeAppendBatch,
|
|
18
|
+
materializeCreateHeader,
|
|
19
|
+
validateStoredEvents,
|
|
20
|
+
type SessionAccess,
|
|
21
|
+
type SessionHandle,
|
|
22
|
+
type SessionHandleAppendOptions,
|
|
23
|
+
type SessionHandleFlushOptions,
|
|
24
|
+
type SessionHandleReadOptions,
|
|
25
|
+
type SessionHandleReadResult,
|
|
26
|
+
type SessionPersistenceCreateOptions,
|
|
27
|
+
type SessionPersistenceListOptions,
|
|
28
|
+
type SessionPersistenceOpenOptions,
|
|
13
29
|
type SessionPersistenceSnapshot,
|
|
14
|
-
type
|
|
15
|
-
type StoredPrefix,
|
|
16
|
-
type StoredSuffix,
|
|
30
|
+
type SessionPersistenceStatOptions,
|
|
17
31
|
} from "@deepseek-ai/dsh-session-persistence";
|
|
18
32
|
import {
|
|
19
33
|
SessionLogOffset,
|
|
34
|
+
type Session,
|
|
20
35
|
type SessionEvent,
|
|
21
|
-
type SurfaceEventType,
|
|
22
|
-
type SessionId,
|
|
23
36
|
type SessionHeader,
|
|
37
|
+
type SessionId,
|
|
38
|
+
type SurfaceEventType,
|
|
24
39
|
} from "@deepseek-ai/dsh-session";
|
|
25
|
-
import { type Backend, type BackendTx, type EventInsert
|
|
40
|
+
import { type Backend, type BackendTx, type EventInsert } from "./backend.ts";
|
|
26
41
|
import { WriteGuard } from "./write-guard.ts";
|
|
27
42
|
import {
|
|
28
|
-
buildSeqMap,
|
|
29
43
|
recomputeReplaceProvenance,
|
|
30
44
|
repairOrphanInboxSplices,
|
|
31
45
|
repairSurfaceOps,
|
|
@@ -37,21 +51,21 @@ import {
|
|
|
37
51
|
DEFAULT_BUSY_TIMEOUT_MS,
|
|
38
52
|
eventDimensions,
|
|
39
53
|
EVENT_ENCODING,
|
|
40
|
-
isPersistedEvent,
|
|
41
54
|
type JournalMode,
|
|
42
55
|
} from "./schema.ts";
|
|
43
56
|
import { SqliteBackend } from "./sqlite.ts";
|
|
44
57
|
import { PostgresBackend } from "./postgres.ts";
|
|
45
58
|
import { SessionBranchRdb } from "./branch.ts";
|
|
46
59
|
import { registerSessionImport } from "./import.ts";
|
|
60
|
+
import { convertLegacyRows, isLegacyVersion } from "./legacy.ts";
|
|
47
61
|
|
|
48
|
-
export { SCHEMA_VERSION
|
|
62
|
+
export { SCHEMA_VERSION } from "./schema.ts";
|
|
49
63
|
export { SessionBranchRdb, SessionBranchRdbProvider, locateTurnEnd } from "./branch.ts";
|
|
50
64
|
|
|
51
65
|
export interface SessionPersistenceRdbInternals {
|
|
52
66
|
readonly backend: Backend;
|
|
53
67
|
readonly writeGuard: WriteGuard;
|
|
54
|
-
create(meta: SessionHeader, inheritedEventCount?: number): Promise<
|
|
68
|
+
create(meta: SessionHeader, inheritedEventCount?: number): Promise<SessionHandle>;
|
|
55
69
|
append(id: SessionId, events: readonly SessionEvent[]): Promise<void>;
|
|
56
70
|
load(id: SessionId): Promise<import("@deepseek-ai/dsh-session-persistence").SessionInspection>;
|
|
57
71
|
inspect(
|
|
@@ -62,7 +76,7 @@ export interface SessionPersistenceRdbInternals {
|
|
|
62
76
|
id: SessionId,
|
|
63
77
|
fromSeq: number,
|
|
64
78
|
signal?: AbortSignal,
|
|
65
|
-
): Promise<
|
|
79
|
+
): Promise<{ meta: SessionHeader; inheritedEventCount: number; events: readonly SessionEvent[] }>;
|
|
66
80
|
listSnapshots(signal?: AbortSignal): Promise<SessionPersistenceSnapshot[]>;
|
|
67
81
|
readStoredRevision(
|
|
68
82
|
id: SessionId,
|
|
@@ -90,10 +104,343 @@ export type Config =
|
|
|
90
104
|
schema?: string;
|
|
91
105
|
};
|
|
92
106
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
107
|
+
/** 一个已创建但未 materialize 的会话(本进程可见,其他进程不可见)。 */
|
|
108
|
+
interface PendingSession {
|
|
109
|
+
readonly header: SessionHeader;
|
|
110
|
+
readonly revision: SessionPersistenceRevision;
|
|
111
|
+
readonly inheritedEventCount: SessionLogOffset;
|
|
112
|
+
/** 输入空间 cursor(全 delta 批次也推进)。 */
|
|
113
|
+
readonly cursor: number;
|
|
114
|
+
/** 是否调用过 append(close 时保留 pending)。 */
|
|
115
|
+
readonly everAppended: boolean;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** 会话级写所有权与 live 路由簿记。 */
|
|
119
|
+
class RdbBackendTracker {
|
|
120
|
+
/** 每个 id 的活跃 write handle;`null` 表示 claim 构造中。 */
|
|
121
|
+
private readonly writers = new Map<SessionId, RdbSessionHandle | null>();
|
|
122
|
+
private readonly pending = new Map<SessionId, PendingSession>();
|
|
123
|
+
private readonly openHandles = new Set<RdbSessionHandle>();
|
|
124
|
+
private counter = 0;
|
|
125
|
+
|
|
126
|
+
constructor(private readonly name: string) {}
|
|
127
|
+
|
|
128
|
+
registerCreated(header: SessionHeader, inheritedEventCount: SessionLogOffset): void {
|
|
129
|
+
if (this.writers.has(header.id)) throw new SessionAlreadyExistsError(header.id);
|
|
130
|
+
this.writers.set(header.id, null);
|
|
131
|
+
this.pending.set(header.id, {
|
|
132
|
+
header,
|
|
133
|
+
revision: SessionPersistenceRevision(`memory:${this.name}:${++this.counter}`),
|
|
134
|
+
inheritedEventCount,
|
|
135
|
+
cursor: 0,
|
|
136
|
+
everAppended: false,
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** 更新 pending 的 cursor / everAppended(handle append 后同步)。 */
|
|
141
|
+
updatePending(id: SessionId, cursor: number, everAppended: boolean): void {
|
|
142
|
+
const entry = this.pending.get(id);
|
|
143
|
+
if (entry === undefined) return;
|
|
144
|
+
this.pending.set(id, { ...entry, cursor, everAppended });
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
claimWrite(id: SessionId): void {
|
|
148
|
+
if (this.writers.has(id)) throw new SessionAlreadyOwnedError(id);
|
|
149
|
+
this.writers.set(id, null);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
releaseClaim(id: SessionId): void {
|
|
153
|
+
this.writers.delete(id);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
pendingOf(id: SessionId): PendingSession | undefined {
|
|
157
|
+
return this.pending.get(id);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
hasPending(id: SessionId): boolean {
|
|
161
|
+
return this.pending.has(id);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
pendingEntries(): IterableIterator<[SessionId, PendingSession]> {
|
|
165
|
+
return this.pending.entries();
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
materialized(id: SessionId): void {
|
|
169
|
+
this.pending.delete(id);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
adopt(handle: RdbSessionHandle): RdbSessionHandle {
|
|
173
|
+
this.openHandles.add(handle);
|
|
174
|
+
if (handle.access === "write") this.writers.set(handle.id, handle);
|
|
175
|
+
return handle;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
release(handle: RdbSessionHandle, materialized: boolean): void {
|
|
179
|
+
this.openHandles.delete(handle);
|
|
180
|
+
if (handle.access !== "write") return;
|
|
181
|
+
this.writers.delete(handle.id);
|
|
182
|
+
if (!materialized) this.pending.delete(handle.id);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
writerOf(id: SessionId): RdbSessionHandle | undefined {
|
|
186
|
+
const writer = this.writers.get(id);
|
|
187
|
+
return writer === null ? undefined : writer;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
async flushAll(): Promise<void> {
|
|
191
|
+
const errors: unknown[] = [];
|
|
192
|
+
for (const writer of this.writers.values()) {
|
|
193
|
+
if (writer === null) continue;
|
|
194
|
+
try {
|
|
195
|
+
await writer.drainLive();
|
|
196
|
+
await writer.flush();
|
|
197
|
+
} catch (error: unknown) {
|
|
198
|
+
if (error instanceof SessionHandleClosedError) continue;
|
|
199
|
+
errors.push(error);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
if (errors.length > 0) throw new AggregateError(errors, `${this.name} flush failed`);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
async closeAll(): Promise<void> {
|
|
206
|
+
const errors: unknown[] = [];
|
|
207
|
+
for (const handle of this.openHandles) {
|
|
208
|
+
try {
|
|
209
|
+
await handle.close();
|
|
210
|
+
} catch (error: unknown) {
|
|
211
|
+
errors.push(error);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
if (errors.length > 0) throw new AggregateError(errors, `${this.name} dispose failed`);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/** 一个打开会话的存储句柄:read / append / flush / close。 */
|
|
219
|
+
class RdbSessionHandle implements SessionHandle {
|
|
220
|
+
private chain: Promise<unknown> = Promise.resolve();
|
|
221
|
+
private closing: Promise<void> | undefined;
|
|
222
|
+
/** 稠密 next-seq(输入空间计数,与旧版 coordinator cursor 同语义)。 */
|
|
223
|
+
private cursor: number;
|
|
224
|
+
private materialized: boolean;
|
|
225
|
+
/** live 路由缓冲(上游 seq 事件,drain 时过滤 delta 后重编号稠密)。 */
|
|
226
|
+
private buffered: SessionEvent[] = [];
|
|
227
|
+
private batchTimer: ReturnType<typeof setTimeout> | undefined;
|
|
228
|
+
private drainPaused = false;
|
|
229
|
+
private draining: Promise<void> | undefined;
|
|
230
|
+
/** write open 时发现的 torn tail 起点(append 前先截断)。 */
|
|
231
|
+
private tornTruncateTo: number | undefined;
|
|
232
|
+
/** 是否调用过 append(即使全 delta 未落库)——close 时保留 pending。 */
|
|
233
|
+
private everAppended = false;
|
|
234
|
+
|
|
235
|
+
constructor(
|
|
236
|
+
private readonly persistence: SessionPersistenceRdb,
|
|
237
|
+
readonly id: SessionId,
|
|
238
|
+
readonly header: SessionHeader,
|
|
239
|
+
readonly access: SessionAccess,
|
|
240
|
+
private readonly state: {
|
|
241
|
+
cursor: number;
|
|
242
|
+
materialized: boolean;
|
|
243
|
+
inheritedEventCount: SessionLogOffset;
|
|
244
|
+
tornTruncateTo?: number;
|
|
245
|
+
},
|
|
246
|
+
) {
|
|
247
|
+
this.cursor = state.cursor;
|
|
248
|
+
this.materialized = state.materialized;
|
|
249
|
+
this.tornTruncateTo = state.tornTruncateTo;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
get inheritedEventCount(): SessionLogOffset {
|
|
253
|
+
return this.state.inheritedEventCount;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/** 输入空间 cursor(下一个待落库的上游 seq)。 */
|
|
257
|
+
get cursorValue(): number {
|
|
258
|
+
return this.cursor;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
async read(
|
|
262
|
+
offset = 0,
|
|
263
|
+
length = Number.MAX_SAFE_INTEGER,
|
|
264
|
+
options?: SessionHandleReadOptions,
|
|
265
|
+
): Promise<SessionHandleReadResult> {
|
|
266
|
+
this.assertOpen("read");
|
|
267
|
+
if (!Number.isSafeInteger(offset) || offset < 0) {
|
|
268
|
+
throw new TypeError(`read offset must be a non-negative safe integer, got ${String(offset)}`);
|
|
269
|
+
}
|
|
270
|
+
if (!Number.isSafeInteger(length) || length < 0) {
|
|
271
|
+
throw new TypeError(`read length must be a non-negative safe integer, got ${String(length)}`);
|
|
272
|
+
}
|
|
273
|
+
options?.signal?.throwIfAborted();
|
|
274
|
+
const log = await this.persistence.readLog(this.id, {}, options?.signal);
|
|
275
|
+
if (log === undefined) {
|
|
276
|
+
if (this.persistence.tracker.hasPending(this.id)) {
|
|
277
|
+
return { eventState: "detached", events: [] };
|
|
278
|
+
}
|
|
279
|
+
throw new SessionPersistenceNotFoundError(this.id);
|
|
280
|
+
}
|
|
281
|
+
// 读取时修复(视图只读,不落库):surface 替换 provenance 重计算、孤儿
|
|
282
|
+
// inbox splice 改写、非法 surface 替换降级。
|
|
283
|
+
repairSurfaceOps(log.events);
|
|
284
|
+
recomputeReplaceProvenance(log.events);
|
|
285
|
+
repairOrphanInboxSplices(log.events);
|
|
286
|
+
return { eventState: "detached", events: log.events.slice(offset, offset + length) };
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
async append(
|
|
290
|
+
events: readonly SessionEvent[],
|
|
291
|
+
options?: SessionHandleAppendOptions,
|
|
292
|
+
): Promise<void> {
|
|
293
|
+
this.assertOpen("append");
|
|
294
|
+
const batch = materializeAppendBatch(events);
|
|
295
|
+
return this.run("append", async () => {
|
|
296
|
+
options?.signal?.throwIfAborted();
|
|
297
|
+
if (this.access !== "write") throw new SessionReadOnlyError(this.id, "append");
|
|
298
|
+
if (batch.length === 0) return;
|
|
299
|
+
this.everAppended = true;
|
|
300
|
+
assertContiguous(this.id, batch, this.cursor);
|
|
301
|
+
await this.persistence.appendBatch(
|
|
302
|
+
this.header,
|
|
303
|
+
this.state.inheritedEventCount,
|
|
304
|
+
batch,
|
|
305
|
+
this.tornTruncateTo,
|
|
306
|
+
);
|
|
307
|
+
this.tornTruncateTo = undefined;
|
|
308
|
+
// 原样存储(与上游 JSONL 一致):cursor 推进 batch.length,全部落库。
|
|
309
|
+
this.cursor += batch.length;
|
|
310
|
+
this.materialized = true;
|
|
311
|
+
this.persistence.tracker.updatePending(this.id, this.cursor, true);
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
async flush(options?: SessionHandleFlushOptions): Promise<void> {
|
|
316
|
+
return this.run("flush", async () => {
|
|
317
|
+
options?.signal?.throwIfAborted();
|
|
318
|
+
if (this.access !== "write") throw new SessionReadOnlyError(this.id, "flush");
|
|
319
|
+
if (this.materialized) return;
|
|
320
|
+
await this.persistence.materializeEmpty(this.header, this.state.inheritedEventCount);
|
|
321
|
+
this.materialized = true;
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
close(): Promise<void> {
|
|
326
|
+
return (this.closing ??= (async () => {
|
|
327
|
+
let drainFailure: unknown;
|
|
328
|
+
for (;;) {
|
|
329
|
+
try {
|
|
330
|
+
await this.drainLive();
|
|
331
|
+
} catch (error: unknown) {
|
|
332
|
+
drainFailure = error;
|
|
333
|
+
break;
|
|
334
|
+
}
|
|
335
|
+
await this.chain;
|
|
336
|
+
if (this.buffered.length === 0) break;
|
|
337
|
+
}
|
|
338
|
+
await this.chain;
|
|
339
|
+
const failures: Error[] = [];
|
|
340
|
+
if (drainFailure !== undefined) {
|
|
341
|
+
failures.push(
|
|
342
|
+
drainFailure instanceof Error ? drainFailure : new Error(JSON.stringify(drainFailure)),
|
|
343
|
+
);
|
|
344
|
+
}
|
|
345
|
+
this.persistence.tracker.release(this, this.materialized || this.everAppended);
|
|
346
|
+
if (failures.length > 1)
|
|
347
|
+
throw new AggregateError(failures, `session "${this.id}": close failed to drain`);
|
|
348
|
+
if (failures[0] !== undefined) throw failures[0];
|
|
349
|
+
})());
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
[Symbol.asyncDispose](): Promise<void> {
|
|
353
|
+
return this.close();
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/** live 路由:缓冲一个已发布事件(持久化自有副本),并启动批量窗口。 */
|
|
357
|
+
enqueueLive(event: SessionEvent, reportBackgroundFailure: (error: unknown) => void): void {
|
|
358
|
+
this.buffered.push(structuredClone(event));
|
|
359
|
+
if (this.batchTimer !== undefined || this.drainPaused) return;
|
|
360
|
+
this.batchTimer = setTimeout(() => {
|
|
361
|
+
this.batchTimer = undefined;
|
|
362
|
+
this.drainLive().catch(reportBackgroundFailure);
|
|
363
|
+
}, RdbSessionHandle.LIVE_WRITE_BATCH_MAX_DELAY_MS);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/** rewind 截断后对齐 handle 的稠密 cursor 与继承前缀(DB 已截断)。 */
|
|
367
|
+
resetAfterRewind(cursor: number, inheritedEventCount?: number): void {
|
|
368
|
+
this.cursor = cursor;
|
|
369
|
+
if (inheritedEventCount !== undefined) {
|
|
370
|
+
(this.state as { inheritedEventCount: SessionLogOffset }).inheritedEventCount =
|
|
371
|
+
SessionLogOffset(inheritedEventCount);
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/** 排空 live 缓冲(过滤 delta + 重编号稠密 + append)。 */
|
|
376
|
+
drainLive(): Promise<void> {
|
|
377
|
+
return (this.draining ??= this.drainBuffered().finally(() => {
|
|
378
|
+
this.draining = undefined;
|
|
379
|
+
}));
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
private async drainBuffered(): Promise<void> {
|
|
383
|
+
if (this.batchTimer !== undefined) {
|
|
384
|
+
clearTimeout(this.batchTimer);
|
|
385
|
+
this.batchTimer = undefined;
|
|
386
|
+
}
|
|
387
|
+
this.drainPaused = false;
|
|
388
|
+
while (this.buffered.length > 0) {
|
|
389
|
+
await this.enqueueChain(async () => {
|
|
390
|
+
const batch = this.buffered.splice(0);
|
|
391
|
+
try {
|
|
392
|
+
// 过滤已由 ensureLiveHandle 落库的 seed 前缀(上游 seq 空间,
|
|
393
|
+
// 与 public append 的 cursor 同空间)。直接走持久化原语(本函数
|
|
394
|
+
// 已在 chain 内,走 public append 会自锁)。
|
|
395
|
+
const fresh = batch.filter((event) => event.seq >= this.cursor);
|
|
396
|
+
if (fresh.length === 0) return;
|
|
397
|
+
for (const [index, event] of fresh.entries()) {
|
|
398
|
+
if (event.seq !== this.cursor + index) {
|
|
399
|
+
throw new Error(
|
|
400
|
+
`append seq mismatch for "${this.id}": expected ${this.cursor + index} at index ${index}, got ${event.seq}`,
|
|
401
|
+
);
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
await this.persistence.appendBatch(
|
|
405
|
+
this.header,
|
|
406
|
+
this.state.inheritedEventCount,
|
|
407
|
+
fresh,
|
|
408
|
+
this.tornTruncateTo,
|
|
409
|
+
);
|
|
410
|
+
this.tornTruncateTo = undefined;
|
|
411
|
+
this.cursor += fresh.length;
|
|
412
|
+
this.materialized = true;
|
|
413
|
+
} catch (error: unknown) {
|
|
414
|
+
this.buffered = batch.concat(this.buffered);
|
|
415
|
+
this.drainPaused = true;
|
|
416
|
+
throw error;
|
|
417
|
+
}
|
|
418
|
+
});
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
private enqueueChain(op: () => Promise<void>): Promise<void> {
|
|
423
|
+
const next = this.chain.then(op);
|
|
424
|
+
this.chain = next.catch(() => {});
|
|
425
|
+
return next;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
private async run(operation: string, op: () => Promise<void>): Promise<void> {
|
|
429
|
+
this.assertOpen(operation);
|
|
430
|
+
return this.enqueueChain(async () => {
|
|
431
|
+
this.assertOpen(operation);
|
|
432
|
+
return op();
|
|
433
|
+
});
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
private assertOpen(operation: string): void {
|
|
437
|
+
if (this.closing !== undefined) throw new SessionHandleClosedError(this.id, operation);
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
static readonly LIVE_WRITE_BATCH_MAX_DELAY_MS = 200;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
export class SessionPersistenceRdb extends SessionPersistence {
|
|
97
444
|
static inject = ["sessions", "settings"];
|
|
98
445
|
|
|
99
446
|
static Config: z<Config> = z.union([
|
|
@@ -114,37 +461,20 @@ export class SessionPersistenceRdb
|
|
|
114
461
|
|
|
115
462
|
override readonly name = "session-rdb";
|
|
116
463
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
override async readRaw(
|
|
120
|
-
id: SessionId,
|
|
121
|
-
signal?: AbortSignal,
|
|
122
|
-
): Promise<import("@deepseek-ai/dsh-session-persistence").SessionRawArtifact | undefined> {
|
|
123
|
-
signal?.throwIfAborted();
|
|
124
|
-
await this.ready;
|
|
125
|
-
signal?.throwIfAborted();
|
|
126
|
-
const log = await this.readLog(id, {}, signal);
|
|
127
|
-
if (log === undefined) return undefined;
|
|
128
|
-
repairSurfaceOps(log.events);
|
|
129
|
-
recomputeReplaceProvenance(log.events);
|
|
130
|
-
const inheritedEventCount = Math.min(log.inheritedEventCount, log.events.length);
|
|
131
|
-
return {
|
|
132
|
-
meta: log.meta,
|
|
133
|
-
inheritedEventCount: SessionLogOffset(inheritedEventCount),
|
|
134
|
-
filename: "session.jsonl",
|
|
135
|
-
content: toJsonlArtifact(log.meta, inheritedEventCount, log.events),
|
|
136
|
-
};
|
|
137
|
-
}
|
|
464
|
+
readonly tracker = new RdbBackendTracker(this.name);
|
|
138
465
|
|
|
139
466
|
private readonly backend: Backend;
|
|
140
467
|
private storeIdentity!: string;
|
|
141
468
|
private readonly ready: Promise<void>;
|
|
142
|
-
private readonly coordinator: PersistenceCoordinator<number>;
|
|
143
469
|
|
|
144
470
|
private readonly writeGuard = new WriteGuard();
|
|
145
471
|
|
|
146
472
|
private readonly reuseEventIds = new Map<SessionId, Map<number, string>>();
|
|
147
473
|
|
|
474
|
+
/** live 路由:session/created 后 handle 就绪前的缓冲。 */
|
|
475
|
+
private readonly liveBuffers = new Map<SessionId, SessionEvent[]>();
|
|
476
|
+
private readonly liveReady = new Map<SessionId, Promise<void>>();
|
|
477
|
+
|
|
148
478
|
constructor(
|
|
149
479
|
ctx: Context,
|
|
150
480
|
public config: Config,
|
|
@@ -172,7 +502,7 @@ export class SessionPersistenceRdb
|
|
|
172
502
|
this.config = resolved;
|
|
173
503
|
this.backend = injectedBackend ?? createBackend(resolved);
|
|
174
504
|
this.ready = this.init();
|
|
175
|
-
this.
|
|
505
|
+
this.installLiveRouting(ctx);
|
|
176
506
|
// 分支 provider 服务(rewind / forkFrom / timeline),随 fiber 卸载自动回滚。
|
|
177
507
|
new SessionBranchRdb(this.ctx);
|
|
178
508
|
// 导入端点:webServer + connection 就绪后注册 `/api/session.import`。
|
|
@@ -184,176 +514,216 @@ export class SessionPersistenceRdb
|
|
|
184
514
|
this.storeIdentity = this.backend.storeIdentity;
|
|
185
515
|
}
|
|
186
516
|
|
|
187
|
-
// --- SessionPersistence service surface
|
|
517
|
+
// --- SessionPersistence service surface ---
|
|
188
518
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
519
|
+
async create(
|
|
520
|
+
header: SessionHeader,
|
|
521
|
+
options?: SessionPersistenceCreateOptions,
|
|
522
|
+
): Promise<SessionHandle> {
|
|
523
|
+
options?.signal?.throwIfAborted();
|
|
524
|
+
const snapshot = materializeCreateHeader(header);
|
|
525
|
+
if (snapshot.isSeeded && options?.inheritedEventCount === undefined) {
|
|
526
|
+
throw new TypeError("seeded session metadata requires an inherited event count");
|
|
527
|
+
}
|
|
528
|
+
const inheritedEventCount = SessionLogOffset(options?.inheritedEventCount ?? 0);
|
|
529
|
+
if (!snapshot.isSeeded && inheritedEventCount !== 0) {
|
|
530
|
+
throw new TypeError("unseeded session metadata inherited event count must be 0");
|
|
531
|
+
}
|
|
532
|
+
await this.ready;
|
|
533
|
+
options?.signal?.throwIfAborted();
|
|
534
|
+
if (
|
|
535
|
+
this.tracker.hasPending(snapshot.id) ||
|
|
536
|
+
(await this.backend.getSession(snapshot.id)) !== undefined
|
|
537
|
+
) {
|
|
538
|
+
throw new SessionAlreadyExistsError(snapshot.id);
|
|
539
|
+
}
|
|
540
|
+
this.tracker.registerCreated(snapshot, inheritedEventCount);
|
|
541
|
+
return this.tracker.adopt(
|
|
542
|
+
new RdbSessionHandle(this, snapshot.id, snapshot, "write", {
|
|
543
|
+
cursor: 0,
|
|
544
|
+
materialized: false,
|
|
545
|
+
inheritedEventCount,
|
|
546
|
+
}),
|
|
197
547
|
);
|
|
198
548
|
}
|
|
199
549
|
|
|
200
|
-
|
|
201
|
-
return this.coordinator.append(id, events);
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
load(id: SessionId): Promise<import("@deepseek-ai/dsh-session-persistence").SessionInspection> {
|
|
205
|
-
return this.coordinator.load(id);
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
inspect(
|
|
209
|
-
id: SessionId,
|
|
210
|
-
signal?: AbortSignal,
|
|
211
|
-
): Promise<import("@deepseek-ai/dsh-session-persistence").SessionInspection> {
|
|
212
|
-
return this.coordinator.inspect(id, signal);
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
readFrom(
|
|
550
|
+
async open(
|
|
216
551
|
id: SessionId,
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
): Promise<
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
552
|
+
access: SessionAccess,
|
|
553
|
+
options?: SessionPersistenceOpenOptions,
|
|
554
|
+
): Promise<SessionHandle> {
|
|
555
|
+
options?.signal?.throwIfAborted();
|
|
556
|
+
await this.ready;
|
|
557
|
+
options?.signal?.throwIfAborted();
|
|
558
|
+
const pending = this.tracker.pendingOf(id);
|
|
559
|
+
if (access === "read") {
|
|
560
|
+
if (pending !== undefined) {
|
|
561
|
+
return this.tracker.adopt(
|
|
562
|
+
new RdbSessionHandle(this, id, pending.header, "read", {
|
|
563
|
+
cursor: 0,
|
|
564
|
+
materialized: false,
|
|
565
|
+
inheritedEventCount: pending.inheritedEventCount,
|
|
566
|
+
}),
|
|
567
|
+
);
|
|
568
|
+
}
|
|
569
|
+
const log = await this.readLog(id, {}, options?.signal);
|
|
570
|
+
if (log === undefined) throw new SessionPersistenceNotFoundError(id);
|
|
571
|
+
// fail-closed:未知事件类型(非 ignorable)拒绝解释。
|
|
572
|
+
validateStoredEvents(log.meta, log.events);
|
|
573
|
+
return this.tracker.adopt(
|
|
574
|
+
new RdbSessionHandle(this, id, log.meta, "read", {
|
|
575
|
+
cursor: log.events.length,
|
|
576
|
+
materialized: true,
|
|
577
|
+
inheritedEventCount: SessionLogOffset(log.inheritedEventCount),
|
|
578
|
+
}),
|
|
579
|
+
);
|
|
580
|
+
}
|
|
581
|
+
this.tracker.claimWrite(id);
|
|
582
|
+
try {
|
|
583
|
+
// pending(created 未 materialize)会话:write open 接管其所有权。
|
|
584
|
+
if (pending !== undefined) {
|
|
585
|
+
return this.tracker.adopt(
|
|
586
|
+
new RdbSessionHandle(this, id, pending.header, "write", {
|
|
587
|
+
cursor: pending.cursor,
|
|
588
|
+
materialized: false,
|
|
589
|
+
inheritedEventCount: pending.inheritedEventCount,
|
|
590
|
+
}),
|
|
591
|
+
);
|
|
592
|
+
}
|
|
593
|
+
const log = await this.readLog(id, {}, options?.signal);
|
|
594
|
+
if (log === undefined) throw new SessionPersistenceNotFoundError(id);
|
|
595
|
+
validateStoredEvents(log.meta, log.events);
|
|
596
|
+
// 确认 head:本实例已读该会话,后续 append 的并发校验以此为基准。
|
|
597
|
+
this.writeGuard.confirmHead(id, log.events.at(-1)?.seq ?? -1);
|
|
598
|
+
return this.tracker.adopt(
|
|
599
|
+
new RdbSessionHandle(this, id, log.meta, "write", {
|
|
600
|
+
cursor: log.events.length,
|
|
601
|
+
materialized: true,
|
|
602
|
+
inheritedEventCount: SessionLogOffset(log.inheritedEventCount),
|
|
603
|
+
...(log.tornFrom !== undefined ? { tornTruncateTo: log.tornFrom } : {}),
|
|
604
|
+
}),
|
|
605
|
+
);
|
|
606
|
+
} catch (error: unknown) {
|
|
607
|
+
this.tracker.releaseClaim(id);
|
|
608
|
+
throw error;
|
|
609
|
+
}
|
|
235
610
|
}
|
|
236
611
|
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
fromSeq: number,
|
|
240
|
-
signal?: AbortSignal,
|
|
241
|
-
): Promise<StoredSuffix | undefined> {
|
|
242
|
-
const log = await this.readLog(id, { fromSeq }, signal);
|
|
243
|
-
if (log === undefined) return undefined;
|
|
244
|
-
return {
|
|
245
|
-
meta: log.meta,
|
|
246
|
-
inheritedEventCount: SessionLogOffset(log.inheritedEventCount),
|
|
247
|
-
events: log.events,
|
|
248
|
-
};
|
|
612
|
+
flush(): Promise<void> {
|
|
613
|
+
return this.tracker.flushAll();
|
|
249
614
|
}
|
|
250
615
|
|
|
251
|
-
|
|
616
|
+
async stat(
|
|
252
617
|
id: SessionId,
|
|
253
|
-
|
|
254
|
-
): Promise<
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
618
|
+
options?: SessionPersistenceStatOptions,
|
|
619
|
+
): Promise<SessionPersistenceSnapshot | undefined> {
|
|
620
|
+
options?.signal?.throwIfAborted();
|
|
621
|
+
await this.ready;
|
|
622
|
+
options?.signal?.throwIfAborted();
|
|
623
|
+
const pending = this.tracker.pendingOf(id);
|
|
624
|
+
if (pending !== undefined) {
|
|
625
|
+
return { header: pending.header, revision: pending.revision };
|
|
260
626
|
}
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
// replace 的 provenance 读取时重计算(sourceEventSeqs 不落库)。
|
|
264
|
-
recomputeReplaceProvenance(log.events);
|
|
265
|
-
// 孤儿 inbox splice(引用已被截断排队消息的插入/消费)会使上游 Inbox
|
|
266
|
-
// 增量重放失败、整个会话不可 resume——读取时改写为 no-op 修复。
|
|
267
|
-
repairOrphanInboxSplices(log.events);
|
|
627
|
+
const row = await this.backend.getSession(id);
|
|
628
|
+
if (row === undefined) return undefined;
|
|
268
629
|
return {
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
events: log.events,
|
|
272
|
-
// revision 表示必须与 readStoredRevision 的表示一致(见 listSnapshots)。
|
|
273
|
-
revision: SessionPersistenceRevision(
|
|
274
|
-
`${this.storeIdentity}:incarnation:${log.incarnation}:revision:${log.revision}`,
|
|
275
|
-
),
|
|
276
|
-
...(log.tornFrom !== undefined ? { tornMarker: log.tornFrom } : {}),
|
|
630
|
+
header: rowToMeta(row),
|
|
631
|
+
revision: this.rowRevision(row),
|
|
277
632
|
};
|
|
278
633
|
}
|
|
279
634
|
|
|
280
|
-
async
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
635
|
+
async list(
|
|
636
|
+
options?: SessionPersistenceListOptions,
|
|
637
|
+
): Promise<readonly SessionPersistenceSnapshot[]> {
|
|
638
|
+
const signal = options?.signal;
|
|
639
|
+
const snapshots: SessionPersistenceSnapshot[] = [];
|
|
640
|
+
const listed = new Set<SessionId>();
|
|
641
|
+
for (const [id, pending] of this.tracker.pendingEntries()) {
|
|
642
|
+
snapshots.push({ header: pending.header, revision: pending.revision });
|
|
643
|
+
listed.add(id);
|
|
644
|
+
}
|
|
284
645
|
signal?.throwIfAborted();
|
|
285
646
|
await this.ready;
|
|
286
647
|
signal?.throwIfAborted();
|
|
287
|
-
const
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
648
|
+
const rows = await this.backend.listSessions();
|
|
649
|
+
signal?.throwIfAborted();
|
|
650
|
+
for (const row of rows) {
|
|
651
|
+
if (listed.has(row.fSessionId as SessionId)) continue;
|
|
652
|
+
snapshots.push({ header: rowToMeta(row), revision: this.rowRevision(row) });
|
|
653
|
+
}
|
|
654
|
+
return snapshots;
|
|
292
655
|
}
|
|
293
656
|
|
|
294
|
-
|
|
657
|
+
// --- RDB 特有能力(rewind / fork / 导出 / 测试支撑) ---
|
|
658
|
+
|
|
659
|
+
/** 导出 artifact(jsonl v2 文本),视图只读不落库。 */
|
|
660
|
+
async readRaw(
|
|
295
661
|
id: SessionId,
|
|
296
|
-
options: { fromSeq?: number } = {},
|
|
297
662
|
signal?: AbortSignal,
|
|
298
663
|
): Promise<
|
|
299
|
-
| {
|
|
300
|
-
meta: SessionHeader;
|
|
301
|
-
|
|
302
|
-
inheritedEventCount: number;
|
|
303
|
-
events: SessionEvent[];
|
|
304
|
-
tornFrom?: number;
|
|
305
|
-
|
|
306
|
-
incarnation: string;
|
|
307
|
-
|
|
308
|
-
revision: number;
|
|
309
|
-
}
|
|
664
|
+
| { meta: SessionHeader; inheritedEventCount: number; filename: string; content: string }
|
|
310
665
|
| undefined
|
|
311
666
|
> {
|
|
312
667
|
signal?.throwIfAborted();
|
|
313
668
|
await this.ready;
|
|
314
669
|
signal?.throwIfAborted();
|
|
315
|
-
const
|
|
316
|
-
if (
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
if (options.fromSeq === undefined) {
|
|
321
|
-
// 全量读:seq map 由同一批行构建(无额外查询)。
|
|
322
|
-
eventRows = await this.backend.getEventRows(id);
|
|
323
|
-
seqMap = buildSeqMap(eventRows);
|
|
324
|
-
} else {
|
|
325
|
-
// 后缀读:坐标重映射需要每一行的上游 seq,另读轻量两列映射。
|
|
326
|
-
eventRows = await this.backend.getEventRows(id, options.fromSeq);
|
|
327
|
-
const seqRows = await this.backend.getSeqMapRows(id);
|
|
328
|
-
seqMap = buildSeqMap(seqRows);
|
|
329
|
-
}
|
|
330
|
-
signal?.throwIfAborted();
|
|
331
|
-
const { preserved, tornFrom } = scanRows(eventRows, options.fromSeq ?? 0, seqMap);
|
|
670
|
+
const log = await this.readLog(id, {}, signal);
|
|
671
|
+
if (log === undefined) return undefined;
|
|
672
|
+
repairSurfaceOps(log.events);
|
|
673
|
+
recomputeReplaceProvenance(log.events);
|
|
674
|
+
const inheritedEventCount = Math.min(log.inheritedEventCount, log.events.length);
|
|
332
675
|
return {
|
|
333
|
-
meta,
|
|
334
|
-
inheritedEventCount
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
revision: row.fRevision,
|
|
338
|
-
...(tornFrom !== undefined ? { tornFrom } : {}),
|
|
676
|
+
meta: log.meta,
|
|
677
|
+
inheritedEventCount,
|
|
678
|
+
filename: "session.jsonl",
|
|
679
|
+
content: toJsonlArtifact(log.meta, inheritedEventCount, log.events),
|
|
339
680
|
};
|
|
340
681
|
}
|
|
341
682
|
|
|
683
|
+
/** 空会话 materialize(flush 的持久化屏障)。 */
|
|
684
|
+
async materializeEmpty(
|
|
685
|
+
meta: SessionHeader,
|
|
686
|
+
inheritedEventCount: SessionLogOffset,
|
|
687
|
+
): Promise<void> {
|
|
688
|
+
await this.ready;
|
|
689
|
+
await this.backend.transaction(async (tx) => {
|
|
690
|
+
await tx.upsertSession({ meta, inheritedEventCount }, randomUUID());
|
|
691
|
+
await tx.bumpRevision(meta.id);
|
|
692
|
+
});
|
|
693
|
+
this.tracker.materialized(meta.id);
|
|
694
|
+
this.writeGuard.confirmHead(meta.id, -1);
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
/** 原样 append 落库(handle 已校验 contiguity;torn tail 先截断)。
|
|
698
|
+
* 与上游 JSONL 一致:ignorable 事件原样存储,不做过滤。写路径校验 v2
|
|
699
|
+
* 形状(fail-closed):未知类型(非 ignorable)与非法消息形状拒绝入库;
|
|
700
|
+
* 旧格式(v0/v1)数据只在读取时经 legacy 转换链动态转换,不落新库。 */
|
|
342
701
|
async appendBatch(
|
|
343
|
-
|
|
702
|
+
meta: SessionHeader,
|
|
703
|
+
inheritedEventCount: SessionLogOffset,
|
|
344
704
|
events: readonly SessionEvent[],
|
|
345
|
-
|
|
346
|
-
): Promise<
|
|
705
|
+
tornTruncateTo?: number,
|
|
706
|
+
): Promise<boolean> {
|
|
347
707
|
await this.ready;
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
708
|
+
if (events.length === 0) return false;
|
|
709
|
+
// 写路径 v2 校验:与读路径同契约(validateStoredEvents),保证新入库
|
|
710
|
+
// 数据只能是 v2 形状。拷贝避免 adopt 替换污染调用方数组。
|
|
711
|
+
validateStoredEvents(meta, [...events]);
|
|
351
712
|
// fork 派生会话的 seed 复用源会话事件行(不复制);消费后清除。
|
|
352
713
|
const reuse = this.reuseEventIds.get(meta.id);
|
|
353
714
|
if (reuse !== undefined) this.reuseEventIds.delete(meta.id);
|
|
354
715
|
let confirmedHead = -1;
|
|
355
716
|
await this.backend.transaction(async (tx) => {
|
|
356
|
-
|
|
717
|
+
if (tornTruncateTo !== undefined) {
|
|
718
|
+
await tx.deleteBridgeTail(meta.id, tornTruncateTo);
|
|
719
|
+
const prev = await tx.getPrevBridge(meta.id, tornTruncateTo - 1);
|
|
720
|
+
if (prev === undefined) {
|
|
721
|
+
await tx.updateHead(meta.id, "", -1);
|
|
722
|
+
} else {
|
|
723
|
+
await tx.updateHead(meta.id, prev.fEventId, prev.fSequence);
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
await tx.upsertSession({ meta, inheritedEventCount }, randomUUID());
|
|
357
727
|
const head = await tx.getHead(meta.id);
|
|
358
728
|
// 重编号前拒绝第二个写入者:多实例共享数据库时,第二个写入者经陈旧
|
|
359
729
|
// 视图 append 会把事件静默重编号到对方尾部、损坏 log。磁盘 head 必须
|
|
@@ -362,7 +732,7 @@ export class SessionPersistenceRdb
|
|
|
362
732
|
const { headEventId, headSequence } = await appendEventTail(
|
|
363
733
|
tx,
|
|
364
734
|
meta,
|
|
365
|
-
|
|
735
|
+
events,
|
|
366
736
|
{ parentId: head.fHeadEventId, nextSeq: head.fHeadSequence + 1 },
|
|
367
737
|
reuse,
|
|
368
738
|
);
|
|
@@ -372,52 +742,61 @@ export class SessionPersistenceRdb
|
|
|
372
742
|
});
|
|
373
743
|
// 提交后才确认新 head:回滚不得留下本实例实际未写的已确认 head。
|
|
374
744
|
this.writeGuard.confirmHead(meta.id, confirmedHead);
|
|
745
|
+
this.tracker.materialized(meta.id);
|
|
746
|
+
return true;
|
|
375
747
|
}
|
|
376
748
|
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
if (prev === undefined) {
|
|
392
|
-
await tx.updateHead(meta.id, "", -1);
|
|
393
|
-
} else {
|
|
394
|
-
await tx.updateHead(meta.id, prev.fEventId, prev.fSequence);
|
|
395
|
-
}
|
|
396
|
-
}
|
|
397
|
-
if (persistedClosers.length > 0) {
|
|
398
|
-
// 锚定实际尾行:head 游标可能滞后于行(手工 torn tail 不更新游标)。
|
|
399
|
-
const last = await tx.getLastBridge(meta.id);
|
|
400
|
-
const { headEventId, headSequence } = await appendEventTail(tx, meta, persistedClosers, {
|
|
401
|
-
parentId: last?.fEventId ?? "",
|
|
402
|
-
nextSeq: (last?.fSequence ?? -1) + 1,
|
|
403
|
-
});
|
|
404
|
-
await tx.updateHead(meta.id, headEventId, headSequence);
|
|
405
|
-
}
|
|
406
|
-
await tx.bumpRevision(meta.id);
|
|
407
|
-
});
|
|
408
|
-
// 修复后重确认 head:截断会回退它、closers 会推进它,下一次 append 不得
|
|
409
|
-
// 基于陈旧确认被拒绝(或更糟,被静默重编号)。
|
|
410
|
-
const row = await this.backend.getSession(meta.id);
|
|
411
|
-
this.writeGuard.confirmHead(meta.id, row?.fHeadSequence ?? -1);
|
|
412
|
-
}
|
|
749
|
+
/** 读取一个会话的稠密 log(含 torn tail 检测,不含修复改写)。 */
|
|
750
|
+
async readLog(
|
|
751
|
+
id: SessionId,
|
|
752
|
+
options: { fromSeq?: number } = {},
|
|
753
|
+
signal?: AbortSignal,
|
|
754
|
+
): Promise<
|
|
755
|
+
| {
|
|
756
|
+
meta: SessionHeader;
|
|
757
|
+
|
|
758
|
+
inheritedEventCount: number;
|
|
759
|
+
events: SessionEvent[];
|
|
760
|
+
tornFrom?: number;
|
|
761
|
+
|
|
762
|
+
incarnation: string;
|
|
413
763
|
|
|
414
|
-
|
|
764
|
+
revision: number;
|
|
765
|
+
}
|
|
766
|
+
| undefined
|
|
767
|
+
> {
|
|
415
768
|
signal?.throwIfAborted();
|
|
416
769
|
await this.ready;
|
|
417
770
|
signal?.throwIfAborted();
|
|
418
|
-
const
|
|
771
|
+
const row = await this.backend.getSession(id);
|
|
772
|
+
if (row === undefined) return undefined;
|
|
773
|
+
const meta = rowToMeta(row);
|
|
774
|
+
const eventRows =
|
|
775
|
+
options.fromSeq === undefined
|
|
776
|
+
? await this.backend.getEventRows(id)
|
|
777
|
+
: await this.backend.getEventRows(id, options.fromSeq);
|
|
419
778
|
signal?.throwIfAborted();
|
|
420
|
-
|
|
779
|
+
// 旧格式(v0/v1)历史数据:行重建为物理记录,经上游迁移链转 v2 逻辑事件。
|
|
780
|
+
// 迁移链自带 seq gap / torn tail 校验(strict recovery),无需 scanRows。
|
|
781
|
+
if (isLegacyVersion(row.fVersion)) {
|
|
782
|
+
const converted = convertLegacyRows(row, eventRows);
|
|
783
|
+
return {
|
|
784
|
+
meta: converted.meta,
|
|
785
|
+
inheritedEventCount: converted.inheritedEventCount,
|
|
786
|
+
events: converted.events,
|
|
787
|
+
incarnation: row.fIncarnation,
|
|
788
|
+
revision: row.fRevision,
|
|
789
|
+
};
|
|
790
|
+
}
|
|
791
|
+
const { preserved, tornFrom } = scanRows(eventRows, options.fromSeq ?? 0);
|
|
792
|
+
return {
|
|
793
|
+
meta,
|
|
794
|
+
inheritedEventCount: row.fSeedLength ?? 0,
|
|
795
|
+
events: preserved,
|
|
796
|
+
incarnation: row.fIncarnation,
|
|
797
|
+
revision: row.fRevision,
|
|
798
|
+
...(tornFrom !== undefined ? { tornFrom } : {}),
|
|
799
|
+
};
|
|
421
800
|
}
|
|
422
801
|
|
|
423
802
|
async listSnapshots(
|
|
@@ -426,15 +805,120 @@ export class SessionPersistenceRdb
|
|
|
426
805
|
signal?.throwIfAborted();
|
|
427
806
|
await this.ready;
|
|
428
807
|
signal?.throwIfAborted();
|
|
808
|
+
const snapshots: Array<SessionPersistenceSnapshot & { inheritedEventCount: number }> = [];
|
|
809
|
+
const listed = new Set<SessionId>();
|
|
810
|
+
for (const [id, pending] of this.tracker.pendingEntries()) {
|
|
811
|
+
snapshots.push({
|
|
812
|
+
header: pending.header,
|
|
813
|
+
revision: pending.revision,
|
|
814
|
+
inheritedEventCount: pending.inheritedEventCount,
|
|
815
|
+
});
|
|
816
|
+
listed.add(id);
|
|
817
|
+
}
|
|
429
818
|
const rows = await this.backend.listSessions();
|
|
430
819
|
signal?.throwIfAborted();
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
820
|
+
for (const row of rows) {
|
|
821
|
+
if (listed.has(row.fSessionId as SessionId)) continue;
|
|
822
|
+
snapshots.push({
|
|
823
|
+
header: rowToMeta(row),
|
|
824
|
+
revision: this.rowRevision(row),
|
|
825
|
+
inheritedEventCount: row.fSeedLength ?? 0,
|
|
826
|
+
});
|
|
827
|
+
}
|
|
828
|
+
return snapshots;
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
async readStoredRevision(
|
|
832
|
+
id: SessionId,
|
|
833
|
+
signal?: AbortSignal,
|
|
834
|
+
): Promise<SessionPersistenceRevision | undefined> {
|
|
835
|
+
signal?.throwIfAborted();
|
|
836
|
+
await this.ready;
|
|
837
|
+
signal?.throwIfAborted();
|
|
838
|
+
const row = await this.backend.getSession(id);
|
|
839
|
+
if (row === undefined) return undefined;
|
|
840
|
+
return this.rowRevision(row);
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
/** 便捷:create + append + close(测试与导入路径共用)。 */
|
|
844
|
+
async createAndAppend(
|
|
845
|
+
header: SessionHeader,
|
|
846
|
+
events: readonly SessionEvent[],
|
|
847
|
+
inheritedEventCount?: number,
|
|
848
|
+
): Promise<void> {
|
|
849
|
+
const handle = await this.create(
|
|
850
|
+
header,
|
|
851
|
+
inheritedEventCount === undefined
|
|
852
|
+
? undefined
|
|
853
|
+
: { inheritedEventCount: SessionLogOffset(inheritedEventCount) },
|
|
854
|
+
);
|
|
855
|
+
try {
|
|
856
|
+
if (events.length > 0) await handle.append(events);
|
|
857
|
+
} finally {
|
|
858
|
+
await handle.close();
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
/** 便捷:open(read) + read 全量 + close。 */
|
|
863
|
+
async load(
|
|
864
|
+
id: SessionId,
|
|
865
|
+
signal?: AbortSignal,
|
|
866
|
+
): Promise<import("@deepseek-ai/dsh-session-persistence").SessionInspection> {
|
|
867
|
+
const handle = await this.open(id, "read", signal === undefined ? undefined : { signal });
|
|
868
|
+
try {
|
|
869
|
+
const { events } = await handle.read(
|
|
870
|
+
0,
|
|
871
|
+
undefined,
|
|
872
|
+
signal === undefined ? undefined : { signal },
|
|
873
|
+
);
|
|
874
|
+
const row = await this.backend.getSession(id);
|
|
875
|
+
if (row === undefined) throw new SessionPersistenceNotFoundError(id);
|
|
876
|
+
// 旧格式(v0/v1)会话:meta 与继承前缀来自转换链(handle.header 已是
|
|
877
|
+
// 转换后的 v2 header);v2 会话用存储行。
|
|
878
|
+
if (isLegacyVersion(row.fVersion)) {
|
|
879
|
+
return {
|
|
880
|
+
meta: handle.header,
|
|
881
|
+
inheritedEventCount: handle.inheritedEventCount,
|
|
882
|
+
events,
|
|
883
|
+
};
|
|
884
|
+
}
|
|
885
|
+
return {
|
|
886
|
+
meta: rowToMeta(row),
|
|
887
|
+
inheritedEventCount: SessionLogOffset(row.fSeedLength ?? 0),
|
|
888
|
+
events,
|
|
889
|
+
};
|
|
890
|
+
} finally {
|
|
891
|
+
await handle.close();
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
/** 便捷:open(write) + append + close。 */
|
|
896
|
+
async append(id: SessionId, events: readonly SessionEvent[]): Promise<void> {
|
|
897
|
+
const handle = await this.open(id, "write");
|
|
898
|
+
try {
|
|
899
|
+
await handle.append(events);
|
|
900
|
+
} finally {
|
|
901
|
+
await handle.close();
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
/** 便捷:readFrom(稠密后缀)。 */
|
|
906
|
+
async readFrom(
|
|
907
|
+
id: SessionId,
|
|
908
|
+
fromSeq: number,
|
|
909
|
+
signal?: AbortSignal,
|
|
910
|
+
): Promise<{
|
|
911
|
+
meta: SessionHeader;
|
|
912
|
+
inheritedEventCount: number;
|
|
913
|
+
events: readonly SessionEvent[];
|
|
914
|
+
}> {
|
|
915
|
+
const log = await this.readLog(id, { fromSeq }, signal);
|
|
916
|
+
if (log === undefined) throw new SessionPersistenceNotFoundError(id);
|
|
917
|
+
return {
|
|
918
|
+
meta: log.meta,
|
|
919
|
+
inheritedEventCount: log.inheritedEventCount,
|
|
920
|
+
events: log.events,
|
|
921
|
+
};
|
|
438
922
|
}
|
|
439
923
|
|
|
440
924
|
async close(): Promise<void> {
|
|
@@ -450,16 +934,170 @@ export class SessionPersistenceRdb
|
|
|
450
934
|
return {
|
|
451
935
|
backend: this.backend,
|
|
452
936
|
writeGuard: this.writeGuard,
|
|
453
|
-
create: (meta, inheritedEventCount) =>
|
|
937
|
+
create: (meta, inheritedEventCount) =>
|
|
938
|
+
this.create(
|
|
939
|
+
meta,
|
|
940
|
+
inheritedEventCount === undefined
|
|
941
|
+
? undefined
|
|
942
|
+
: { inheritedEventCount: SessionLogOffset(inheritedEventCount) },
|
|
943
|
+
),
|
|
454
944
|
append: (id, events) => this.append(id, events),
|
|
455
945
|
load: (id) => this.load(id),
|
|
456
|
-
inspect: (id, signal) => this.
|
|
946
|
+
inspect: (id, signal) => this.load(id, signal),
|
|
457
947
|
readFrom: (id, fromSeq, signal) => this.readFrom(id, fromSeq, signal),
|
|
458
948
|
listSnapshots: (signal) => this.listSnapshots(signal),
|
|
459
949
|
readStoredRevision: (id, signal) => this.readStoredRevision(id, signal),
|
|
460
950
|
registerReuseEventIds: (childId, map) => this.registerReuseEventIds(childId, map),
|
|
461
951
|
};
|
|
462
952
|
}
|
|
953
|
+
|
|
954
|
+
private rowRevision(row: import("./backend.ts").SessionRow): SessionPersistenceRevision {
|
|
955
|
+
return SessionPersistenceRevision(
|
|
956
|
+
`${this.storeIdentity}:incarnation:${row.fIncarnation}:revision:${row.fRevision}`,
|
|
957
|
+
);
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
// --- live 路由:session/created → create/adopt handle;event → 缓冲;flush → drain ---
|
|
961
|
+
|
|
962
|
+
private installLiveRouting(ctx: Context): void {
|
|
963
|
+
ctx.on("session/created", (session: Session) => {
|
|
964
|
+
this.liveBuffers.set(session.id, []);
|
|
965
|
+
const ready = this.ensureLiveHandle(session);
|
|
966
|
+
this.liveReady.set(session.id, ready);
|
|
967
|
+
void ready.catch((error: unknown) => {
|
|
968
|
+
ctx.logger.warn(
|
|
969
|
+
`session-rdb: live session "${session.id}" persistence init failed: ${String(error)}`,
|
|
970
|
+
);
|
|
971
|
+
});
|
|
972
|
+
});
|
|
973
|
+
// HMR:插件 apply 时已存在的 live 会话不重放 session/created——补种。
|
|
974
|
+
for (const session of ctx.sessions.list()) {
|
|
975
|
+
this.liveBuffers.set(session.id, []);
|
|
976
|
+
const ready = this.ensureLiveHandle(session);
|
|
977
|
+
this.liveReady.set(session.id, ready);
|
|
978
|
+
void ready.catch((error: unknown) => {
|
|
979
|
+
ctx.logger.warn(
|
|
980
|
+
`session-rdb: live session "${session.id}" persistence init failed: ${String(error)}`,
|
|
981
|
+
);
|
|
982
|
+
});
|
|
983
|
+
}
|
|
984
|
+
ctx.on("session/event", (session: Session, event: SessionEvent) => {
|
|
985
|
+
const handle = this.tracker.writerOf(session.id);
|
|
986
|
+
if (handle !== undefined) {
|
|
987
|
+
handle.enqueueLive(event, (error) => {
|
|
988
|
+
ctx.logger.warn(
|
|
989
|
+
`session-rdb: background write for session "${session.id}" failed (buffered events retained): ${String(error)}`,
|
|
990
|
+
);
|
|
991
|
+
});
|
|
992
|
+
return;
|
|
993
|
+
}
|
|
994
|
+
this.liveBuffers.get(session.id)?.push(structuredClone(event));
|
|
995
|
+
});
|
|
996
|
+
ctx.on("session/flush", (session: Session) => {
|
|
997
|
+
const handle = this.tracker.writerOf(session.id);
|
|
998
|
+
if (handle === undefined) {
|
|
999
|
+
const ready = this.liveReady.get(session.id);
|
|
1000
|
+
if (ready === undefined) return undefined;
|
|
1001
|
+
return ready.then(() => {
|
|
1002
|
+
const settled = this.tracker.writerOf(session.id);
|
|
1003
|
+
if (settled === undefined) return undefined;
|
|
1004
|
+
return settled.drainLive().then(() => settled.flush());
|
|
1005
|
+
});
|
|
1006
|
+
}
|
|
1007
|
+
return handle.drainLive().then(() => handle.flush());
|
|
1008
|
+
});
|
|
1009
|
+
ctx.on("session/disposed", (session: Session) => {
|
|
1010
|
+
const ready = this.liveReady.get(session.id);
|
|
1011
|
+
this.liveBuffers.delete(session.id);
|
|
1012
|
+
this.liveReady.delete(session.id);
|
|
1013
|
+
const closeHandle = (): void => {
|
|
1014
|
+
const handle = this.tracker.writerOf(session.id);
|
|
1015
|
+
if (handle === undefined) return;
|
|
1016
|
+
handle.close().catch((error: unknown) => {
|
|
1017
|
+
ctx.logger.warn(
|
|
1018
|
+
`session-rdb: final drain for session "${session.id}" failed: ${String(error)}`,
|
|
1019
|
+
);
|
|
1020
|
+
});
|
|
1021
|
+
};
|
|
1022
|
+
if (ready === undefined) {
|
|
1023
|
+
closeHandle();
|
|
1024
|
+
return;
|
|
1025
|
+
}
|
|
1026
|
+
// handle 可能仍在构造(ensureLiveHandle 异步):等就绪后再 close。
|
|
1027
|
+
void ready.then(closeHandle, closeHandle);
|
|
1028
|
+
});
|
|
1029
|
+
ctx.effect(
|
|
1030
|
+
() => async () => {
|
|
1031
|
+
// 先等所有 live handle 就绪(ensureLiveHandle 异步),再统一关闭。
|
|
1032
|
+
await Promise.allSettled(this.liveReady.values());
|
|
1033
|
+
await this.tracker.closeAll();
|
|
1034
|
+
// 等 init(异步 open)settle 后关闭后端连接:dispose 返回后调用方
|
|
1035
|
+
// 可能立即释放存储(pg 测试 drop 数据库),未完成的 open 会以
|
|
1036
|
+
// 无人处理的 rejection 泄漏。
|
|
1037
|
+
await this.close();
|
|
1038
|
+
},
|
|
1039
|
+
`${this.name} open handles`,
|
|
1040
|
+
);
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
/** session/created 后为 live 会话建立 write handle(create 或 adopt)。 */
|
|
1044
|
+
private async ensureLiveHandle(session: Session): Promise<void> {
|
|
1045
|
+
const id = session.header.id;
|
|
1046
|
+
if (this.tracker.writerOf(id) !== undefined) return;
|
|
1047
|
+
await this.ready;
|
|
1048
|
+
const stored = await this.readLog(id, {});
|
|
1049
|
+
let handle: RdbSessionHandle;
|
|
1050
|
+
if (stored === undefined) {
|
|
1051
|
+
// 新会话:注册 pending 并返回 write handle;构造 seed 事件不发布
|
|
1052
|
+
// session/event,须在此一次性落库(与旧版 onCreated 同语义)。
|
|
1053
|
+
handle = (await this.create(session.header, {
|
|
1054
|
+
inheritedEventCount: session.inheritedEventCount,
|
|
1055
|
+
})) as RdbSessionHandle;
|
|
1056
|
+
const seed = session.snapshotEvents();
|
|
1057
|
+
if (seed.length > 0) await handle.append(seed);
|
|
1058
|
+
} else {
|
|
1059
|
+
// adopt:校验 cwd / inheritedEventCount / seed 前缀匹配后接管写所有权。
|
|
1060
|
+
if (stored.meta.cwd !== session.header.cwd) {
|
|
1061
|
+
throw new Error(
|
|
1062
|
+
`session "${id}" is already persisted at a different cwd (persisted: ${String(stored.meta.cwd)}, live: ${String(session.header.cwd)}) (id collision)`,
|
|
1063
|
+
);
|
|
1064
|
+
}
|
|
1065
|
+
if (stored.inheritedEventCount !== session.inheritedEventCount) {
|
|
1066
|
+
throw new Error(
|
|
1067
|
+
`session "${id}" is already persisted with a different inherited event count (id collision)`,
|
|
1068
|
+
);
|
|
1069
|
+
}
|
|
1070
|
+
assertVersion(stored.meta);
|
|
1071
|
+
const seed = session.snapshotEvents();
|
|
1072
|
+
if (!seedCoversPrefix(seed, stored.events)) {
|
|
1073
|
+
throw new Error(
|
|
1074
|
+
`session "${id}" already has a persisted log on disk that does not match this live session (id collision)`,
|
|
1075
|
+
);
|
|
1076
|
+
}
|
|
1077
|
+
handle = (await this.open(id, "write")) as RdbSessionHandle;
|
|
1078
|
+
// 持久化 seed 后缀(构造 seed 事件不发布 session/event,缓冲看不到)。
|
|
1079
|
+
const suffix = seed.slice(stored.events.length);
|
|
1080
|
+
if (suffix.length > 0) await handle.append(suffix);
|
|
1081
|
+
}
|
|
1082
|
+
// 把 handle 就绪前缓冲的事件移交。
|
|
1083
|
+
const buffered = this.liveBuffers.get(id);
|
|
1084
|
+
if (buffered !== undefined && buffered.length > 0) {
|
|
1085
|
+
this.liveBuffers.set(id, []);
|
|
1086
|
+
for (const event of buffered) {
|
|
1087
|
+
handle.enqueueLive(event, () => {});
|
|
1088
|
+
}
|
|
1089
|
+
}
|
|
1090
|
+
}
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
function seedCoversPrefix(seed: readonly SessionEvent[], prefix: readonly SessionEvent[]): boolean {
|
|
1094
|
+
return (
|
|
1095
|
+
prefix.length <= seed.length &&
|
|
1096
|
+
prefix.every((event, index) => {
|
|
1097
|
+
const seedEvent = seed[index];
|
|
1098
|
+
return seedEvent !== undefined && JSON.stringify(seedEvent) === JSON.stringify(event);
|
|
1099
|
+
})
|
|
1100
|
+
);
|
|
463
1101
|
}
|
|
464
1102
|
|
|
465
1103
|
function createBackend(config: Config): Backend {
|
|
@@ -504,7 +1142,6 @@ async function appendEventTail(
|
|
|
504
1142
|
fSessionId: SessionId;
|
|
505
1143
|
fEventId: string;
|
|
506
1144
|
fSequence: number;
|
|
507
|
-
fOriginalSeq: number;
|
|
508
1145
|
fSurfaceOp: string | null;
|
|
509
1146
|
}> = [];
|
|
510
1147
|
for (const event of events) {
|
|
@@ -512,6 +1149,15 @@ async function appendEventTail(
|
|
|
512
1149
|
const eventId = reusedId ?? randomUUID();
|
|
513
1150
|
if (reusedId === undefined) {
|
|
514
1151
|
const { kind, role, name, actionId } = eventDimensions(event);
|
|
1152
|
+
// fData 存完整事件(含 ignorable 信封,与 JSONL 每行同构):data 部分
|
|
1153
|
+
// 与信封字段在同一 JSON 记录里,读回时整体解析。surfaceOp 走桥接行
|
|
1154
|
+
// 列(f_surface_op),sourceEventSeqs 不落库(读取时重计算)。
|
|
1155
|
+
const raw = event as SessionEvent & {
|
|
1156
|
+
ignorable?: unknown;
|
|
1157
|
+
surfaceOp?: unknown;
|
|
1158
|
+
sourceEventSeqs?: unknown;
|
|
1159
|
+
};
|
|
1160
|
+
const { data, surfaceOp: _surfaceOp, sourceEventSeqs: _sourceEventSeqs, ...envelope } = raw;
|
|
515
1161
|
eventRows.push({
|
|
516
1162
|
fEventId: eventId,
|
|
517
1163
|
fParentId: parentId,
|
|
@@ -521,7 +1167,7 @@ async function appendEventTail(
|
|
|
521
1167
|
fName: name,
|
|
522
1168
|
fActionId: actionId,
|
|
523
1169
|
fEncoding: EVENT_ENCODING,
|
|
524
|
-
fData: JSON.stringify(
|
|
1170
|
+
fData: JSON.stringify({ ...envelope, data }),
|
|
525
1171
|
fCreatedAt: event.time,
|
|
526
1172
|
});
|
|
527
1173
|
}
|
|
@@ -533,7 +1179,6 @@ async function appendEventTail(
|
|
|
533
1179
|
fSessionId: meta.id,
|
|
534
1180
|
fEventId: eventId,
|
|
535
1181
|
fSequence: nextSeq,
|
|
536
|
-
fOriginalSeq: event.seq,
|
|
537
1182
|
fSurfaceOp: surfaceOp,
|
|
538
1183
|
});
|
|
539
1184
|
parentId = eventId;
|