# 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` 不再决定普通持久化何时开始。它仍是循环和检查点策略使用的顺序与错误观测边界，因此检查点成功仍表示在其完成前接纳的每个事件都已持久化。
