DSH / Atlas
2026-08-05implementedarchitecture

Reusable Session preparation before publication

发布前可复用的 Session 准备阶段

Cold history inspection and Agent resume independently materialized the same persisted session log. For a large compressed log, each operation repeated the full read, decompression, parse, validation, freezing, and Session construction. Pagination could therefore pay the cold-read cost again, while making a history query activate an Agent would couple a read lifecycle to a live Agent with no natural retirement point.

English

Problem

Cold history inspection and Agent resume independently materialized the same persisted session log. For a large compressed log, each operation repeated the full read, decompression, parse, validation, freezing, and Session construction. Pagination could therefore pay the cold-read cost again, while making a history query activate an Agent would couple a read lifecycle to a live Agent with no natural retirement point.

Fresh creation and persisted resume also reached the same publication boundary through different construction flows. This obscured the invariant that setup must finish against one unpublished Session before that exact Session and its Agent become visible together.

Decision

SessionPreparation owns one exact unpublished Session until publication or rollback. It is a Session lifecycle object, not an Agent lifecycle or activation object. Fresh creation wraps the result of SessionStore.prepare(); persisted resume obtains a preparation from SessionPersistence.prepare().

The Agent loop consumes both forms through one setup-and-publication pipeline: it acquires the preparation, builds the private Agent context around preparation.session, awaits optional setup, publishes that exact Session and Agent, and disposes the preparation on every exit. Publication transfers the live lifecycle to the existing Session and Agent stores; SessionPreparation itself owns no Agent behavior.

This refines the publication boundary from the Agent lifecycle and ownership decision without replacing its ownership model.

Persisted preparation lifecycle

A coordinator-backed persistence implementation loads one cold source into a prepared Session. The backend transfers fresh, mutually unaliased metadata and events together with the source-qualified revision that identifies those exact values; the Session restore path validates and freezes the graphs in place instead of cloning them. The coordinator computes interrupted-turn closers and constructs the exact unpublished Session once. Its immutable header and balanced logical event log form the SessionInspection borrowed by readers, while the revision remains internal to persistence.

inspect(id, signal?) does not mutate storage. Synthetic closers exist only in the prepared in-memory view, and a torn physical tail remains untouched. Same-id callers share an in-flight cold read. Once ready, the preparation may remain in a per-coordinator LRU whose capacity defaults to five and is configurable by first-party backends. Before reusing a retained source, the coordinator reads that id's current revision; a mismatch evicts a ready source and repeats the cold materialization. A source already committing or reserved for resume remains exclusively owned, so concurrent inspection borrows that immutable view until publication or release.

prepare(id, signal?) exclusively reserves the prepared Session. It confirms the retained revision before committing any torn-tail and interrupted-turn repair, establishes the durable cursor, then returns a disposable preparation. A stale source is discarded and reloaded instead of being repaired or published. A successful repair also discards the pre-repair source and materializes the committed log again before reservation, so a newer revision is never associated with an older event graph. Another same-id preparation waits until the reservation is published or released. Publication accepts only the exact reserved Session and attaches the committed cursor without rebuilding its history. Failed setup or cancellation returns an unchanged unpublished Session to the LRU; mutation or attachment consumes the reservation.

The legacy load(id) API uses the same preparation and repair machinery, then discards its reservation and returns the immutable logical view. It remains a compatibility API, not the history-to-resume reuse path. This lifecycle extends the shared persistence coordinator while preserving the storage and recovery rules owned by the session persistence decision.

History and resume reuse

History reads use inspect(), so repeated pages borrow the same immutable prepared state without activating an Agent. A later resume uses prepare() and receives the exact Session retained by inspection; it does not read, decompress, parse, clone, validate, or freeze the complete log again.

If the durable log changes after inspection, its revision changes. The next history read or resume discards a retained ready Session and materializes the new log, so an old event graph cannot be associated with a newer snapshot revision. A source already claimed by an in-flight resume is not evicted: its exclusive owner keeps it through publication or release, and concurrent history may borrow the same immutable view.

Cold continuable-subagent access follows the same path. Descriptor authorization first inspects the child, then ctx.agents.resume() reserves and publishes the retained Session. This preserves the lifecycle and authorization rules in the continuable subagent conversation decision while removing its duplicate cold read.

Boundaries

  • readFrom() remains a detached physical-suffix API. It neither creates nor consumes a preparation, synthesizes logical closers, or joins the LRU.
  • HMR adoption keeps the live Session authoritative and reads the stored prefix directly. It may truncate a torn physical fragment but never closes the live open turn as interrupted.
  • The cache belongs to one persistence coordinator, not a process-global Session map. Live Sessions are owned by the existing stores and never occupy preparation capacity.
  • A fresh create never claims a cold persisted preparation with the same id. Persistence collisions continue to reject.
  • Third-party persistence implementations retain the abstract prepare() fallback through load(). They receive the same publication interface but gain exact-object reuse only when they override preparation.
  • Revision validation establishes freshness at the reuse and repair-commit points; it does not add cross-process writer exclusion to a backend. Retries converge after the durable log remains unchanged for one read/check round trip, so continuous external writers can delay preparation.

