The persistence seam is now create/open/stat/list returning per-session SessionHandles (read/append/flush/close); every log read and write flows through the owning handle. The seam package exports only the service and handle contracts, consumer-visible errors, and pure durable-data validation helpers; each backend owns its complete storage runtime, and the shared contract suites pin equivalent observable behavior. The backend routes published sessions' live events by id into the active write handle; agent-loop only acquires, seeds, and closes the handle. Resume appends interruptedTurnClosers through its write handle; session-query owns the revision-keyed cold cache. Legacy-only surfaces are removed in the same swap: locate/readRaw/supportsRawArtifacts, the legacy event-shape read migration, zstd torn-frame salvage, DSH_SESSION_JSONL, and hook transcript_path population; a torn final zstd frame is discarded whole; the session-list cold blank probe returns on stat metadata (eventCount derived from the last physical row, sizeBytes). The WebUI ZIP export serializes the logical log from a read handle, so both backends export identically. Refs #3245
4.6 KiB
Agent Note: 发布前可复用的 Session 准备阶段
Status: implemented
English | 中文
问题
新建和持久化恢复通过不同构造流程抵达相同的发布边界。这使一项关键不变量不够清楚:设置必须基于一个未发布的 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 缓存。