@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.
Files changed (70) hide show
  1. package/README.md +59 -31
  2. package/dist/artifact.d.mts +25 -65
  3. package/dist/artifact.mjs +3 -3
  4. package/dist/{schema-DPcuEh_a.d.mts → backend-DpdtxYpz.d.mts} +360 -107
  5. package/dist/branch-5JzX9rUq.mjs +390 -0
  6. package/dist/deletion.d.mts +7 -0
  7. package/dist/deletion.mjs +2 -0
  8. package/dist/dist-vIVO6bA-.mjs +1524 -0
  9. package/dist/import.d.mts +4 -4
  10. package/dist/import.mjs +214 -1
  11. package/dist/index.d.mts +2 -3
  12. package/dist/index.mjs +4 -1630
  13. package/dist/{log-DO69NQnn.mjs → log-CnYct2Dv.mjs} +37 -82
  14. package/dist/{sqlite-DYExtbLo.mjs → sqlite-fpvm5Dzs.mjs} +205 -75
  15. package/dist/src-CWTWV7vx.mjs +1904 -0
  16. package/dist/storage.d.mts +14 -5
  17. package/dist/storage.mjs +2 -2
  18. package/dist/testing.d.mts +2 -26
  19. package/dist/testing.mjs +10504 -9614
  20. package/drizzle/postgres/20260918120000_v3_event_usage/migration.sql +14 -0
  21. package/drizzle/postgres/20260918120000_v3_event_usage/snapshot.json +1309 -0
  22. package/drizzle/sqlite/20260918120000_v3_event_usage/migration.sql +14 -0
  23. package/drizzle/sqlite/20260918120000_v3_event_usage/snapshot.json +1031 -0
  24. package/package.json +29 -30
  25. package/src/adapters/to-postgres.ts +1 -3
  26. package/src/adapters/to-sqlite.ts +0 -2
  27. package/src/adapters/types.ts +0 -3
  28. package/src/artifact.ts +0 -2
  29. package/src/backend.ts +31 -2
  30. package/src/branch.ts +223 -135
  31. package/src/deletion.ts +87 -0
  32. package/src/drizzle/postgres-v2.ts +0 -1
  33. package/src/drizzle/postgres-v3.ts +0 -1
  34. package/src/drizzle/sqlite-v2.ts +0 -1
  35. package/src/drizzle/sqlite-v3.ts +0 -1
  36. package/src/entities/v2/session-events.ts +0 -1
  37. package/src/entities/v3/event-usage.ts +25 -0
  38. package/src/entities/v3/events.ts +1 -3
  39. package/src/entities/v3/index.ts +4 -0
  40. package/src/entities/v3/session-events.ts +1 -2
  41. package/src/entities/v3/session-projcache-rows.ts +0 -8
  42. package/src/entities/v3/sessions.ts +3 -3
  43. package/src/entities/v3/storage-units.ts +0 -1
  44. package/src/entities/v3/workspace-sessions.ts +0 -5
  45. package/src/entities/v3/workspace-state.ts +0 -6
  46. package/src/entities/v3/workspaces.ts +0 -5
  47. package/src/export.ts +103 -0
  48. package/src/gc.ts +76 -0
  49. package/src/import-storages.ts +4 -37
  50. package/src/import.ts +54 -31
  51. package/src/index.ts +150 -114
  52. package/src/legacy.ts +4 -42
  53. package/src/log.ts +63 -106
  54. package/src/postgres.ts +249 -22
  55. package/src/schema.ts +6 -6
  56. package/src/session-query.ts +0 -4
  57. package/src/sqlite.ts +231 -55
  58. package/src/storage-takeover/index.ts +2 -28
  59. package/src/storage-takeover/projection-cache.ts +30 -134
  60. package/src/storage-takeover/repository.ts +20 -59
  61. package/src/storage-takeover/storage-backend.ts +3 -33
  62. package/src/storage-takeover/types.ts +14 -56
  63. package/src/storage.ts +0 -2
  64. package/src/testing/contract.ts +19 -50
  65. package/src/testing/coordinator-contract.ts +10 -25
  66. package/src/testing.ts +2 -2
  67. package/src/usage.ts +191 -0
  68. package/dist/import-Bc2QQa5G.mjs +0 -473
  69. package/dist/index-CCcK9tia.d.mts +0 -270
  70. 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
- /** 脏 checkpoint 在强制点之间允许滞留的最长毫秒数(默认 5000)。 */
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
- /** 一个已创建但未 materialize 的会话(本进程可见,其他进程不可见)。 */
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
- /** 输入空间 cursor(全 delta 批次也推进)。 */
142
+
126
143
  readonly cursor: number;