Verification

The shared persistence contract pins non-mutating balanced cold inspection and later repair. persistence.spec.ts and preparations.spec.ts pin same-id in-flight sharing, exact Session reuse across inspect and prepare, revision-triggered refresh before history and resume, single repair commit, exclusive reservation, release after failed setup, ready-entry LRU eviction, append rejection during reservation, and publication of only the reserved Session. Backend tests pin that full and lightweight reads use the same revision identity. Agent-loop and continuable-subagent tests pin the common publication pipeline and inspection-to-resume path across cancellation and teardown.

Alternatives considered

Activate an Agent for history reads. Rejected because pagination would keep query-only Agents live and transfer cache retirement into the Agent lifecycle.

Cache only { meta, events }. Rejected because resume would still reconstruct, validate, freeze, and copy a Session from the cached values. The exact unpublished Session is the reusable unit.

Keep a process-global Session map. Rejected because it would cross backend and runtime ownership boundaries, retain unbounded identities, and duplicate the live Session store.

Add a restore transaction or coordinator to the Agent loop. Rejected because cold reading, repair, reservation, and cursor attachment are persistence and Session concerns. The Agent loop only needs the uniform SessionPreparation ownership boundary.

Turn readFrom() into logical preparation. Rejected because watermark consumers need a detached physical suffix and, on seek-capable backends, a bounded read. Recovery balancing and whole-Session reuse have different semantics.

Consequences

One cold materialization can serve history pagination, subagent descriptor inspection, and a later resume. Ownership transfer removes redundant restoration clones, while the bounded per-coordinator LRU limits memory and avoids creating live Agents for queries. Create and resume share one publication protocol without merging Agent and Session responsibilities.

The first cold inspection now pays the complete validation and Session-construction cost and may retain that unpublished Session until eviction. Persistence must coordinate reservation, append, repair, and publication, and callers must treat inspection values as immutable borrowed state. Backends that rely on the default prepare() remain correct but do not receive the reuse optimization.

中文

问题

冷历史检查和 agent(智能体)恢复会分别实体化同一份持久会话日志。对于大型压缩日志,每次操作都会重新完整读取、解压、解析、验证、冻结并构造 Session。因此,历史分页可能反复承担冷读成本;如果改为由历史查询激活 agent,读取生命周期又会与缺少自然退出时机的实时 agent 耦合。

新建和持久化恢复也通过不同构造流程抵达相同的发布边界。这使一项关键不变量不够清楚:设置必须基于一个未发布的 Session 完成,之后系统才能同时公开这个精确 Session 及其 agent。

决策

SessionPreparation 持有一个精确的未发布 Session,直至发布或回滚。它属于 Session 生命周期,不属于 agent 生命周期或激活机制。新建流程包装 SessionStore.prepare() 的结果;持久化恢复则从 SessionPersistence.prepare() 取得准备对象。

agent loop(智能体循环)通过同一条设置与发布流水线消费这两种形式:先取得准备对象,围绕 preparation.session 构建私有 agent 上下文,等待可选设置完成,再发布该精确 Session 和 agent,并在所有退出路径上对准备对象执行 dispose(资源释放)。发布后,实时生命周期由现有 Session 与 agent 存储接管;SessionPreparation 本身不负责任何 agent 行为。

该机制细化了 agent 生命周期与所有权决策中的发布边界,但不替换其所有权模型。

持久化准备生命周期

使用协调器的持久化实现会将一个冷源加载为准备完成的 Session。后端转移新鲜、彼此无别名的元数据和事件,以及标识这些精确值的来源限定 revision;Session 恢复路径直接验证并冻结这些对象图,不再复制。协调器计算中断轮次的 closer,并且只构造一次精确的未发布 Session。其不可变 header 与已配平的逻辑事件日志构成读取方借用的 SessionInspection,revision 则保留在持久化内部。

inspect(id, signal?) 不修改存储。合成 closer 只存在于准备完成的内存视图中,撕裂的物理尾部保持不变。同 id 调用方共享进行中的冷读。准备完成后,该对象可以进入每个协调器自己的 LRU;第一方后端可配置容量,默认保留五个。协调器复用保留源之前会读取该 id 的当前 revision;如果不匹配,就淘汰处于就绪阶段的源并重新完成冷实体化。已经进入提交或为恢复而预留的源仍由其所有者独占,因此并发检查会借用该不可变视图,直至发布或释放。

