@morlay/session-rdb 0.0.19 → 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 +3 -3
- package/dist/{schema-DPcuEh_a.d.mts → backend-DpdtxYpz.d.mts} +360 -107
- package/dist/branch-5JzX9rUq.mjs +390 -0
- 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 +214 -1
- package/dist/index.d.mts +2 -3
- package/dist/index.mjs +4 -1630
- 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 +2 -26
- package/dist/testing.mjs +10504 -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 +29 -30
- 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 +2 -2
- package/src/usage.ts +191 -0
- package/dist/import-Bc2QQa5G.mjs +0 -473
- package/dist/index-CCcK9tia.d.mts +0 -270
- package/dist/magic-string.es-BgJoa-3K.mjs +0 -1017
package/src/index.ts
CHANGED
|
@@ -51,12 +51,17 @@ import {
|
|
|
51
51
|
import { SqliteBackend } from "./sqlite.ts";
|
|
52
52
|
import { PostgresBackend } from "./postgres.ts";
|
|
53
53
|
import { SessionBranchRdb } from "./branch.ts";
|
|
54
|
+
import { balanceRewindPrefix } from "@morlay/session-branch";
|
|
54
55
|
import { registerSessionImport } from "./import.ts";
|
|
56
|
+
import { registerSessionDeletion } from "./deletion.ts";
|
|
57
|
+
import { registerSessionExport } from "./export.ts";
|
|
58
|
+
import { registerSessionGc } from "./gc.ts";
|
|
59
|
+
import { registerSessionUsage } from "./usage.ts";
|
|
60
|
+
import type { UsageAggregate } from "./usage.ts";
|
|
55
61
|
import { SessionQueryRdb } from "./session-query.ts";
|
|
56
62
|
import { adoptLegacyRows, convertLegacyRows, isLegacyVersion } from "./legacy.ts";
|
|
57
63
|
import { installStorageTakeover } from "./storage-takeover/index.ts";
|
|
58
64
|
|
|
59
|
-
/** 投影缓存默认写节流(与上游 base 装配的部署值一致)。 */
|
|
60
65
|
const DEFAULT_PROJECTION_WRITE_EVERY_EVENTS = 200;
|
|
61
66
|
const DEFAULT_PROJECTION_WRITE_INTERVAL_MS = 5000;
|
|
62
67
|
|
|
@@ -87,11 +92,9 @@ export interface SessionPersistenceRdbInternals {
|
|
|
87
92
|
registerReuseEventIds(childId: SessionId, map: ReadonlyMap<number, string>): void;
|
|
88
93
|
}
|
|
89
94
|
|
|
90
|
-
/** 投影 checkpoint 写节流(替换上游 `session-projection-cache` 的 Config)。 */
|
|
91
95
|
export interface ProjectionCacheOptions {
|
|
92
|
-
/** 两次强制点之间累积多少已提交事件强制落盘(默认 200)。 */
|
|
93
96
|
writeEveryEvents?: number;
|
|
94
|
-
|
|
97
|
+
|
|
95
98
|
writeIntervalMs?: number;
|
|
96
99
|
}
|
|
97
100
|
|
|
@@ -117,20 +120,32 @@ export type Config =
|
|
|
117
120
|
projectionCache?: ProjectionCacheOptions;
|
|
118
121
|
};
|
|
119
122
|
|
|
120
|
-
|
|
123
|
+
export type SessionDeletionErrorCode =
|
|
124
|
+
| "SESSION_NOT_FOUND"
|
|
125
|
+
| "SESSION_NOT_ARCHIVED"
|
|
126
|
+
| "SESSION_LIVE";
|
|
127
|
+
|
|
128
|
+
export class SessionDeletionError extends Error {
|
|
129
|
+
constructor(
|
|
130
|
+
message: string,
|
|
131
|
+
readonly code: SessionDeletionErrorCode,
|
|
132
|
+
) {
|
|
133
|
+
super(message);
|
|
134
|
+
this.name = "SessionDeletionError";
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
121
138
|
interface PendingSession {
|
|
122
139
|
readonly header: SessionHeader;
|
|
123
140
|
readonly revision: SessionPersistenceRevision;
|
|
124
141
|
readonly inheritedEventCount: SessionLogOffset;
|
|
125
|
-
|
|
142
|
+
|
|
126
143
|
readonly cursor: number;
|
|
127
|
-
|
|
144
|
+
|
|
128
145
|
readonly everAppended: boolean;
|
|
129
146
|
}
|
|
130
147
|
|
|
131
|
-
/** 会话级写所有权与 live 路由簿记。 */
|
|
132
148
|
class RdbBackendTracker {
|
|
133
|
-
/** 每个 id 的活跃 write handle;`null` 表示 claim 构造中。 */
|
|
134
149
|
private readonly writers = new Map<SessionId, RdbSessionHandle | null>();
|
|
135
150
|
private readonly pending = new Map<SessionId, PendingSession>();
|
|
136
151
|
private readonly openHandles = new Set<RdbSessionHandle>();
|
|
@@ -150,7 +165,6 @@ class RdbBackendTracker {
|
|
|
150
165
|
});
|
|
151
166
|
}
|
|
152
167
|
|
|
153
|
-
/** 更新 pending 的 cursor / everAppended(handle append 后同步)。 */
|
|
154
168
|
updatePending(id: SessionId, cursor: number, everAppended: boolean): void {
|
|
155
169
|
const entry = this.pending.get(id);
|
|
156
170
|
if (entry === undefined) return;
|
|
@@ -200,6 +214,13 @@ class RdbBackendTracker {
|
|
|
200
214
|
return writer === null ? undefined : writer;
|
|
201
215
|
}
|
|
202
216
|
|
|
217
|
+
hasOpenHandle(id: SessionId): boolean {
|
|
218
|
+
for (const handle of this.openHandles) {
|
|
219
|
+
if (handle.id === id) return true;
|
|
220
|
+
}
|
|
221
|
+
return false;
|
|
222
|
+
}
|
|
223
|
+
|
|
203
224
|
async flushAll(): Promise<void> {
|
|
204
225
|
const errors: unknown[] = [];
|
|
205
226
|
for (const writer of this.writers.values()) {
|
|
@@ -228,21 +249,20 @@ class RdbBackendTracker {
|
|
|
228
249
|
}
|
|
229
250
|
}
|
|
230
251
|
|
|
231
|
-
/** 一个打开会话的存储句柄:read / append / flush / close。 */
|
|
232
252
|
class RdbSessionHandle implements SessionHandle {
|
|
233
253
|
private chain: Promise<unknown> = Promise.resolve();
|
|
234
254
|
private closing: Promise<void> | undefined;
|
|
235
|
-
|
|
255
|
+
|
|
236
256
|
private cursor: number;
|
|
237
257
|
private materialized: boolean;
|
|
238
|
-
|
|
258
|
+
|
|
239
259
|
private buffered: SessionEvent[] = [];
|
|
240
260
|
private batchTimer: ReturnType<typeof setTimeout> | undefined;
|
|
241
261
|
private drainPaused = false;
|
|
242
262
|
private draining: Promise<void> | undefined;
|
|
243
|
-
|
|
263
|
+
|
|
244
264
|
private tornTruncateTo: number | undefined;
|
|
245
|
-
|
|
265
|
+
|
|
246
266
|
private everAppended = false;
|
|
247
267
|
|
|
248
268
|
constructor(
|
|
@@ -266,7 +286,6 @@ class RdbSessionHandle implements SessionHandle {
|
|
|
266
286
|
return this.state.inheritedEventCount;
|
|
267
287
|
}
|
|
268
288
|
|
|
269
|
-
/** 输入空间 cursor(下一个待落库的上游 seq)。 */
|
|
270
289
|
get cursorValue(): number {
|
|
271
290
|
return this.cursor;
|
|
272
291
|
}
|
|
@@ -291,9 +310,7 @@ class RdbSessionHandle implements SessionHandle {
|
|
|
291
310
|
}
|
|
292
311
|
throw new SessionPersistenceNotFoundError(this.id);
|
|
293
312
|
}
|
|
294
|
-
|
|
295
|
-
// 或按 metering 数量夹取、metering range 对齐、provenance 重算、孤儿
|
|
296
|
-
// inbox splice 改写。
|
|
313
|
+
|
|
297
314
|
repairReadView(log.events);
|
|
298
315
|
return { eventState: "detached", events: log.events.slice(offset, offset + length) };
|
|
299
316
|
}
|
|
@@ -317,7 +334,7 @@ class RdbSessionHandle implements SessionHandle {
|
|
|
317
334
|
this.tornTruncateTo,
|
|
318
335
|
);
|
|
319
336
|
this.tornTruncateTo = undefined;
|
|
320
|
-
|
|
337
|
+
|
|
321
338
|
this.cursor += batch.length;
|
|
322
339
|
this.materialized = true;
|
|
323
340
|
this.persistence.tracker.updatePending(this.id, this.cursor, true);
|
|
@@ -365,7 +382,6 @@ class RdbSessionHandle implements SessionHandle {
|
|
|
365
382
|
return this.close();
|
|
366
383
|
}
|
|
367
384
|
|
|
368
|
-
/** live 路由:缓冲一个已发布事件(持久化自有副本),并启动批量窗口。 */
|
|
369
385
|
enqueueLive(event: SessionEvent, reportBackgroundFailure: (error: unknown) => void): void {
|
|
370
386
|
this.buffered.push(structuredClone(event));
|
|
371
387
|
if (this.batchTimer !== undefined || this.drainPaused) return;
|
|
@@ -375,7 +391,6 @@ class RdbSessionHandle implements SessionHandle {
|
|
|
375
391
|
}, RdbSessionHandle.LIVE_WRITE_BATCH_MAX_DELAY_MS);
|
|
376
392
|
}
|
|
377
393
|
|
|
378
|
-
/** rewind 截断后对齐 handle 的稠密 cursor 与继承前缀(DB 已截断)。 */
|
|
379
394
|
resetAfterRewind(cursor: number, inheritedEventCount?: number): void {
|
|
380
395
|
this.cursor = cursor;
|
|
381
396
|
if (inheritedEventCount !== undefined) {
|
|
@@ -384,7 +399,6 @@ class RdbSessionHandle implements SessionHandle {
|
|
|
384
399
|
}
|
|
385
400
|
}
|
|
386
401
|
|
|
387
|
-
/** 排空 live 缓冲(过滤 delta + 重编号稠密 + append)。 */
|
|
388
402
|
drainLive(): Promise<void> {
|
|
389
403
|
return (this.draining ??= this.drainBuffered().finally(() => {
|
|
390
404
|
this.draining = undefined;
|
|
@@ -401,9 +415,6 @@ class RdbSessionHandle implements SessionHandle {
|
|
|
401
415
|
await this.enqueueChain(async () => {
|
|
402
416
|
const batch = this.buffered.splice(0);
|
|
403
417
|
try {
|
|
404
|
-
// 过滤已由 ensureLiveHandle 落库的 seed 前缀(上游 seq 空间,
|
|
405
|
-
// 与 public append 的 cursor 同空间)。直接走持久化原语(本函数
|
|
406
|
-
// 已在 chain 内,走 public append 会自锁)。
|
|
407
418
|
const fresh = batch.filter((event) => event.seq >= this.cursor);
|
|
408
419
|
if (fresh.length === 0) return;
|
|
409
420
|
for (const [index, event] of fresh.entries()) {
|
|
@@ -501,7 +512,6 @@ export class SessionPersistenceRdb extends SessionPersistence {
|
|
|
501
512
|
|
|
502
513
|
private readonly reuseEventIds = new Map<SessionId, Map<number, string>>();
|
|
503
514
|
|
|
504
|
-
/** live 路由:session/created 后 handle 就绪前的缓冲。 */
|
|
505
515
|
private readonly liveBuffers = new Map<SessionId, SessionEvent[]>();
|
|
506
516
|
private readonly liveReady = new Map<SessionId, Promise<void>>();
|
|
507
517
|
|
|
@@ -511,8 +521,6 @@ export class SessionPersistenceRdb extends SessionPersistence {
|
|
|
511
521
|
|
|
512
522
|
injectedBackend?: Backend,
|
|
513
523
|
) {
|
|
514
|
-
// settings.yaml 的 `session-rdb` namespace 覆盖 cordis 层 entry config;
|
|
515
|
-
// settings 服务缺失时(纯 cordis 装配/测试)退化为 entry config。
|
|
516
524
|
let resolved: Config = config;
|
|
517
525
|
const settings = ctx.reflect.get("settings") as unknown as SettingsProvider | undefined;
|
|
518
526
|
if (settings !== undefined) {
|
|
@@ -523,26 +531,30 @@ export class SessionPersistenceRdb extends SessionPersistence {
|
|
|
523
531
|
);
|
|
524
532
|
resolved = scope.get();
|
|
525
533
|
scope.watch(() => {
|
|
526
|
-
// 后端在构造时建成,settings 变更后需重启 dsh 生效。
|
|
527
534
|
ctx.logger.warn("session-rdb: settings changed; restart to apply the new configuration");
|
|
528
535
|
});
|
|
529
536
|
}
|
|
530
537
|
super(ctx);
|
|
531
|
-
|
|
538
|
+
|
|
532
539
|
this.config = resolved;
|
|
533
540
|
this.backend = injectedBackend ?? createBackend(resolved);
|
|
534
541
|
this.ready = this.init();
|
|
535
542
|
this.installLiveRouting(ctx);
|
|
536
|
-
|
|
543
|
+
|
|
537
544
|
new SessionBranchRdb(this.ctx);
|
|
538
|
-
|
|
539
|
-
// 其装配行由 better-session patch 禁用);class-plugin 装载以复用基类的
|
|
540
|
-
// 精确读 / 过滤 / 血缘实现。
|
|
545
|
+
|
|
541
546
|
this.ctx.plugin(SessionQueryRdb, {});
|
|
542
|
-
|
|
547
|
+
|
|
543
548
|
registerSessionImport(this.ctx, this);
|
|
544
|
-
|
|
545
|
-
|
|
549
|
+
|
|
550
|
+
registerSessionDeletion(this.ctx, this);
|
|
551
|
+
|
|
552
|
+
registerSessionExport(this.ctx, this);
|
|
553
|
+
|
|
554
|
+
registerSessionGc(this.ctx, this);
|
|
555
|
+
|
|
556
|
+
registerSessionUsage(this.ctx, this);
|
|
557
|
+
|
|
546
558
|
installStorageTakeover(this.ctx, {
|
|
547
559
|
repository: this.backend.storage,
|
|
548
560
|
ready: this.ready,
|
|
@@ -560,8 +572,6 @@ export class SessionPersistenceRdb extends SessionPersistence {
|
|
|
560
572
|
this.storeIdentity = this.backend.storeIdentity;
|
|
561
573
|
}
|
|
562
574
|
|
|
563
|
-
// --- SessionPersistence service surface ---
|
|
564
|
-
|
|
565
575
|
async create(
|
|
566
576
|
header: SessionHeader,
|
|
567
577
|
options?: SessionPersistenceCreateOptions,
|
|
@@ -614,11 +624,9 @@ export class SessionPersistenceRdb extends SessionPersistence {
|
|
|
614
624
|
}
|
|
615
625
|
const log = await this.readLog(id, {}, options?.signal);
|
|
616
626
|
if (log === undefined) throw new SessionPersistenceNotFoundError(id);
|
|
617
|
-
|
|
618
|
-
// 降级/夹取、request/header 归一,否则上游事件校验 fail loud 拒绝整个
|
|
619
|
-
// 会话(load / open 是历史会话的加载入口,不能只依赖 handle.read)。
|
|
627
|
+
|
|
620
628
|
repairReadView(log.events);
|
|
621
|
-
|
|
629
|
+
|
|
622
630
|
validateStoredEvents(log.meta, log.events);
|
|
623
631
|
return this.tracker.adopt(
|
|
624
632
|
new RdbSessionHandle(this, id, log.meta, "read", {
|
|
@@ -630,7 +638,6 @@ export class SessionPersistenceRdb extends SessionPersistence {
|
|
|
630
638
|
}
|
|
631
639
|
this.tracker.claimWrite(id);
|
|
632
640
|
try {
|
|
633
|
-
// pending(created 未 materialize)会话:write open 接管其所有权。
|
|
634
641
|
if (pending !== undefined) {
|
|
635
642
|
return this.tracker.adopt(
|
|
636
643
|
new RdbSessionHandle(this, id, pending.header, "write", {
|
|
@@ -644,13 +651,11 @@ export class SessionPersistenceRdb extends SessionPersistence {
|
|
|
644
651
|
if (log === undefined) throw new SessionPersistenceNotFoundError(id);
|
|
645
652
|
repairReadView(log.events);
|
|
646
653
|
validateStoredEvents(log.meta, log.events);
|
|
647
|
-
|
|
648
|
-
// 存储桥接行数不再相等——写打开时把迁移视图整体落库,使读写同坐标;
|
|
649
|
-
// 否则 append 按存储 head 重编号会撞上已有行或写坏 log。
|
|
654
|
+
|
|
650
655
|
if (log.migrated && log.events.length !== log.storedCount) {
|
|
651
656
|
await this.rewriteMigratedLog(id, log);
|
|
652
657
|
}
|
|
653
|
-
|
|
658
|
+
|
|
654
659
|
this.writeGuard.confirmHead(id, log.events.at(-1)?.seq ?? -1);
|
|
655
660
|
return this.tracker.adopt(
|
|
656
661
|
new RdbSessionHandle(this, id, log.meta, "write", {
|
|
@@ -711,9 +716,82 @@ export class SessionPersistenceRdb extends SessionPersistence {
|
|
|
711
716
|
return snapshots;
|
|
712
717
|
}
|
|
713
718
|
|
|
714
|
-
|
|
719
|
+
async deleteSession(id: SessionId, signal?: AbortSignal): Promise<void> {
|
|
720
|
+
signal?.throwIfAborted();
|
|
721
|
+
await this.ready;
|
|
722
|
+
signal?.throwIfAborted();
|
|
723
|
+
const row = await this.backend.getSession(id);
|
|
724
|
+
if (row === undefined) {
|
|
725
|
+
throw new SessionDeletionError(`session "${id}" not found`, "SESSION_NOT_FOUND");
|
|
726
|
+
}
|
|
727
|
+
if (row.fArchivedAt === null) {
|
|
728
|
+
throw new SessionDeletionError(
|
|
729
|
+
`session "${id}" is not archived; only archived sessions can be deleted`,
|
|
730
|
+
"SESSION_NOT_ARCHIVED",
|
|
731
|
+
);
|
|
732
|
+
}
|
|
733
|
+
if (this.tracker.hasPending(id) || this.tracker.hasOpenHandle(id)) {
|
|
734
|
+
throw new SessionDeletionError(
|
|
735
|
+
`session "${id}" is live; stop it before deleting`,
|
|
736
|
+
"SESSION_LIVE",
|
|
737
|
+
);
|
|
738
|
+
}
|
|
739
|
+
await this.unarchiveBeforeDeletion(id);
|
|
740
|
+
await this.backend.transaction(async (tx) => {
|
|
741
|
+
await tx.deleteSession(id);
|
|
742
|
+
});
|
|
743
|
+
this.liveBuffers.delete(id);
|
|
744
|
+
this.liveReady.delete(id);
|
|
745
|
+
this.reuseEventIds.delete(id);
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
/** GC 通道:回收已无桥接行引用的事件行(孤儿),返回删除行数。 */
|
|
749
|
+
async collectOrphans(): Promise<number> {
|
|
750
|
+
await this.ready;
|
|
751
|
+
return this.backend.collectOrphans();
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
/** GC 通道:回收父已不存在的 subagent 会话(live 的跳过),返回删除的会话数。 */
|
|
755
|
+
async collectOrphanSessions(): Promise<number> {
|
|
756
|
+
await this.ready;
|
|
757
|
+
const orphans = await this.backend.listOrphanSubagentSessions();
|
|
758
|
+
const deletable = orphans.filter(
|
|
759
|
+
(id) => !this.tracker.hasPending(id) && !this.tracker.hasOpenHandle(id),
|
|
760
|
+
);
|
|
761
|
+
if (deletable.length === 0) return 0;
|
|
762
|
+
const deleted = await this.backend.transaction((tx) => tx.deleteSessions(deletable));
|
|
763
|
+
for (const id of deletable) {
|
|
764
|
+
this.liveBuffers.delete(id);
|
|
765
|
+
this.liveReady.delete(id);
|
|
766
|
+
this.reuseEventIds.delete(id);
|
|
767
|
+
}
|
|
768
|
+
return deleted;
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
/** 用量统计:SQL 聚合的按天 × 模型桶与按会话行(事件行去重、排除孤儿行)。 */
|
|
772
|
+
async usageReport(sinceMs?: number): Promise<UsageAggregate> {
|
|
773
|
+
await this.ready;
|
|
774
|
+
return this.backend.usageReport(sinceMs);
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
/** GC 通道:VACUUM;调用方需先停止运行中的写路径(见 `registerSessionGc`)。 */
|
|
778
|
+
async vacuum(): Promise<void> {
|
|
779
|
+
await this.ready;
|
|
780
|
+
await this.backend.vacuum();
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
private async unarchiveBeforeDeletion(id: SessionId): Promise<void> {
|
|
784
|
+
const registry = this.ctx.get("workspaceRegistry") as unknown as
|
|
785
|
+
| {
|
|
786
|
+
readonly archivedSessionIds: readonly SessionId[];
|
|
787
|
+
unarchiveSession(sessionId: SessionId): Promise<void>;
|
|
788
|
+
}
|
|
789
|
+
| undefined;
|
|
790
|
+
if (registry === undefined) return;
|
|
791
|
+
if (!registry.archivedSessionIds.includes(id)) return;
|
|
792
|
+
await registry.unarchiveSession(id);
|
|
793
|
+
}
|
|
715
794
|
|
|
716
|
-
/** 导出当前世代的 jsonl artifact,视图只读不落库。 */
|
|
717
795
|
async readRaw(
|
|
718
796
|
id: SessionId,
|
|
719
797
|
signal?: AbortSignal,
|
|
@@ -727,17 +805,18 @@ export class SessionPersistenceRdb extends SessionPersistence {
|
|
|
727
805
|
const log = await this.readLog(id, {}, signal);
|
|
728
806
|
if (log === undefined) return undefined;
|
|
729
807
|
repairReadView(log.events);
|
|
730
|
-
|
|
808
|
+
// 整段日志导出:只丢弃「已不平衡」之后的尾部(尾部未闭合的 step 是中断运行的正常形状,由上游 resume 补 closers)
|
|
809
|
+
const events = balanceRewindPrefix(log.events, { keepOpenTail: true });
|
|
810
|
+
const inheritedEventCount = Math.min(log.inheritedEventCount, events.length);
|
|
731
811
|
return {
|
|
732
812
|
meta: log.meta,
|
|
733
813
|
inheritedEventCount,
|
|
734
|
-
|
|
814
|
+
|
|
735
815
|
filename: sessionFormatLogFilename(SESSION_FORMAT_VERSION),
|
|
736
|
-
content: toJsonlArtifact(log.meta, inheritedEventCount,
|
|
816
|
+
content: toJsonlArtifact(log.meta, inheritedEventCount, events),
|
|
737
817
|
};
|
|
738
818
|
}
|
|
739
819
|
|
|
740
|
-
/** 空会话 materialize(flush 的持久化屏障)。 */
|
|
741
820
|
async materializeEmpty(
|
|
742
821
|
meta: SessionHeader,
|
|
743
822
|
inheritedEventCount: SessionLogOffset,
|
|
@@ -751,10 +830,6 @@ export class SessionPersistenceRdb extends SessionPersistence {
|
|
|
751
830
|
this.writeGuard.confirmHead(meta.id, -1);
|
|
752
831
|
}
|
|
753
832
|
|
|
754
|
-
/** 原样 append 落库(handle 已校验 contiguity;torn tail 先截断)。
|
|
755
|
-
* 与上游 JSONL 一致:ignorable 事件原样存储,不做过滤。写路径校验当前
|
|
756
|
-
* 格式形状(fail-closed):未知类型(非 ignorable)与非法消息形状拒绝入库;
|
|
757
|
-
* 非当前格式(v0/v1/v2)数据只在读取时经 legacy 转换链动态转换,不落新库。 */
|
|
758
833
|
async appendBatch(
|
|
759
834
|
meta: SessionHeader,
|
|
760
835
|
inheritedEventCount: SessionLogOffset,
|
|
@@ -763,10 +838,9 @@ export class SessionPersistenceRdb extends SessionPersistence {
|
|
|
763
838
|
): Promise<boolean> {
|
|
764
839
|
await this.ready;
|
|
765
840
|
if (events.length === 0) return false;
|
|
766
|
-
|
|
767
|
-
// 数据只能是当前格式形状。拷贝避免 adopt 替换污染调用方数组。
|
|
841
|
+
|
|
768
842
|
validateStoredEvents(meta, [...events]);
|
|
769
|
-
|
|
843
|
+
|
|
770
844
|
const reuse = this.reuseEventIds.get(meta.id);
|
|
771
845
|
if (reuse !== undefined) this.reuseEventIds.delete(meta.id);
|
|
772
846
|
let confirmedHead = -1;
|
|
@@ -782,9 +856,7 @@ export class SessionPersistenceRdb extends SessionPersistence {
|
|
|
782
856
|
}
|
|
783
857
|
await tx.upsertSession({ meta, inheritedEventCount }, randomUUID());
|
|
784
858
|
const head = await tx.getHead(meta.id);
|
|
785
|
-
|
|
786
|
-
// 视图 append 会把事件静默重编号到对方尾部、损坏 log。磁盘 head 必须
|
|
787
|
-
// 等于本实例确认过的最后一个 head。
|
|
859
|
+
|
|
788
860
|
this.writeGuard.assertNoConcurrentWriter(meta.id, head.fHeadSequence);
|
|
789
861
|
const { headEventId, headSequence } = await appendEventTail(
|
|
790
862
|
tx,
|
|
@@ -797,13 +869,12 @@ export class SessionPersistenceRdb extends SessionPersistence {
|
|
|
797
869
|
await tx.bumpRevision(meta.id);
|
|
798
870
|
confirmedHead = headSequence;
|
|
799
871
|
});
|
|
800
|
-
|
|
872
|
+
|
|
801
873
|
this.writeGuard.confirmHead(meta.id, confirmedHead);
|
|
802
874
|
this.tracker.materialized(meta.id);
|
|
803
875
|
return true;
|
|
804
876
|
}
|
|
805
877
|
|
|
806
|
-
/** 读取一个会话的稠密 log(含 torn tail 检测,不含修复改写)。 */
|
|
807
878
|
async readLog(
|
|
808
879
|
id: SessionId,
|
|
809
880
|
options: { fromSeq?: number } = {},
|
|
@@ -820,10 +891,8 @@ export class SessionPersistenceRdb extends SessionPersistence {
|
|
|
820
891
|
|
|
821
892
|
revision: number;
|
|
822
893
|
|
|
823
|
-
/** 存储桥接行数(迁移链可能生成/合并事件,与 `events.length` 不同)。 */
|
|
824
894
|
storedCount: number;
|
|
825
895
|
|
|
826
|
-
/** 是否经上游迁移链转换(非当前格式 → 当前格式)。 */
|
|
827
896
|
migrated: boolean;
|
|
828
897
|
}
|
|
829
898
|
| undefined
|
|
@@ -839,11 +908,7 @@ export class SessionPersistenceRdb extends SessionPersistence {
|
|
|
839
908
|
? await this.backend.getEventRows(id)
|
|
840
909
|
: await this.backend.getEventRows(id, options.fromSeq);
|
|
841
910
|
signal?.throwIfAborted();
|
|
842
|
-
|
|
843
|
-
// 当前逻辑事件。迁移链自带 seq gap / torn tail 校验(strict recovery),
|
|
844
|
-
// 无需 scanRows。混合世代 log(旧写入器跨上游版本追加)不是任何单一已
|
|
845
|
-
// 发布格式,迁移链必然拒绝——回退为当前格式视图(header 版本归一 +
|
|
846
|
-
// 读取视图修复)。
|
|
911
|
+
|
|
847
912
|
if (isLegacyVersion(row.fVersion)) {
|
|
848
913
|
try {
|
|
849
914
|
const converted = convertLegacyRows(row, eventRows);
|
|
@@ -886,15 +951,6 @@ export class SessionPersistenceRdb extends SessionPersistence {
|
|
|
886
951
|
};
|
|
887
952
|
}
|
|
888
953
|
|
|
889
|
-
/**
|
|
890
|
-
* 把迁移链读出的当前格式视图整体落库(非当前格式会话写打开时的一次性迁移)。
|
|
891
|
-
*
|
|
892
|
-
* 迁移链会生成/合并事件(end-seed / attempt / chunk 合并),事件 seq 空间
|
|
893
|
-
* 与存储桥接行数不再相等;写路径以存储 head 为锚点重编号,二者不一致会让
|
|
894
|
-
* append 撞上已有行。这里在同一事务内删光本会话桥接行、按迁移视图重建
|
|
895
|
-
* (新事件行,完整信封),并更新 head 与 revision;旧事件行保留(可能被
|
|
896
|
-
* fork 子会话引用,孤儿由惰性 GC 处理)。
|
|
897
|
-
*/
|
|
898
954
|
private async rewriteMigratedLog(
|
|
899
955
|
id: SessionId,
|
|
900
956
|
log: { meta: SessionHeader; inheritedEventCount: number; events: SessionEvent[] },
|
|
@@ -955,7 +1011,6 @@ export class SessionPersistenceRdb extends SessionPersistence {
|
|
|
955
1011
|
return this.rowRevision(row);
|
|
956
1012
|
}
|
|
957
1013
|
|
|
958
|
-
/** 便捷:create + append + close(测试与导入路径共用)。 */
|
|
959
1014
|
async createAndAppend(
|
|
960
1015
|
header: SessionHeader,
|
|
961
1016
|
events: readonly SessionEvent[],
|
|
@@ -974,7 +1029,6 @@ export class SessionPersistenceRdb extends SessionPersistence {
|
|
|
974
1029
|
}
|
|
975
1030
|
}
|
|
976
1031
|
|
|
977
|
-
/** 便捷:open(read) + read 全量 + close。 */
|
|
978
1032
|
async load(
|
|
979
1033
|
id: SessionId,
|
|
980
1034
|
signal?: AbortSignal,
|
|
@@ -988,8 +1042,7 @@ export class SessionPersistenceRdb extends SessionPersistence {
|
|
|
988
1042
|
);
|
|
989
1043
|
const row = await this.backend.getSession(id);
|
|
990
1044
|
if (row === undefined) throw new SessionPersistenceNotFoundError(id);
|
|
991
|
-
|
|
992
|
-
// 当前格式 header);当前格式会话用存储行。
|
|
1045
|
+
|
|
993
1046
|
if (isLegacyVersion(row.fVersion)) {
|
|
994
1047
|
return {
|
|
995
1048
|
meta: handle.header,
|
|
@@ -1007,7 +1060,6 @@ export class SessionPersistenceRdb extends SessionPersistence {
|
|
|
1007
1060
|
}
|
|
1008
1061
|
}
|
|
1009
1062
|
|
|
1010
|
-
/** 便捷:open(write) + append + close。 */
|
|
1011
1063
|
async append(id: SessionId, events: readonly SessionEvent[]): Promise<void> {
|
|
1012
1064
|
const handle = await this.open(id, "write");
|
|
1013
1065
|
try {
|
|
@@ -1017,7 +1069,6 @@ export class SessionPersistenceRdb extends SessionPersistence {
|
|
|
1017
1069
|
}
|
|
1018
1070
|
}
|
|
1019
1071
|
|
|
1020
|
-
/** 便捷:readFrom(稠密后缀)。 */
|
|
1021
1072
|
async readFrom(
|
|
1022
1073
|
id: SessionId,
|
|
1023
1074
|
fromSeq: number,
|
|
@@ -1072,8 +1123,6 @@ export class SessionPersistenceRdb extends SessionPersistence {
|
|
|
1072
1123
|
);
|
|
1073
1124
|
}
|
|
1074
1125
|
|
|
1075
|
-
// --- live 路由:session/created → create/adopt handle;event → 缓冲;flush → drain ---
|
|
1076
|
-
|
|
1077
1126
|
private installLiveRouting(ctx: Context): void {
|
|
1078
1127
|
ctx.on("session/created", (session: Session) => {
|
|
1079
1128
|
this.liveBuffers.set(session.id, []);
|
|
@@ -1085,7 +1134,7 @@ export class SessionPersistenceRdb extends SessionPersistence {
|
|
|
1085
1134
|
);
|
|
1086
1135
|
});
|
|
1087
1136
|
});
|
|
1088
|
-
|
|
1137
|
+
|
|
1089
1138
|
for (const session of ctx.sessions.list()) {
|
|
1090
1139
|
this.liveBuffers.set(session.id, []);
|
|
1091
1140
|
const ready = this.ensureLiveHandle(session);
|
|
@@ -1138,24 +1187,20 @@ export class SessionPersistenceRdb extends SessionPersistence {
|
|
|
1138
1187
|
closeHandle();
|
|
1139
1188
|
return;
|
|
1140
1189
|
}
|
|
1141
|
-
|
|
1190
|
+
|
|
1142
1191
|
void ready.then(closeHandle, closeHandle);
|
|
1143
1192
|
});
|
|
1144
1193
|
ctx.effect(
|
|
1145
1194
|
() => async () => {
|
|
1146
|
-
// 先等所有 live handle 就绪(ensureLiveHandle 异步),再统一关闭。
|
|
1147
1195
|
await Promise.allSettled(this.liveReady.values());
|
|
1148
1196
|
await this.tracker.closeAll();
|
|
1149
|
-
|
|
1150
|
-
// 可能立即释放存储(pg 测试 drop 数据库),未完成的 open 会以
|
|
1151
|
-
// 无人处理的 rejection 泄漏。
|
|
1197
|
+
|
|
1152
1198
|
await this.close();
|
|
1153
1199
|
},
|
|
1154
1200
|
`${this.name} open handles`,
|
|
1155
1201
|
);
|
|
1156
1202
|
}
|
|
1157
1203
|
|
|
1158
|
-
/** session/created 后为 live 会话建立 write handle(create 或 adopt)。 */
|
|
1159
1204
|
private async ensureLiveHandle(session: Session): Promise<void> {
|
|
1160
1205
|
const id = session.header.id;
|
|
1161
1206
|
if (this.tracker.writerOf(id) !== undefined) return;
|
|
@@ -1163,15 +1208,12 @@ export class SessionPersistenceRdb extends SessionPersistence {
|
|
|
1163
1208
|
const stored = await this.readLog(id, {});
|
|
1164
1209
|
let handle: RdbSessionHandle;
|
|
1165
1210
|
if (stored === undefined) {
|
|
1166
|
-
// 新会话:注册 pending 并返回 write handle;构造 seed 事件不发布
|
|
1167
|
-
// session/event,须在此一次性落库(与旧版 onCreated 同语义)。
|
|
1168
1211
|
handle = (await this.create(session.header, {
|
|
1169
1212
|
inheritedEventCount: session.inheritedEventCount,
|
|
1170
1213
|
})) as RdbSessionHandle;
|
|
1171
1214
|
const seed = session.snapshotEvents();
|
|
1172
1215
|
if (seed.length > 0) await handle.append(seed);
|
|
1173
1216
|
} else {
|
|
1174
|
-
// adopt:校验 cwd / inheritedEventCount / seed 前缀匹配后接管写所有权。
|
|
1175
1217
|
if (stored.meta.cwd !== session.header.cwd) {
|
|
1176
1218
|
throw new Error(
|
|
1177
1219
|
`session "${id}" is already persisted at a different cwd (persisted: ${String(stored.meta.cwd)}, live: ${String(session.header.cwd)}) (id collision)`,
|
|
@@ -1183,8 +1225,7 @@ export class SessionPersistenceRdb extends SessionPersistence {
|
|
|
1183
1225
|
);
|
|
1184
1226
|
}
|
|
1185
1227
|
assertVersion(stored.meta);
|
|
1186
|
-
|
|
1187
|
-
// stream、surface 修复),未修复的存储视图会把修复差异误判为 id 冲突。
|
|
1228
|
+
|
|
1188
1229
|
repairReadView(stored.events);
|
|
1189
1230
|
const seed = session.snapshotEvents();
|
|
1190
1231
|
if (!seedCoversPrefix(seed, stored.events)) {
|
|
@@ -1193,11 +1234,11 @@ export class SessionPersistenceRdb extends SessionPersistence {
|
|
|
1193
1234
|
);
|
|
1194
1235
|
}
|
|
1195
1236
|
handle = (await this.open(id, "write")) as RdbSessionHandle;
|
|
1196
|
-
|
|
1237
|
+
|
|
1197
1238
|
const suffix = seed.slice(stored.events.length);
|
|
1198
1239
|
if (suffix.length > 0) await handle.append(suffix);
|
|
1199
1240
|
}
|
|
1200
|
-
|
|
1241
|
+
|
|
1201
1242
|
const buffered = this.liveBuffers.get(id);
|
|
1202
1243
|
if (buffered !== undefined && buffered.length > 0) {
|
|
1203
1244
|
this.liveBuffers.set(id, []);
|
|
@@ -1227,8 +1268,7 @@ function createBackend(config: Config): Backend {
|
|
|
1227
1268
|
});
|
|
1228
1269
|
}
|
|
1229
1270
|
const pool = new Pool({ connectionString: config.connectionString });
|
|
1230
|
-
|
|
1231
|
-
// 以 uncaughtException 崩溃进程;池级错误在下次查询处可见,这里只消费。
|
|
1271
|
+
|
|
1232
1272
|
pool.on("error", () => {});
|
|
1233
1273
|
const db = drizzlePg({ client: pool });
|
|
1234
1274
|
const identityBase = [
|
|
@@ -1254,7 +1294,7 @@ async function appendEventTail(
|
|
|
1254
1294
|
): Promise<{ headEventId: string; headSequence: number }> {
|
|
1255
1295
|
let parentId = anchor.parentId;
|
|
1256
1296
|
let nextSeq = anchor.nextSeq;
|
|
1257
|
-
|
|
1297
|
+
|
|
1258
1298
|
const eventRows: EventInsert[] = [];
|
|
1259
1299
|
const bridgeRows: Array<{
|
|
1260
1300
|
fSessionId: SessionId;
|
|
@@ -1267,9 +1307,7 @@ async function appendEventTail(
|
|
|
1267
1307
|
const eventId = reusedId ?? randomUUID();
|
|
1268
1308
|
if (reusedId === undefined) {
|
|
1269
1309
|
const { kind, role, name, actionId } = eventDimensions(event);
|
|
1270
|
-
|
|
1271
|
-
// 与信封字段在同一 JSON 记录里,读回时整体解析。surfaceOp 走桥接行
|
|
1272
|
-
// 列(f_surface_op),sourceEventSeqs 不落库(读取时重计算)。
|
|
1310
|
+
|
|
1273
1311
|
const raw = event as SessionEvent & {
|
|
1274
1312
|
ignorable?: unknown;
|
|
1275
1313
|
surfaceOp?: unknown;
|
|
@@ -1304,9 +1342,7 @@ async function appendEventTail(
|
|
|
1304
1342
|
}
|
|
1305
1343
|
if (eventRows.length > 0) await tx.insertEvents(eventRows);
|
|
1306
1344
|
await tx.insertBridges(bridgeRows);
|
|
1307
|
-
|
|
1308
|
-
// 列表消费直接取列(sessions.title 直取)。
|
|
1309
|
-
// 插件合并类型不在 core 的判别联合内,按字符串比较(同 eventDimensions)。
|
|
1345
|
+
|
|
1310
1346
|
if (events.some((event) => (event.type as string) === "session/title")) {
|
|
1311
1347
|
await tx.refreshTitle(meta.id);
|
|
1312
1348
|
}
|