# Agent Note: 将实时持久化归并到单个刷新控制器 Status: implemented [English](2026-07-23-collapse-persistence-flush-state.md) | 中文 ## 问题 持久化协调器使用彼此独立的缓冲区、初始化容器和退役容器,以及按 id 划分的操作链,表示一个活跃会话的写入生命周期。这些结构反映的是同一个事实:该 `Session` 是否仍有初始化操作或事件必须完成,之后才能释放其状态。仅由检查点触发的排空还会让每个事件都停留在易失状态,直至另一个插件请求 `session/flush`,尽管后端可以在不阻塞同步生产方的情况下开始持久化工作。 ## 决策 每个活跃的 `Session` 都有一个控制器,其中包含 `pending`、`init` 和可选的当前 `flush` promise。`session/event` 监听器将冻结的事件复制到 `pending`,并立即调度 `ensureFlush()`。活跃写入期间的调用复用同一个 promise。排空操作会对待处理事件中一个稳定的前缀生成快照,并且只在 `appendBatch` 提交后移除该前缀;写入期间接纳的事件保留在该前缀之后,并调度一个后续批次。 `session/flush` 是观测屏障。它等待初始化完成,并反复等待或启动控制器的刷新,直至当前 promise 和待处理事件均不存在。即时写入失败会被记录,但不会拒绝同步事件生产方;完整批次会保留下来,由下一次显式刷新、退役尝试或后端资源销毁重试。若该次重试仍失败,显式刷新和资源销毁仍会向调用方暴露失败。 初始化只进入现有的按 id 操作链一次,并在占有该轮执行权时调用未串行化的核心操作。该操作链与活跃控制器保持分离,因为公共 `create`、`append`、`load` 调用即使没有 `Session` 对象仍可能发生竞态,依然需要按标识串行执行。 崩溃修复仅适用于冷态标识。对于活跃标识,`load(id)` 会在等待刷新完成前,先对内存中的权威事件生成快照,再将这些事件与 `SessionState.meta`(即持久化写入实际使用的标头)一同返回;若轮次仍打开,则在不读取或修复存储的情况下拒绝该次加载。冷态加载会先在按 id 操作链内同步占用对应标识,再等待读取已存储前缀或执行修复写入;在这项占用解除前,`session/created` 发布边界会拒绝同 id 活跃会话的发布并将其回滚。HMR 接管仍由 `loadStored` 与协调器的 cwd 检查独立处理,会截断撕裂的存储,但不会闭合权威的活跃轮次。 活跃控制器映射同时也是退役注册表。退役成功时,系统排空并移除其控制器;退役失败时,控制器保留在映射中。后端资源销毁会停止接纳事件,刷新所有仍存在的控制器,等待其余按 id 操作完成,然后关闭后端。无需另设退役集合来重新发现未完成的工作。 ## 备选方案 **保留仅由检查点触发的延后写入。** 这种方式可以形成更大的批次,但会让持久性依赖另行挂载的检查点策略,并使检查点之间因崩溃而丢失数据的窗口达到最大。即时调度仍会合并同步突发事件,以及活跃写入期间到达的事件。 **在整个协调器范围内使用一个刷新 promise。** 这种挂接方式适用于单个文件,但全局 promise 会串行化互不相关的会话。每个活跃会话各有一个控制器,既能让不同会话的后端操作独立推进,又由按 id 操作链保护同一标识的操作。 **永久锁存首次即时写入错误。** 这会让后续每次刷新都得到确定的结果,却会阻止现有的资源销毁重试从暂时性存储故障中恢复。保留批次但不锁存错误,可以同时保留可观测性和重试能力。 **拒绝对所有活跃会话的加载。** 这样做很安全,但会让持久化消费方和测试无法再使用既有的闭合活跃会话快照。先生成快照再刷新,为调用提供了稳定的线性化点:刷新成功即可证明正是该快照已持久化,而活跃路径绝不调用崩溃修复。 ## 验证 - 一个针对协调器的测试会阻塞第一次追加,在该次写入期间接纳另一个事件,并在不调用 `session/flush` 的情况下观测到自动执行的第二个持久批次。 - 共享协调器契约仍覆盖内存、JSONL 和 SQLite 后端上的活跃会话接管、冲突、崩溃修复,以及会话和后端的资源释放。 - 失败和资源销毁测试会让写入失败的批次保持待处理,在关闭前重试这些批次,并证明尚在执行的控制器会延迟后端关闭。 - 共享后端契约会持久化一个仍打开的活跃轮次,证明 `load` 会拒绝且不会写入合成闭合事件,随后完成该轮次并让其所有者退役,最后重新加载完全相同的已完成轮次。 - AgentLoop 回归测试让 `resume()` 与一个仍打开的活跃轮次发生竞态,并证明原有的 agent(智能体)仍能完成该轮次并将其持久化,其间不会注入 `interrupted` 边界。 - 一个受控后端会阻塞 `loadStored`,在修复操作持有标识占用期间尝试发布同 id 会话,并证明回滚不会留下残留控制器,之后可以成功恢复一个闭合会话。 - 无所有者声明契约会为活跃 `Session` 设置不同的 `createdAt`,并证明活跃加载和之后的冷态加载均返回最初存储的标头。 ## 后果 协调器有三个长生命周期容器:持久化的标识状态、活跃会话控制器和按 id 操作链。即时写入缩短了通常情况下因崩溃而丢失数据的窗口,并移除了彼此独立的缓冲区、初始化注册表和退役注册表。与仅由检查点触发的排空相比,这种方式可能产生更多后端批次;同一轮事件循环内的突发事件和一次写入期间接纳的事件仍会合并。 `session/flush` 不再决定普通持久化何时开始。它仍是循环和检查点策略使用的顺序与错误观测边界,因此检查点成功仍表示在其完成前接纳的每个事件都已持久化。