127
- /** 是否调用过 append(close 时保留 pending)。 */
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
- /** 稠密 next-seq(输入空间计数,与旧版 coordinator cursor 同语义)。 */
255
+
236
256
  private cursor: number;
237
257
  private materialized: boolean;
238
- /** live 路由缓冲(上游 seq 事件,drain 时过滤 delta 后重编号稠密)。 */
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
- /** write open 时发现的 torn tail 起点(append 前先截断)。 */
263
+
244
264
  private tornTruncateTo: number | undefined;
245
- /** 是否调用过 append(即使全 delta 未落库)——close 时保留 pending。 */
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
- // 读取时修复(视图只读,不落库):结算字段补全、非法 surface 替换降级
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
- // 原样存储(与上游 JSONL 一致):cursor 推进 batch.length,全部落库。
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
- // 异步打开连接,避免阻塞插件 apply;存储钩子统一 await 同一个 readiness。
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
- // 分支 provider 服务(rewind / forkFrom / timeline),随 fiber 卸载自动回滚。
543
+
537
544
  new SessionBranchRdb(this.ctx);
538
- // 会话查询服务:接管官方 session-query-sqlite(同名 provide 会 fail loud,
539
- // 其装配行由 better-session patch 禁用);class-plugin 装载以复用基类的
540
- // 精确读 / 过滤 / 血缘实现。
545
+
541
546
  this.ctx.plugin(SessionQueryRdb, {});
542
- // 导入端点:webServer + connection 就绪后注册 `/api/session.import`。
547
+
543
548
  registerSessionImport(this.ctx, this);
544
- // storages 接管:storage hub 的 `rdb` 后端(workspace 域)与
545
- // `ctx.sessionProjectionCache` 服务(替换上游插件)。
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
- // 读取视图修复必须先于校验:越界 replace(旧写入器重编号遗留的旧坐标)
618
- // 降级/夹取、request/header 归一,否则上游事件校验 fail loud 拒绝整个
619
- // 会话(load / open 是历史会话的加载入口,不能只依赖 handle.read)。
627
+
620
628
  repairReadView(log.events);
621
- // fail-closed:未知事件类型(非 ignorable)拒绝解释。
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
- // 迁移链会生成/合并事件(end-seed / attempt / chunk 合并),事件坐标与
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
- // 确认 head:本实例已读该会话,后续 append 的并发校验以此为基准。
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
- // --- RDB 特有能力(rewind / fork / 导出 / 测试支撑) ---
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
- const inheritedEventCount = Math.min(log.inheritedEventCount, log.events.length);
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
- // 内容按当前世代编码,文件名必须声明同一世代(上游导出用 session.vN.jsonl)。
814
+
735
815
  filename: sessionFormatLogFilename(SESSION_FORMAT_VERSION),
736
- content: toJsonlArtifact(log.meta, inheritedEventCount, log.events),
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
- // 写路径当前格式校验:与读路径同契约(validateStoredEvents),保证新入库
767
- // 数据只能是当前格式形状。拷贝避免 adopt 替换污染调用方数组。
841
+
768
842
  validateStoredEvents(meta, [...events]);
769
- // fork 派生会话的 seed 复用源会话事件行(不复制);消费后清除。
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
- // 提交后才确认新 head:回滚不得留下本实例实际未写的已确认 head。
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
- // 非当前格式(v0/v1/v2)历史数据:行重建为物理记录,经上游迁移链转
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
- // 非当前格式会话:meta 与继承前缀来自转换链(handle.header 已是转换后的
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
- // HMR:插件 apply 时已存在的 live 会话不重放 session/created——补种。
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
- // handle 可能仍在构造(ensureLiveHandle 异步):等就绪后再 close。
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
- // 等 init(异步 open)settle 后关闭后端连接:dispose 返回后调用方
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
- // adopt 比较必须与读取视图同源:live seed 来自修复后的读取视图(补
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
- // 持久化 seed 后缀(构造 seed 事件不发布 session/event,缓冲看不到)。
1237
+
1197
1238
  const suffix = seed.slice(stored.events.length);
1198
1239
  if (suffix.length > 0) await handle.append(suffix);
1199
1240
  }
1200
- // 把 handle 就绪前缓冲的事件移交。
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
- // node-postgres 要求 Pool 必须监听 error:未监听的 idle client error 会
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
- // 两个批次一次性多行 INSERT(N 事件 2 条语句,而非 2N)。
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
- // fData 存完整事件(含 ignorable 信封,与 JSONL 每行同构):data 部分
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
- // 标题是会话数据的一部分:批内出现 session/title 时同步刷新会话行,
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
  }