@x-otto/session 0.0.1-alpha.4 → 0.0.1-alpha.6

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/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { DiskPersistence, MemoryPersistenceBase, Migration, PaginatedResult, PersistenceShape, RemotePersistence, RemotePersistenceError, RemoteSnapshotConflictError } from "@x-otto/persistence";
1
+ import { MemoryPersistenceBase, Migration, PaginatedResult, PersistenceShape, RemotePersistence, RemotePersistenceError, RemoteSnapshotConflictError } from "@x-otto/persistence";
2
2
 
3
3
  //#region src/types.d.ts
4
4
  type SessionEntry<T = unknown> = {
@@ -40,6 +40,22 @@ type SessionEntry<T = unknown> = {
40
40
  id: string;
41
41
  parentId?: string;
42
42
  timestamp: number;
43
+ }
44
+ /**
45
+ * RFC-381 M1:压缩锁 bracket(跨进程 durable 锁)。
46
+ * 压缩开始写 phase:'start'、结束写 phase:'end'(同 token);崩溃后 start 无配对 end
47
+ * 即为孤儿锁(可超时/takeover 接管)。
48
+ * **三类边界**:锁 entry 非消息(buildContext/messages/visibleMessages 均不可见)、
49
+ * 不计 messageCount、不 bump lastActiveAt(派生记账非真实活动)。
50
+ * **additive optional variant——不升 SESSION_SNAPSHOT_VERSION**(旧快照无此条目即无锁)。
51
+ */
52
+ | {
53
+ type: 'compaction-lock';
54
+ id: string;
55
+ parentId?: string; /** 'start' = 持锁;'end' = 释放。 */
56
+ phase: 'start' | 'end'; /** 持锁进程 token(pid + 随机后缀)——end 必须匹配同 token;孤儿判定依据。 */
57
+ token: string;
58
+ timestamp: number;
43
59
  };
44
60
  interface SessionContext<T = unknown> {
45
61
  summary?: string;
@@ -246,6 +262,17 @@ interface Session<T = unknown> {
246
262
  clearContext(options?: {
247
263
  purge?: boolean;
248
264
  }): void;
265
+ /**
266
+ * RFC-381 M1/M2:压缩锁 bracket 写日志(compaction-lock entry,start/end + token)。
267
+ * 可选——旧实现/轻量替身无此能力时 lock 端口应拒绝接线(见 session-compaction-lock)。
268
+ */
269
+ appendCompactionLock?(phase: 'start' | 'end', token: string): void;
270
+ /** RFC-381 M1/M2:当前压缩锁状态(最近一个 compaction-lock entry;无则 undefined)。 */
271
+ compactionLockState?(): {
272
+ phase: 'start' | 'end';
273
+ token: string;
274
+ timestamp: number;
275
+ } | undefined;
249
276
  /**
250
277
  * RFC-194 D3:物理释放最后一个 clear 边界之前的内存驻留(message 条目 splice 移除 +
251
278
  * 非活跃 compaction 的 replacement 剥离;DB append-forever 不动)。幂等;无边界 no-op。
@@ -284,6 +311,25 @@ interface Session<T = unknown> {
284
311
  * (不保留旧分组,防止"未置顶但残留分组名"的僵尸状态,见 RFC-158 §4)。
285
312
  */
286
313
  setPinned(pinned: boolean, group?: string): void;
314
+ /** 绝对回合总数(resume 末尾对齐派生 turnBoundaries)。领域记账,非 UI 面板态。 */
315
+ getTurnCount(): number | undefined;
316
+ setTurnCount(n: number): void;
317
+ }
318
+ /**
319
+ * UI 面板态持久化接口——todoList/editedFiles/subagents/drafts/turnSummaries 五个
320
+ * TUI 面板 + 已废弃的输入历史迁移读取。
321
+ *
322
+ * 终局 review 2026-08-14:从 `Session` 拆出(原 Session 接口 40 方法中 14 个是面板
323
+ * get/set,领域接口被 UI 演进绑架——每加一个新面板都要改领域接口、聚合根与快照格式
324
+ * 三处)。拆分后消费方按需收窄类型:领域读者用 `Session`,面板读者用
325
+ * `SessionPanelState`,两者都要用 `Session & SessionPanelState`。
326
+ *
327
+ * **终局方向(投影化)**:面板态是 entry 事件流上的 UI 投影(读模型),理想形态是从
328
+ * seq watermark 折叠出面板视图并独立落库(投影缓存,机制参照 deepseek-harness
329
+ * session-projection-cache),而不是聚合根的附属存储。本接口是迁移的中途形态:先做
330
+ * 接口层分离(零行为变化),投影化存储变更作为专项另行推进。
331
+ */
332
+ interface SessionPanelState {
287
333
  /**
288
334
  * 持久化 config 字段存取器(取代 cli 侧 `as unknown as {...}` 裸 cast,给持久化契约真类型)。
289
335
  * RFC-108 D3:todoList/editedFiles/subagents/drafts 四个 UI 面板字段从 PersistedSessionConfig
@@ -306,9 +352,6 @@ interface Session<T = unknown> {
306
352
  /** 子代理面板持久化(stat-only)。RFC-108 D3:存储迁至 panelState 独立字段。 */
307
353
  getSubagents(): SubagentEntry[] | undefined;
308
354
  setSubagents(entries: SubagentEntry[], options?: PanelWriteOptions): void;
309
- /** 绝对回合总数(resume 末尾对齐派生 turnBoundaries)。 */
310
- getTurnCount(): number | undefined;
311
- setTurnCount(n: number): void;
312
355
  /** 草稿面板持久化(DraftPane 的 Ctrl+S 保存/加载)。RFC-108 D3:存储迁至 panelState 独立字段。 */
313
356
  getDrafts(): DraftEntry[] | undefined;
314
357
  setDrafts(entries: DraftEntry[], options?: PanelWriteOptions): void;
@@ -364,6 +407,9 @@ interface SessionPersistence<T = unknown> extends PersistenceShape<SessionSnapsh
364
407
  /**
365
408
  * RFC-325 M4:DB 冷归档能力(optional,只有 relational SQLite 后端实现)。
366
409
  * 将 entries 复制到外部 .jsonl.gz + 标 archived=1,**不删除 DB 行**(RFC-159 红线)。
410
+ *
411
+ * **能力探测**:消费方应使用 {@link supportsArchive} 类型守卫,而非 `typeof` 检查或 `as` cast
412
+ *(RFC-393 M2:三态收拢为统一能力接口模式)。
367
413
  */
368
414
  archiveSession?: (sessionId: string) => Promise<string>;
369
415
  /** 从外部归档恢复(幂等 append,不删除既有条目)。 */
@@ -377,10 +423,21 @@ interface PaginationOptions {
377
423
  }
378
424
  //#endregion
379
425
  //#region src/in-memory-session.d.ts
380
- declare class InMemorySession<T = unknown> implements Session<T> {
426
+ /** 注入端口形状(结构类型,与 @x-otto/agent ClockPort/IdPort 兼容)。 */
427
+ interface InMemorySessionPorts {
428
+ clock?: {
429
+ now(): number;
430
+ };
431
+ idGen?: {
432
+ uuid(): string;
433
+ };
434
+ }
435
+ declare class InMemorySession<T = unknown> implements Session<T>, SessionPanelState {
381
436
  readonly id: string;
382
437
  private readonly sessionEntries;
383
438
  private readonly entryMap;
439
+ private readonly clock;
440
+ private readonly idGen;
384
441
  /**
385
442
  * RFC-178 D2:会话消息内容字节估算(增量维护,避免每回合 O(n) 重算——lesson_54)。
386
443
  * 覆盖全部变更路径:append 累加 / cap splice 递减 / clearContext purge 递减 /
@@ -404,6 +461,8 @@ declare class InMemorySession<T = unknown> implements Session<T> {
404
461
  * 时机静默错位。本方法给测试(每个 mutation 路径测试的收尾断言)与 debug 排查提供
405
462
  * 唯一真源比对点。O(n) 全量遍历,**勿在热路径调用**(lesson_54)。
406
463
  *
464
+ * RFC-393 M3:实现委托给 {@link ResidencyPolicy}(计数器 owner 仍为本类)。
465
+ *
407
466
  * @returns 漂移量(`recomputed - counter`;0 = 无漂移)
408
467
  */
409
468
  verifyEstimatedContentBytes(): {
@@ -419,7 +478,16 @@ declare class InMemorySession<T = unknown> implements Session<T> {
419
478
  * 无需修改。turnSummaries 是后续新增字段(回合完成摘要行持久化,见 TurnSummaryEntry)。
420
479
  */
421
480
  private readonly panelState;
422
- constructor(id: string, parentSessionId?: string, forkPoint?: number);
481
+ /**
482
+ * RFC-393 M3:驻留治理策略(capStoredHistory / capStoredHistoryByBytes / wouldTrim* /
483
+ * verifyEstimatedContentBytes / removeOldestMessages)从聚合根抽离。
484
+ * 计数器唯一 owner 仍是本类(`_estimatedContentBytes` / `meta.messageCount`)——
485
+ * policy 经下方闭包 host 读写,不持有可变状态。
486
+ */
487
+ private readonly residencyPolicy;
488
+ constructor(id: string, parentSessionId?: string, forkPoint?: number, ports?: InMemorySessionPorts);
489
+ /** RFC-393 M3:policy 访问内部状态的窄接口(闭包捕获私有字段,不暴露 public 方法)。 */
490
+ private createResidencyHost;
423
491
  private _leafId;
424
492
  get leafId(): string | undefined;
425
493
  /**
@@ -492,6 +560,27 @@ declare class InMemorySession<T = unknown> implements Session<T> {
492
560
  clearContext(options?: {
493
561
  purge?: boolean;
494
562
  }): void;
563
+ /**
564
+ * RFC-381 M1:压缩锁 bracket 写日志(append-only,历史不可变)。
565
+ * 写入 `compaction-lock` 结构 entry(phase start/end + token)。
566
+ * **三类边界**:不 bump messageCount、不 markActivity(派生记账非真实活动)、
567
+ * buildContext/messages/visibleMessages 均不可见(非 message 类型天然被过滤)。
568
+ * 落库由持久化层照常走(type+data 通用行,零序列化改动)。
569
+ */
570
+ appendCompactionLock(phase: 'start' | 'end', token: string): void;
571
+ /**
572
+ * RFC-381 M1:当前压缩锁状态(最近一个 compaction-lock entry)。
573
+ * - undefined:无锁(从未加锁或已 end);
574
+ * - phase 'end':已释放;
575
+ * - phase 'start':持锁中(token 是持有者;跨进程据此判定孤儿——start 长时间无
576
+ * 配对 end 即崩溃残留,可超时/takeover)。
577
+ * 只读当前 leaf 分支(walkBranch),不触碰完整转录。
578
+ */
579
+ compactionLockState(): {
580
+ phase: 'start' | 'end';
581
+ token: string;
582
+ timestamp: number;
583
+ } | undefined;
495
584
  /**
496
585
  * RFC-194 D3:物理释放最后一个 clear 边界之前的内存驻留(DB append-forever 不动)。
497
586
  * 供持久化层在「clear 边界确认落盘成功后」调用(顺序保证见 RFC-194 D2——先 save
@@ -622,7 +711,7 @@ declare class InMemorySession<T = unknown> implements Session<T> {
622
711
  * 操作零 UI 反馈,用户长会话(>2000 条)resume 后发现前段历史消失却毫无线索。**本方法只
623
712
  * 操作内存**——持久层是否也删除取决于调用方使用的 persistence 实现:RFC-159 起默认的
624
713
  * `RelationalSessionPersistence`(SQLite)是 append-forever,被本方法裁掉的条目仍完整
625
- * 保留在 DB;仅已废弃的 `OTTO_SESSION_BLOB=1` 逃生口后端会因整份快照覆写而真实丢失。
714
+ * 保留在 DB(RFC-393 P2 已删除 OTTO_SESSION_BLOB=1 blob 逃生口,不再有整份快照覆写丢失)。
626
715
  * 返回 `{capped:false}` 时未发生任何裁剪(正常场景,消息数未超上限)。
627
716
  *
628
717
  * **RFC-321 R5——返回语义是"尽力裁"而非"裁到 maxMessages"**:裁剪永不跨越活跃
@@ -633,6 +722,8 @@ declare class InMemorySession<T = unknown> implements Session<T> {
633
722
  * @returns `capped`/`removedCount` 同前;`boundaryLimited` = 因边界保护提前停止;
634
723
  * `activeOversized` = 边界前死历史已裁尽、**活跃区自身仍超限**(RFC-321 D1-a′ 逃生阀
635
724
  * 信号,供上层请求强制压缩;M1 只产出信号,消费方接线属 M2/T320-08b)。
725
+ *
726
+ * RFC-393 M3:实现委托给 {@link ResidencyPolicy}(计数器 owner 仍为本类)。
636
727
  */
637
728
  capStoredHistory(maxMessages: number, canStartHistory?: (msg: T) => boolean): {
638
729
  capped: boolean;
@@ -651,9 +742,11 @@ declare class InMemorySession<T = unknown> implements Session<T> {
651
742
  *
652
743
  * 判据:活跃边界之前的 message 条目数 < 需要移除的条数。无活跃边界时,边界前死历史
653
744
  * 视为 0——即任何裁剪都会触及活跃上下文,恒返回 true(这正是最该先压缩的场景)。
745
+ *
746
+ * RFC-393 M3:实现委托给 {@link ResidencyPolicy}。
654
747
  */
655
748
  wouldTrimTouchActiveContext(targetMessages: number): boolean;
656
- /** 同上,字节口径(供 `capStoredHistoryByBytes` 的前置判定)。 */
749
+ /** 同上,字节口径(供 `capStoredHistoryByBytes` 的前置判定)。RFC-393 M3:委托给 policy。 */
657
750
  wouldByteTrimTouchActiveContext(targetBytes: number): boolean;
658
751
  /**
659
752
  * RFC-178 D3 硬段:把存储的会话历史按**字节估算**收紧到 maxBytes 以内(从最早的消息条目
@@ -665,6 +758,8 @@ declare class InMemorySession<T = unknown> implements Session<T> {
665
758
  *
666
759
  * **RFC-321 R4/R5**:同样受活跃 compaction 边界保护、同样是"尽力裁"语义,返回字段与
667
760
  * `capStoredHistory` 同构(`activeOversized` 此处按字节口径判定:裁后估算字节仍超预算)。
761
+ *
762
+ * RFC-393 M3:实现委托给 {@link ResidencyPolicy}。
668
763
  */
669
764
  capStoredHistoryByBytes(maxBytes: number, canStartHistory?: (msg: T) => boolean): {
670
765
  capped: boolean;
@@ -672,28 +767,6 @@ declare class InMemorySession<T = unknown> implements Session<T> {
672
767
  boundaryLimited: boolean;
673
768
  activeOversized: boolean;
674
769
  };
675
- /**
676
- * 共享裁剪循环(条数/字节两模式复用):从 sessionEntries 头部移除 removeCount 条 message
677
- * 条目,随后按 canStartHistory 谓词继续移除至工具边界对齐(保留区第一条非孤儿
678
- * tool_result)。每次移除同步递减字节估算(RFC-178 D2 路径 2)。
679
- *
680
- * **RFC-321 R4(边界即止,不可削弱)**:裁剪**永不跨越活跃 compaction 边界**——遇到
681
- * 该边界即停止,宁可少裁(`removed < removeCount`)也不破坏 branch 链完整性。
682
- *
683
- * 修复前的缺陷(RFC-321 P1-a,实证):本循环只按 `type === 'message'` 从头部 splice,
684
- * 既不感知 compaction 边界、也不修复被删条目后继的 `parentId`。一旦裁剪深度超过边界前的
685
- * 死历史条数,边界条目的 `parentId` 就会指向已删 id → `walkBranch` 回溯撞空提前截断 →
686
- * 边界不在 branch 上 → `buildContext` 走 `lastBoundaryIndex === -1` 分支 → **summary 静默
687
- * 丢失**(模型上下文降级,用户与日志均无信号)。主要可达面是 `ResidencyGovernor` 的
688
- * critical 档(cap 除以 4,`residency-governor.ts`)——内存压力下恰恰最可能触发。
689
- *
690
- * 边界之前的条目全是"已被摘要替换的死历史",边界之后是模型仍在用的活跃上下文;以边界
691
- * 为界即把"裁剪只能裁死历史"从时序假设变成**结构不变式**(不再依赖上游触发器配合)。
692
- *
693
- * @returns `removed` 实际移除条数(可能 < removeCount);`boundaryLimited` 是否因边界
694
- * 保护提前停止(诊断用,非错误)。
695
- */
696
- private removeOldestMessages;
697
770
  setTags(tags: string[]): void;
698
771
  addTag(tag: string): void;
699
772
  restoreEntries(entries: SessionEntry<T>[], meta: SessionMetadata, restoredLeafId?: string): void;
@@ -713,6 +786,96 @@ declare class InMemorySession<T = unknown> implements Session<T> {
713
786
  private lastBoundaryIndex;
714
787
  }
715
788
  //#endregion
789
+ //#region src/residency-policy.d.ts
790
+ /**
791
+ * ResidencyPolicy 访问 InMemorySession 内部状态的窄接口。
792
+ * 由 session 以闭包实现(捕获私有字段),不暴露为 public 方法。
793
+ */
794
+ interface ResidencyPolicyHost<T> {
795
+ /** 活 entry 数组(policy 就地 splice 裁剪)。 */
796
+ getEntries(): SessionEntry<T>[];
797
+ /** entry id → entry 索引(policy 删除条目时同步清理)。 */
798
+ getEntryMap(): Map<string, SessionEntry<T>>;
799
+ /** 当前字节估算计数器(唯一 owner = session)。 */
800
+ getEstimatedBytes(): number;
801
+ setEstimatedBytes(n: number): void;
802
+ /** 当前消息条数计数器(唯一 owner = session)。 */
803
+ getMessageCount(): number;
804
+ setMessageCount(n: number): void;
805
+ /** 当前 leaf parent 链上最近的 compaction 边界 id(无则 undefined;clear 截断)。 */
806
+ getActiveCompactionId(): string | undefined;
807
+ }
808
+ interface ResidencyTrimResult {
809
+ capped: boolean;
810
+ removedCount: number;
811
+ boundaryLimited: boolean;
812
+ activeOversized: boolean;
813
+ }
814
+ declare class ResidencyPolicy<T> {
815
+ private readonly host;
816
+ constructor(host: ResidencyPolicyHost<T>);
817
+ /**
818
+ * 把存储的会话历史封顶到最近 maxMessages 条(删最早的消息条目,compaction/clear 边界
819
+ * 不参与计数、不被裁)。调用后 messageCount 重新派生。
820
+ *
821
+ * **RFC-321 R5——返回语义是"尽力裁"而非"裁到 maxMessages"**:裁剪永不跨越活跃
822
+ * compaction 边界(R4,见 removeOldestMessages),故 removedCount 可能小于所需,裁后
823
+ * 条数可能仍 > maxMessages。
824
+ */
825
+ capStoredHistory(maxMessages: number, canStartHistory?: (msg: T) => boolean): ResidencyTrimResult;
826
+ /**
827
+ * RFC-321 R12(M5 压缩前置):**只读预判**——把历史裁到 `targetMessages` 条是否会
828
+ * 触及活跃上下文(即边界前的死历史不够裁)。不做任何修改。
829
+ *
830
+ * 判据:活跃边界之前的 message 条目数 < 需要移除的条数。无活跃边界时,边界前死历史
831
+ * 视为 0——即任何裁剪都会触及活跃上下文,恒返回 true(这正是最该先压缩的场景)。
832
+ */
833
+ wouldTrimTouchActiveContext(targetMessages: number): boolean;
834
+ /** 同上,字节口径(供 capStoredHistoryByBytes 的前置判定)。 */
835
+ wouldByteTrimTouchActiveContext(targetBytes: number): boolean;
836
+ /**
837
+ * RFC-178 D3 硬段:把存储的会话历史按**字节估算**收紧到 maxBytes 以内(从最早的消息
838
+ * 条目开始裁,compaction/clear 边界不裁)。与 capStoredHistory(条数模式)共享同一裁剪
839
+ * 循环与工具边界对齐谓词。同为**纯内存防御**,DB append-forever 不受影响(RFC-159)。
840
+ *
841
+ * 保底护栏:无论多超限,保留区至少留 1 条消息(避免字节预算配置过小时清空整个会话视图)。
842
+ */
843
+ capStoredHistoryByBytes(maxBytes: number, canStartHistory?: (msg: T) => boolean): ResidencyTrimResult;
844
+ /**
845
+ * 计数器漂移校验:从 entries 全量重算字节估算,与增量计数器比对。
846
+ * O(n) 全量遍历,**勿在热路径调用**(lesson_54)。
847
+ *
848
+ * @returns 漂移量(`recomputed - counter`;0 = 无漂移)
849
+ */
850
+ verifyEstimatedContentBytes(): {
851
+ counter: number;
852
+ recomputed: number;
853
+ drift: number;
854
+ };
855
+ /**
856
+ * 共享裁剪循环(条数/字节两模式复用):从 entries 头部移除 removeCount 条 message
857
+ * 条目,随后按 canStartHistory 谓词继续移除至工具边界对齐(保留区第一条非孤儿
858
+ * tool_result)。每次移除同步递减字节估算(RFC-178 D2 路径 2)。
859
+ *
860
+ * **RFC-321 R4(边界即止,不可削弱)**:裁剪**永不跨越活跃 compaction 边界**——遇到
861
+ * 该边界即停止,宁可少裁(removed < removeCount)也不破坏 branch 链完整性。
862
+ *
863
+ * 修复前的缺陷(RFC-321 P1-a,实证):本循环只按 type === 'message' 从头部 splice,
864
+ * 既不感知 compaction 边界、也不修复被删条目后继的 parentId。一旦裁剪深度超过边界前的
865
+ * 死历史条数,边界条目的 parentId 就会指向已删 id → walkBranch 回溯撞空提前截断 →
866
+ * 边界不在 branch 上 → buildContext 走 lastBoundaryIndex === -1 分支 → **summary 静默
867
+ * 丢失**(模型上下文降级,用户与日志均无信号)。主要可达面是 ResidencyGovernor 的
868
+ * critical 档(cap 除以 4,residency-governor.ts)——内存压力下恰恰最可能触发。
869
+ *
870
+ * 边界之前的条目全是"已被摘要替换的死历史",边界之后是模型仍在用的活跃上下文;以边界
871
+ * 为界即把"裁剪只能裁死历史"从时序假设变成**结构不变式**(不再依赖上游触发器配合)。
872
+ *
873
+ * @returns `removed` 实际移除条数(可能 < removeCount);`boundaryLimited` 是否因边界
874
+ * 保护提前停止(诊断用,非错误)。
875
+ */
876
+ private removeOldestMessages;
877
+ }
878
+ //#endregion
716
879
  //#region src/message-bytes.d.ts
717
880
  /**
718
881
  * message-bytes.ts — RFC-178 D2:消息内容字节估算(会话驻留治理的字节维度口径)。
@@ -734,8 +897,18 @@ declare class InMemorySession<T = unknown> implements Session<T> {
734
897
  declare function estimateMessageContentBytes(message: unknown): number;
735
898
  //#endregion
736
899
  //#region src/store.d.ts
900
+ /**
901
+ * 内存会话容器(活 Session 持有者)。注意与 {@link InMemorySessionPersistence} 区分:
902
+ * 本类是 `SessionStore` 实现——持有 InMemorySession 实例、提供 create/get/list/delete/fork;
903
+ * InMemorySessionPersistence 是 `SessionPersistence` 实现——存取 SessionSnapshot 信封。
904
+ * 两者都叫 "InMemory" 但服务不同契约(活对象 vs 快照存取)。
905
+ */
737
906
  declare class InMemorySessionStore<T = unknown> implements SessionStore<T> {
738
907
  private readonly sessions;
908
+ private readonly ports?;
909
+ constructor(ports?: InMemorySessionPorts);
910
+ /** 会话 id 生成走注入端口(M-2 确定性),缺省 fallback 安全随机(与 InMemorySession 同纪律)。 */
911
+ private newId;
739
912
  createSession(id?: string): Promise<Session<T>>;
740
913
  getSession(id: string): Session<T> | undefined;
741
914
  listSessions(): ReadonlyArray<Session<T>>;
@@ -743,11 +916,15 @@ declare class InMemorySessionStore<T = unknown> implements SessionStore<T> {
743
916
  deleteSession(id: string): Promise<boolean>;
744
917
  forkSession(sourceId: string, newId?: string): Promise<Session<T> | undefined>;
745
918
  }
746
- declare function createInMemorySessionStore<T = unknown>(): SessionStore<T>;
919
+ declare function createInMemorySessionStore<T = unknown>(ports?: InMemorySessionPorts): SessionStore<T>;
747
920
  //#endregion
748
921
  //#region src/memory-persistence.d.ts
749
922
  /**
750
- * 内存态会话持久化。复用内核抽象基类 {@link MemoryPersistenceBase},仅声明 `extractId` +
923
+ * 内存态会话持久化(SessionSnapshot 存取)。注意与 {@link InMemorySessionStore} 区分:
924
+ * 本类是 `SessionPersistence` 实现——存取 SessionSnapshot 信封(save/load/delete/list);
925
+ * InMemorySessionStore 是 `SessionStore` 实现——持有活 InMemorySession 实例。
926
+ * 两者都叫 "InMemory" 但服务不同契约(快照存取 vs 活对象容器)。
927
+ * 复用内核抽象基类 {@link MemoryPersistenceBase},仅声明 `extractId` +
751
928
  * 按 `SessionMetadata`(lastActiveAt/createdAt)取排序值——与 File/Remote 同款薄子类模式。
752
929
  * 内核已统一 save/load/delete/list/listPaginated 实现(M2-02,路径 A)。
753
930
  */
@@ -759,34 +936,6 @@ declare class InMemorySessionPersistence<T = unknown> extends MemoryPersistenceB
759
936
  listRecentMeta(limit: number): Promise<string[]>;
760
937
  }
761
938
  //#endregion
762
- //#region src/file-persistence.d.ts
763
- interface FileSessionPersistenceOptions {
764
- baseDir: string;
765
- }
766
- /**
767
- * @deprecated blob session persistence replaced by RelationalSessionPersistence。
768
- * 仅保留为 OTTO_SESSION_BLOB=1 逃生口;关系路径线上跑稳后删除。
769
- */
770
- declare class FileSessionPersistence<T = unknown> extends DiskPersistence<SessionSnapshot<T>> implements SessionPersistence<T> {
771
- /**
772
- * RFC-305 M3:文件后端整份快照覆写(非游标增量)→ 'snapshot'。该后端仅为
773
- * OTTO_SESSION_BLOB=1 逃生口(M63 已废弃,跑稳后删除),节流语义与 remote 一致。
774
- */
775
- readonly turnFlush: "snapshot";
776
- private readonly sessionBaseDir;
777
- private readonly sessionExtension;
778
- constructor(options: FileSessionPersistenceOptions);
779
- protected extractId(snapshot: SessionSnapshot<T>): string;
780
- /**
781
- * RFC-154 D1(blob 逃生口实现):单文件即整个 snapshot(entries+metadata 打包),无法像
782
- * SQLite 那样只读元数据列——只能先按 mtime(lastActiveAt 代理值)排序,再逐个 load() 直到
783
- * 凑够 limit 个非归档会话。比"全量 load 后排序"仍更省:mtime 排序在前,命中 limit 后即
784
- * 停止读取,不会读到排序靠后但从不会被选中的文件。该路径 M63 起标记弃用,不追求极致优化
785
- * (RFC-154 §3 D1)。
786
- */
787
- listRecentMeta(limit: number): Promise<string[]>;
788
- }
789
- //#endregion
790
939
  //#region src/session-repository.d.ts
791
940
  /**
792
941
  * SessionRepository — 会话关系化仓储(row 级,结构类型)。
@@ -797,13 +946,6 @@ declare class FileSessionPersistence<T = unknown> extends DiskPersistence<Sessio
797
946
  * 本层仍只认结构化行(workspaces / sessions / session_entries),**不依赖 SessionSnapshot 领域形状**
798
947
  * (snapshot↔rows 桥在 relational-bridge)。change_log(entry 级)属后续阶段C(gated),本层不建。
799
948
  */
800
- interface WorkspaceRow {
801
- readonly id: string;
802
- readonly name: string;
803
- readonly ownerId?: string;
804
- readonly orgId?: string;
805
- readonly createdAt: number;
806
- }
807
949
  interface SessionRow {
808
950
  readonly id: string;
809
951
  /** 分区第一维(= ws_<id>);默认 'default'。 */
@@ -837,7 +979,7 @@ interface SessionRow {
837
979
  /** 置顶分组名(RFC-158)。仅 pinned=true 时生效;缺省落入默认组。 */
838
980
  readonly pinGroup?: string;
839
981
  }
840
- type SessionEntryType = 'message' | 'compaction' | 'clear';
982
+ type SessionEntryType = 'message' | 'compaction' | 'clear' | 'compaction-lock';
841
983
  /** 追加输入:seq 由仓储发号,调用方只给 entryId(幂等键)+ 内容。 */
842
984
  interface EntryInput {
843
985
  /** 客户端生成(规则0/1 幂等键);(sessionId, entryId) 唯一。 */
@@ -853,21 +995,34 @@ interface EntryRow extends EntryInput {
853
995
  /** 会话内插入序(规则0:≠ change_log 游标)。 */
854
996
  readonly seq: number;
855
997
  }
998
+ /**
999
+ * touchSession 的 options 对象(RFC-393 M1:从 9 位置参数收拢)。
1000
+ *
1001
+ * COALESCE 语义保持不变:缺省(undefined)的字段不覆盖既有值。
1002
+ * leaf_id 悬空防护独立于 COALESCE——校验通过才写,否则走 leafPersisted=false 分支。
1003
+ */
1004
+ interface TouchSessionOptions {
1005
+ id: string;
1006
+ leafId?: string;
1007
+ lastActiveAt: number;
1008
+ title?: string;
1009
+ titleSource?: 'custom' | 'ai' | 'derived';
1010
+ config?: unknown;
1011
+ archived?: boolean;
1012
+ pinned?: boolean;
1013
+ pinGroup?: string;
1014
+ }
1015
+ /**
1016
+ * 仓储层归档能力接口(RFC-393 M2:统一能力接口模式,范式 = ReplacementHydrationCapable)。
1017
+ * 只有 SQLite 后端实现(SqliteSessionRepository)。消费方用 {@link supportsArchive} 探测。
1018
+ */
1019
+ interface ArchiveCapable {
1020
+ archiveSession(sessionId: string): Promise<string>;
1021
+ restoreArchivedSession(sessionId: string): Promise<number>;
1022
+ }
1023
+ /** 能力探测(类型守卫)。 */
1024
+ declare function supportsArchive(repo: unknown): repo is ArchiveCapable;
856
1025
  interface SessionRepository {
857
- /**
858
- * ⚠️ **DORMANT(resume/workspace 关联审计,2026-07-11 核实)**:`workspaces` 表 + 本三方法
859
- * 当前**零生产调用方**(仅 `sqlite-session-repository.test.ts` 覆盖)——对应 RFC-034 M57
860
- * 里程碑设计的"web-ui workspaces CRUD"(`POST/GET/DELETE /workspaces`),但该 HTTP 路由
861
- * 从未实现,只落地了本层 SQLite 原语。当前 local workspace 身份完全靠 `.otto/workspace.json`
862
- * marker(`@x-otto/workspace` 包)+ `sessions.workspace_key` 列分区,不依赖本表。
863
- * 参考 RFC-067 M115-04(workspace 相关代码"现实修正:不强删活包"的既有判定)与
864
- * M115-02(remote-persistence-server 删除前需产品确认)先例——**接它前先把 web-ui
865
- * workspaces CRUD 路由一并接上**,删它前先问是否已确认 web-ui 落地路线放弃该设计。
866
- * 勿孤立增删本三方法。
867
- */
868
- upsertWorkspace(ws: WorkspaceRow): Promise<void>;
869
- getWorkspace(id: string): Promise<WorkspaceRow | null>;
870
- listWorkspaces(): Promise<WorkspaceRow[]>;
871
1026
  createSession(row: SessionRow): Promise<void>;
872
1027
  /**
873
1028
  * 轻量排序 id 列表(RFC-154 D1)——只读 sessions 表的 `id`/`last_active_at`/`archived` 列,
@@ -902,7 +1057,7 @@ interface SessionRepository {
902
1057
  * sqlite-session-repository.ts 实现注释的完整事故分析)。
903
1058
  * @returns `leafPersisted` — 本次传入的 `leafId` 是否真的被采纳写入(false = 悬空,已拒绝覆盖)。
904
1059
  */
905
- touchSession(id: string, leafId: string | undefined, lastActiveAt: number, title?: string, titleSource?: 'custom' | 'ai' | 'derived', config?: unknown, archived?: boolean, pinned?: boolean, pinGroup?: string): Promise<{
1060
+ touchSession(options: TouchSessionOptions): Promise<{
906
1061
  leafPersisted: boolean;
907
1062
  }>;
908
1063
  getSession(id: string): Promise<SessionRow | null>;
@@ -1042,6 +1197,13 @@ interface SessionWriteLeaseOptions {
1042
1197
  renewEveryMs?: number;
1043
1198
  /** 是否注册 process exit 钩子同步释放(默认 true;测试注入 false 避免跨用例泄漏)。 */
1044
1199
  installExitHook?: boolean;
1200
+ /**
1201
+ * 确定性 token 生成端口(M-2)。**token 随机性是安全特性(防伪造)**,生产缺省仍走
1202
+ * `crypto.randomUUID()`;此注入仅在测试 / 确定性重放场景使用,不得在生产路径关闭随机性。
1203
+ */
1204
+ idGen?: {
1205
+ uuid(): string;
1206
+ };
1045
1207
  }
1046
1208
  declare function createSessionWriteLeaseManager(options: SessionWriteLeaseOptions): SessionWriteLeaseManager;
1047
1209
  /**
@@ -1172,6 +1334,8 @@ declare class RelationalSessionPersistence<T = unknown> implements SessionPersis
1172
1334
  declare function sessionRowFromSnapshot<T>(snapshot: SessionSnapshot<T>, workspaceKey: string): SessionRow;
1173
1335
  /** 一条 SessionEntry → 仓储输入(id/parentId/type/timestamp 进列,其余进 data)。 */
1174
1336
  declare function entryToInput<T>(e: SessionEntry<T>): EntryInput;
1337
+ /** 一行 → SessionEntry(列还原 + data 展开)。 */
1338
+ declare function rowToEntry<T>(r: EntryRow): SessionEntry<T>;
1175
1339
  /** 炸开 snapshot 到仓储行(createSession + 逐条 appendEntry,保序 + parentId/leaf/config)。 */
1176
1340
  declare function explodeSnapshot<T>(snapshot: SessionSnapshot<T>, workspaceKey: string, repo: SessionRepository): Promise<void>;
1177
1341
  /** RFC-159 D4:尾窗口加载选项。 */
@@ -1251,6 +1415,26 @@ declare const SESSION_IDLE_TIMEOUT_MS: number;
1251
1415
  declare const SESSION_MAX: number;
1252
1416
  declare const SESSION_PAGE_SIZE: number;
1253
1417
  declare const SESSION_SNAPSHOT_VERSION: number;
1418
+ /**
1419
+ * session_entries.type 白名单(RFC-393 P2 收尾:entry 类型注册表最小落地)。
1420
+ * 写侧(appendEntry)校验白名单,防 bug/外部导入写入垃圾 type 行;
1421
+ * 读侧(rowToEntry)未知 type 自然被消费方过滤(无害,无需拒绝)。
1422
+ * 未来新增 entry 类型时必须同时加到此白名单(防漂移规则:新增类型 = 白名单 + 序列化双改动)。
1423
+ */
1424
+ declare const SESSION_ENTRY_TYPES: readonly ["message", "compaction", "clear", "compaction-lock"];
1425
+ /** 类型守卫:entry type 是否在白名单内。 */
1426
+ declare function isKnownSessionEntryType(type: string): boolean;
1427
+ /**
1428
+ * 面板态持久化上限(RFC-393 M1:从 in-memory-session.ts 硬编码收拢为常量)。
1429
+ * 防止面板态无界增长;超出截断(最新在前或最新在末尾取决于面板语义)。
1430
+ */
1431
+ declare const PANEL_STATE_LIMITS: {
1432
+ /** 编辑文件面板:最新在前,max 50 条。 */readonly editedFiles: 50; /** 子代理面板:stat-only,max 100 条。 */
1433
+ readonly subagents: 100; /** 回合摘要面板:最新在末尾,max 200 条。 */
1434
+ readonly turnSummaries: 200; /** 草稿面板:max 50 条。 */
1435
+ readonly drafts: 50; /** 输入历史(@deprecated RFC-088 M4b):最近在末尾,max 100 条。 */
1436
+ readonly inputHistory: 100;
1437
+ };
1254
1438
  //#endregion
1255
1439
  //#region src/sqlite-session-repository.d.ts
1256
1440
  /**
@@ -1271,24 +1455,18 @@ interface SqliteSessionRepositoryOptions {
1271
1455
  */
1272
1456
  declare class SqliteSessionRepository implements SessionRepository {
1273
1457
  private readonly db;
1274
- private readonly archiveDir;
1458
+ /** RFC-393 P1-4:归档格式编排下沉独立类(gzip/jsonl/目录 + 幂等 append),本类只组合委托。 */
1459
+ private readonly archiveStore;
1275
1460
  /** RFC-345:内存会话(dbPath=':memory:')——归档不落盘(磁盘零写;:memory: 无跨重启需求)。 */
1276
1461
  private readonly ephemeral;
1277
1462
  constructor(options: SqliteSessionRepositoryOptions);
1278
1463
  close(): void;
1279
- /** RFC-324 D4 M4:归档——把会话的全部 entries 读出,逐行写入 .jsonl.gzgzip 压缩)。
1280
- * 不删除 entries 行(RFC-159 R4 红线),只标记 archived=1 + 复制到外部文件。
1281
- * 返回值:归档文件路径。 */
1464
+ /** RFC-324 D4 M4:归档——委托 {@link SessionArchiveStore}RFC-393 P1-4 下沉)。 */
1282
1465
  archiveSession(sessionId: string): Promise<string>;
1283
- /** RFC-324 D4 M4:恢复——从 .jsonl.gz 读回并重建 entries(RFC-159 append-forever)
1284
- * 已有的 entries 不删(幂等 append 可能产生重复,appendEntry 幂等去重可容忍),
1285
- * 仅当 DB 中的条目数 < 归档条目数时追加缺失部分。 */
1466
+ /** RFC-324 D4 M4:恢复——委托 {@link SessionArchiveStore}(RFC-393 P1-4 下沉)。 */
1286
1467
  restoreArchivedSession(sessionId: string): Promise<number>;
1287
- upsertWorkspace(ws: WorkspaceRow): Promise<void>;
1288
- getWorkspace(id: string): Promise<WorkspaceRow | null>;
1289
- listWorkspaces(): Promise<WorkspaceRow[]>;
1290
1468
  createSession(row: SessionRow): Promise<void>;
1291
- touchSession(id: string, leafId: string | undefined, lastActiveAt: number, title?: string, titleSource?: 'custom' | 'ai' | 'derived', config?: unknown, archived?: boolean, pinned?: boolean, pinGroup?: string): Promise<{
1469
+ touchSession(options: TouchSessionOptions): Promise<{
1292
1470
  leafPersisted: boolean;
1293
1471
  }>;
1294
1472
  getSession(id: string): Promise<SessionRow | null>;
@@ -1332,6 +1510,37 @@ declare class SqliteSessionRepository implements SessionRepository {
1332
1510
  private batchInQuery;
1333
1511
  }
1334
1512
  //#endregion
1513
+ //#region src/session-archive-store.d.ts
1514
+ interface SessionArchiveStoreDeps {
1515
+ /** 归档目录(与 dbPath 同级的 archives/ 子目录)。 */
1516
+ archiveDir: string;
1517
+ /** 内存会话(:memory:)——归档不落盘(磁盘零写;:memory: 无跨重启需求)。 */
1518
+ ephemeral?: boolean;
1519
+ loadEntries(sessionId: string): Promise<EntryRow[]>;
1520
+ appendEntry(sessionId: string, entry: EntryInput): Promise<number>;
1521
+ /** 标记/清除 archived 标志(只改 archived 列,不动 lastActiveAt——管理动作非活动,RFC-159 判据)。 */
1522
+ setArchived(sessionId: string, archived: boolean): void;
1523
+ }
1524
+ /**
1525
+ * 会话冷归档:entries 复制到外部 .jsonl.gz + 标 archived=1(不删除 DB 行,RFC-159 R4)。
1526
+ * 恢复:从 .jsonl.gz 读回并幂等 append 缺失部分(appendEntry 幂等去重可容忍重复)。
1527
+ */
1528
+ declare class SessionArchiveStore {
1529
+ private readonly archiveDir;
1530
+ private readonly ephemeral;
1531
+ private readonly deps;
1532
+ constructor(options: SessionArchiveStoreDeps);
1533
+ private archivePath;
1534
+ /** RFC-324 D4 M4:归档——把会话的全部 entries 读出,逐行写入 .jsonl.gz(gzip 压缩)。
1535
+ * 不删除 entries 行(RFC-159 R4 红线),只标记 archived=1 + 复制到外部文件。
1536
+ * 返回值:归档文件路径。 */
1537
+ archiveSession(sessionId: string): Promise<string>;
1538
+ /** RFC-324 D4 M4:恢复——从 .jsonl.gz 读回并重建 entries(RFC-159 append-forever)
1539
+ * 已有的 entries 不删(幂等 append 可能产生重复,appendEntry 幂等去重可容忍),
1540
+ * 仅当 DB 中的条目数 < 归档条目数时追加缺失部分。 */
1541
+ restoreArchivedSession(sessionId: string): Promise<number>;
1542
+ }
1543
+ //#endregion
1335
1544
  //#region src/remote-persistence.d.ts
1336
1545
  interface RemoteSessionPersistenceOptions {
1337
1546
  baseUrl: string;
@@ -1375,5 +1584,5 @@ declare class RemoteSessionPersistence<T = unknown> extends RemotePersistence<Se
1375
1584
  listRecentMeta(limit: number): Promise<string[]>;
1376
1585
  }
1377
1586
  //#endregion
1378
- export { type DraftEntry, type EditedFile, type EntryInput, type EntryRow, FileSessionPersistence, type FileSessionPersistenceOptions, InMemorySession, InMemorySessionPersistence, InMemorySessionStore, type LeaseInspection, type PaginationOptions, type PanelWriteOptions, type PersistedSessionConfig, type ReassembleOptions, RelationalSessionPersistence, RemotePersistenceError, RemoteSessionPersistence, type RemoteSessionPersistenceOptions, RemoteSnapshotConflictError, type ReplacementHydrationCapable, SESSION_IDLE_TIMEOUT_MS, SESSION_MAX, SESSION_MIGRATIONS, SESSION_PAGE_SIZE, SESSION_SNAPSHOT_VERSION, type Session, type SessionContext, type SessionEntry, type SessionEntryType, type SessionMetadata, type SessionPersistence, type SessionRepository, type SessionRow, type SessionSnapshot, type SessionStore, SessionWriteLeaseDeniedError, type SessionWriteLeaseManager, type SessionWriteLeaseOptions, SqliteSessionRepository, type SqliteSessionRepositoryOptions, type SubagentEntry, type TodoItem, type TurnSummaryEntry, type WorkspaceRow, createInMemorySessionStore, createNoopSessionWriteLeaseManager, createSessionWriteLeaseManager, decodeReplacementFromBlobs, encodeReplacementToBlobs, entryToInput, estimateMessageContentBytes, explodeSnapshot, hasStrippedReplacements, hashMessageContent, hydrateStrippedReplacements, reassembleSnapshot, sessionRowFromSnapshot, supportsReplacementHydration };
1587
+ export { type ArchiveCapable, type DraftEntry, type EditedFile, type EntryInput, type EntryRow, InMemorySession, InMemorySessionPersistence, InMemorySessionStore, type LeaseInspection, PANEL_STATE_LIMITS, type PaginationOptions, type PanelWriteOptions, type PersistedSessionConfig, type ReassembleOptions, RelationalSessionPersistence, RemotePersistenceError, RemoteSessionPersistence, type RemoteSessionPersistenceOptions, RemoteSnapshotConflictError, type ReplacementHydrationCapable, ResidencyPolicy, type ResidencyPolicyHost, type ResidencyTrimResult, SESSION_ENTRY_TYPES, SESSION_IDLE_TIMEOUT_MS, SESSION_MAX, SESSION_MIGRATIONS, SESSION_PAGE_SIZE, SESSION_SNAPSHOT_VERSION, type Session, SessionArchiveStore, type SessionArchiveStoreDeps, type SessionContext, type SessionEntry, type SessionEntryType, type SessionMetadata, type SessionPanelState, type SessionPersistence, type SessionRepository, type SessionRow, type SessionSnapshot, type SessionStore, SessionWriteLeaseDeniedError, type SessionWriteLeaseManager, type SessionWriteLeaseOptions, SqliteSessionRepository, type SqliteSessionRepositoryOptions, type SubagentEntry, type TodoItem, type TouchSessionOptions, type TurnSummaryEntry, createInMemorySessionStore, createNoopSessionWriteLeaseManager, createSessionWriteLeaseManager, decodeReplacementFromBlobs, encodeReplacementToBlobs, entryToInput, estimateMessageContentBytes, explodeSnapshot, hasStrippedReplacements, hashMessageContent, hydrateStrippedReplacements, isKnownSessionEntryType, reassembleSnapshot, rowToEntry, sessionRowFromSnapshot, supportsArchive, supportsReplacementHydration };
1379
1588
  //# sourceMappingURL=index.d.ts.map