fix: keep crash repair away from live sessions
This commit is contained in:
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write
|
||||
2026-07-23-collapse-persistence-flush-state.md: 69403fe3c2ee556cb10593fd43857e0d844242df
|
||||
2026-07-23-collapse-persistence-flush-state.zh.md: 0b38ee26b9e6273672cbc218826c0dc399158a71
|
||||
2026-07-23-collapse-persistence-flush-state.md: 9a2de00b2ad0c2417b6cdcba9cbc4f020b93037d
|
||||
2026-07-23-collapse-persistence-flush-state.zh.md: f9152989fef79efbe0afc27d256d8d3418ce84ba
|
||||
|
||||
@@ -16,6 +16,8 @@ Each live `Session` has one controller containing `pending`, `init`, and the opt
|
||||
|
||||
Initialization now enters the existing per-id operation chain once and calls the unserialized core operations while it owns that turn. The chain remains separate from the live controller because detached public `create`/`append`/`load` calls can race without a `Session` object and still require identity-level serialization.
|
||||
|
||||
Crash repair is cold-only. For a live identity, `load(id)` snapshots the authoritative in-memory header and events before awaiting their flush; it returns that durable snapshot when balanced and rejects an open turn without reading or repairing storage. A cold identity follows the stored-prefix repair path. HMR adoption remains separate through `loadLive` and truncates torn storage without closing the authoritative live turn.
|
||||
|
||||
The live-controller map is also the retirement registry. Successful retirement drains and removes its controller; failed retirement leaves it in the map. Backend teardown stops event admission, flushes every controller still present, awaits remaining per-id operations, and closes the backend. No separate retirement set is needed to rediscover unfinished work.
|
||||
|
||||
## Alternatives considered
|
||||
@@ -26,11 +28,15 @@ The live-controller map is also the retirement registry. Successful retirement d
|
||||
|
||||
**Latch the first eager error permanently.** This makes every later flush deterministic, but prevents the existing teardown retry from recovering a transient storage failure. Retaining the batch without latching the error preserves both observability and retry.
|
||||
|
||||
**Reject every live load.** This is safe but removes established balanced live snapshots used by persistence consumers and tests. Snapshot-before-flush gives the call a stable linearization point: successful flush proves exactly that snapshot is durable, while the live path never invokes crash repair.
|
||||
|
||||
## Verification
|
||||
|
||||
- A focused coordinator test gates the first append, admits another event during that write, and observes an automatic second durable batch without calling `session/flush`.
|
||||
- The shared coordinator contract still covers live adoption, collisions, crash repair, and session/backend disposal over the in-memory, JSONL, and SQLite backends.
|
||||
- Failure and teardown tests keep rejected batches pending, retry them before close, and prove an in-flight controller delays backend close.
|
||||
- The shared backend contract persists an open live turn, proves `load` rejects without writing synthetic closers, completes and retires the owner, then reloads the exact completed turn.
|
||||
- An AgentLoop regression races `resume()` against a live open turn and proves the original agent can still durably complete it without an injected `interrupted` boundary.
|
||||
|
||||
## Consequences
|
||||
|
||||
|
||||
@@ -16,6 +16,8 @@ Status: implemented
|
||||
|
||||
初始化只进入现有的按 id 操作链一次,并在占有该轮执行权时调用未串行化的核心操作。该操作链与活跃控制器保持分离,因为公共 `create`、`append`、`load` 调用即使没有 `Session` 对象仍可能发生竞态,依然需要按标识串行执行。
|
||||
|
||||
崩溃修复仅适用于冷态标识。对于活跃标识,`load(id)` 会在等待刷新完成前,先对内存中的权威标头和事件生成快照;若快照闭合,则返回这个已持久化的快照;若轮次仍打开,则在不读取或修复存储的情况下拒绝该次加载。冷态标识沿用已存储前缀的修复路径。HMR 接管仍通过 `loadLive` 独立处理,会截断撕裂的存储,但不会闭合权威的活跃轮次。
|
||||
|
||||
活跃控制器映射同时也是退役注册表。退役成功时,系统排空并移除其控制器;退役失败时,控制器保留在映射中。后端资源销毁会停止接纳事件,刷新所有仍存在的控制器,等待其余按 id 操作完成,然后关闭后端。无需另设退役集合来重新发现未完成的工作。
|
||||
|
||||
## 备选方案
|
||||
@@ -26,11 +28,15 @@ Status: implemented
|
||||
|
||||
**永久锁存首次即时写入错误。** 这会让后续每次刷新都得到确定的结果,却会阻止现有的资源销毁重试从暂时性存储故障中恢复。保留批次但不锁存错误,可以同时保留可观测性和重试能力。
|
||||
|
||||
**拒绝对所有活跃会话的加载。** 这样做很安全,但会让持久化消费方和测试无法再使用既有的闭合活跃会话快照。先生成快照再刷新,为调用提供了稳定的线性化点:刷新成功即可证明正是该快照已持久化,而活跃路径绝不调用崩溃修复。
|
||||
|
||||
## 验证
|
||||
|
||||
- 一个针对协调器的测试会阻塞第一次追加,在该次写入期间接纳另一个事件,并在不调用 `session/flush` 的情况下观测到自动执行的第二个持久批次。
|
||||
- 共享协调器契约仍覆盖内存、JSONL 和 SQLite 后端上的活跃会话接管、冲突、崩溃修复,以及会话和后端的资源释放。
|
||||
- 失败和资源销毁测试会让写入失败的批次保持待处理,在关闭前重试这些批次,并证明尚在执行的控制器会延迟后端关闭。
|
||||
- 共享后端契约会持久化一个仍打开的活跃轮次,证明 `load` 会拒绝且不会写入合成闭合事件,随后完成该轮次并让其所有者退役,最后重新加载完全相同的已完成轮次。
|
||||
- AgentLoop 回归测试让 `resume()` 与一个仍打开的活跃轮次发生竞态,并证明原有的 agent(智能体)仍能完成该轮次并将其持久化,其间不会注入 `interrupted` 边界。
|
||||
|
||||
## 后果
|
||||
|
||||
|
||||
Reference in New Issue
Block a user