DSH / Atlas
2026-08-05implementedarchitecturearchived 2026-09-04

Reusable Session preparation before publication

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

Fresh creation and persisted resume 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. Cold history inspection and Agent resume also independently materialized the same persisted session log, which this note originally answered with a persistence-

English

Problem

Fresh creation and persisted resume 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.

Cold history inspection and Agent resume also independently materialized the same persisted session log, which this note originally answered with a persistence-side prepared-Session cache; that half is superseded below.

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 reads the stored log through the session's write handle, appends interruptedTurnClosers, and wraps SessionStore.prepare(id, { seed, meta, seedSource: 'persistence' }) — the restoration branch that validates and freezes the transferred graphs in place.

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.

Superseded: the persistence-side preparation lifecycle

This note originally also gave persistence a prepare(id)/inspect(id) lifecycle: a coordinator-backed bounded LRU of cold unpublished Sessions with exclusive reservations, revision-checked reuse, and repair committed inside prepare/load, so history pagination and a later resume shared one cold materialization. The handle-based persistence seam deletes all of it: persistence exposes handles only, resume reads the log through its write handle and owns repair, and read-only observers (session-query) own their cold-Session cache keyed by the stat().revision change token. The read-reuse goal survives in that cache; the exclusive-reservation machinery does not, because the write handle's single-writer ownership is the exclusion resume actually needs. Resume pays one whole-log read through the handle where the prepared cache sometimes served a warm Session — an accepted cost recorded in the handle note.

Boundaries

  • The preparation is one disposable ownership window, not a cache: disposal is synchronous and idempotent, and publication accepts only the exact prepared Session.
  • A fresh create never claims a persisted identity implicitly. Persistence collisions continue to reject (SessionAlreadyExistsError, SessionAlreadyOwnedError).
  • Live Sessions are owned by the existing stores; preparations hold only unpublished ones.

Verification

Agent-loop tests pin the common publication pipeline across create, createAgent, and resume, including rollback on setup failure, cancellation, and teardown, and that disposal releases the write handle (reopening for write succeeds). Session-store tests pin the restoration branch's validate-and-freeze-in-place transfer.

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. This rationale still guards the session-query cold cache: observation never creates an Agent.

Cache only { meta, events }. Rejected at the time because resume would still reconstruct a Session from the cached values. Under the handle seam this is exactly what the read side does — session-query caches a cold Session per revision for reads only — while resume rebuilds from the handle read, trading the warm-Session reuse for a single write-ownership door.

Add a restore transaction or coordinator to the Agent loop. Rejected because cold reading and Session construction are persistence and Session concerns. The Agent loop only needs the uniform SessionPreparation ownership boundary; the handle seam kept that split while moving repair to the loop's resume path.

Consequences

Create and resume share one publication protocol without merging Agent and Session responsibilities, and every exit path disposes exactly one preparation. The persistence-side reuse consequences originally recorded here (shared cold materialization, LRU bounds, reservation coordination) now belong to the handle note and the session-query cache that replaced them.

中文

问题

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

冷历史检查和 agent(智能体)恢复也曾分别实体化同一份持久会话日志,本 Note 最初以持久化侧的已准备 Session 缓存回答了这一半问题;那一半已在下文中被取代。

决策

SessionPreparation 持有一个精确的未发布 Session,直至发布或回滚。它属于 Session 生命周期,不属于 agent 生命周期或激活机制。新建流程包装 SessionStore.prepare() 的结果;持久化恢复通过该会话的写句柄读取已存储的日志、追加 interruptedTurnClosers,再包装 SessionStore.prepare(id, { seed, meta, seedSource: 'persistence' })——即就地验证并冻结转移对象图的恢复分支。

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

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

已被取代:持久化侧的准备生命周期

本 Note 最初还赋予持久化一个 prepare(id)/inspect(id) 生命周期:由协调器支撑的、装有冷未发布 Session 的有界 LRU,带独占预留、按 revision 校验的复用,以及在 prepare/load 内部提交的修复,使历史分页与后续恢复共享一次冷实体化。基于句柄的持久化 seam 删除了这一切:持久化只暴露句柄,恢复通过其写句柄读取日志并自行负责修复,只读观察方(session-query)拥有自己的冷 Session 缓存,以 stat().revision 变更令牌为键。读取复用的目标在该缓存中得以延续;独占预留机制则没有延续,因为写句柄的单写者所有权正是恢复真正需要的排他手段。在已准备缓存有时能提供温 Session 的场景下,恢复要为通过句柄的一次全日志读取付出代价——这是句柄 Note 中记录的、已被接受的成本。

边界

  • 准备对象是一个可 dispose 的所有权窗口,而不是缓存:dispose 同步且幂等,发布只接受精确的已准备 Session。
  • 新建流程绝不隐式认领持久化身份。持久化冲突仍会被拒绝(SessionAlreadyExistsError、SessionAlreadyOwnedError)。
  • 实时 Session 由现有存储持有;准备对象只持有未发布的 Session。

验证

agent loop 测试覆盖 create、createAgent 与 resume 之间的统一发布流水线,包括设置失败时的回滚、取消与清理,以及 dispose 会释放写句柄(重新以写模式打开可以成功)。Session store 测试覆盖恢复分支的就地验证并冻结的所有权转移。

考虑过的替代方案

由历史读取激活 agent。 不采用,因为分页会使仅用于查询的 agent 长期保持实时状态,并把缓存退出问题转移到 agent 生命周期。该理由仍然守护着 session-query 冷缓存:观察绝不创建 agent。

只缓存 { meta, events }。 当时不采用,因为恢复仍需从缓存值重新构造 Session。在句柄 seam 下,这恰好是读取侧的做法——session-query 按 revision 为只读用途缓存一个冷 Session——而恢复则从句柄读取重建,以温 Session 复用换取唯一的写所有权之门。

在 agent loop 中增加恢复事务或协调器。 不采用,因为冷读与 Session 构造属于持久化与 Session 职责。agent loop 只需要统一的 SessionPreparation 所有权边界;句柄 seam 保留了这一分工,同时把修复移入循环的恢复路径。

后果

新建和恢复共享同一发布协议,同时保持 agent 与 Session 职责分离,且每条退出路径恰好 dispose 一个准备对象。本 Note 最初记录的持久化侧复用后果(共享冷实体化、LRU 上限、预留协调)如今归属于句柄 Note 以及取代它们的 session-query 缓存。