prepare(id, signal?) 独占预留准备完成的 Session。它先确认保留的 revision,再提交撕裂尾部和中断轮次修复、建立持久游标,最后返回可 dispose 的准备对象。陈旧源会被丢弃并重新读取,不会参与修复或发布。修复成功后也会丢弃修复前的源,并在预留前重新实体化已提交日志,以免把较新的 revision 关联到较旧的事件对象图。同 id 的另一个准备请求会等待当前预留发布或释放。发布只接受精确的预留 Session,并直接附接已提交游标,无需重建历史。设置失败或取消时,未发生变化的未发布 Session 会返回 LRU;发生变更或完成附接后,系统会消费该预留。

存量 load(id) API 使用相同的准备和修复机制,随后丢弃其预留并返回不可变逻辑视图。它保留为兼容 API,不承担历史到恢复的复用路径。该生命周期扩展了共享持久化协调器,同时继续遵循会话持久化决策所规定的存储与恢复规则。

历史与恢复复用

历史读取使用 inspect(),因此重复分页可以借用同一份不可变准备状态,而不会激活 agent。后续恢复调用 prepare(),直接取得检查阶段保留的精确 Session;系统不会再次完整读取、解压、解析、复制、验证或冻结日志。

如果持久日志在检查后发生变化,其 revision 也会变化。下一次历史读取或恢复会丢弃保留且处于就绪阶段的 Session,并实体化新日志,因此旧事件对象图不会被关联到较新的快照 revision。已经由进行中恢复操作取得的源不会被淘汰:其独占所有者会持有它直至发布或释放,并发历史读取可以借用同一个不可变视图。

冷 continuable subagent 访问沿用同一路径。系统先检查子会话并完成 descriptor 授权,再由 ctx.agents.resume() 预留并发布保留的 Session。这样既遵循 continuable subagent 会话决策中的生命周期与授权规则,也消除了重复冷读。

边界

  • readFrom() 仍是脱离的物理后缀 API。它不会创建或消费准备对象,不会合成逻辑 closer,也不会进入 LRU。
  • HMR(热模块替换)接管继续以实时 Session 为权威,并直接读取已存储前缀。它可以截断撕裂的物理碎片,但绝不把实时开放轮次关闭为中断状态。
  • 缓存属于单个持久化协调器,而不是进程全局 Session map。实时 Session 由现有存储持有,绝不占用准备容量。
  • 新建流程绝不认领相同 id 的冷持久化准备对象。持久化冲突仍会被拒绝。
  • 第三方持久化实现继续获得通过 load() 实现的抽象 prepare() 回退。它们使用相同发布接口,但只有覆盖准备流程后才能复用精确对象。
  • Revision 校验在复用点和修复提交点建立新鲜度,但不会为后端增加跨进程 writer 排他。持久日志在一次读取与复核往返内保持不变后,重试才能收敛,因此持续的外部写入可能延迟准备。

验证

共享持久化约定规定冷检查不得修改存储且须保持配平,并覆盖后续修复。persistence.spec.tspreparations.spec.ts 覆盖同 id 进行中读取共享、检查与准备之间的精确 Session 复用、在历史读取与恢复前由 revision 触发刷新、修复只提交一次、独占预留、设置失败后释放、就绪项 LRU 淘汰、预留期间拒绝 append,以及只允许发布预留 Session。后端测试覆盖完整读取与轻量读取使用同一 revision 身份。agent loop 与 continuable subagent 测试覆盖统一发布流水线,以及取消和清理期间从检查到恢复的路径。

考虑过的替代方案

由历史读取激活 agent。 不采用,因为分页会使仅用于查询的 agent 长期保持实时状态,并把缓存退出问题转移到 agent 生命周期。

只缓存 { meta, events } 不采用,因为恢复仍需从缓存值重新构造、验证、冻结并复制 Session。真正可复用的单元是精确的未发布 Session。

维护进程全局 Session map。 不采用,因为它会跨越后端和运行时所有权边界,无界保留身份,并与实时 Session 存储重复。

在 agent loop 中增加恢复事务或协调器。 不采用,因为冷读、修复、预留和游标附接都属于持久化与 Session 职责。agent loop 只需要统一的 SessionPreparation 所有权边界。

readFrom() 改成逻辑准备流程。 不采用,因为水位消费方需要脱离的物理后缀;对于可寻址后端,还需要限制实际读取范围。恢复平衡与完整 Session 复用具有不同语义。

后果

一次冷实体化可以同时服务历史分页、subagent descriptor 检查和后续恢复。所有权转移去除了恢复阶段的冗余复制;每个协调器的有界 LRU 限制内存占用,也避免查询创建实时 agent。新建和恢复共享同一发布协议,同时保持 agent 与 Session 职责分离。

首次冷检查需要承担完整验证与 Session 构造成本,并可能保留该未发布 Session 直至淘汰。持久化层必须协调预留、append、修复和发布;调用方必须把检查结果视为借用的不可变状态。依赖默认 prepare() 的后端仍然正确,但无法获得复用优化。