Truncate interrupted final turns on load
加载时截断被中断的最终轮次
The current persistence contract preserves a final turn that was durably written but never closed. On load, `interruptedTurnClosers()` scans the tail, synthesizes error `tool/result` events for unanswered tool calls, appends a `step/end` when a step is open, appends `turn/end { kind: 'interrupted' }`, and asks the backend to durably commit that repair. The coordinator, JSONL backend, SQLite backend, session event voc
English
Problem
The current persistence contract preserves a final turn that was durably written but never closed. On load, interruptedTurnClosers() scans the tail, synthesizes error tool/result events for unanswered tool calls, appends a step/end when a step is open, appends turn/end { kind: 'interrupted' }, and asks the backend to durably commit that repair. The coordinator, JSONL backend, SQLite backend, session event vocabulary, invariants, docs, and tests all model this synthetic close path.
This is a lot of machinery to preserve partial work from the last crashed turn. It also invents events that never happened. A synthetic tool result is useful because it makes provider history valid, but it also means the resumed log contains model-visible text that no tool produced. The current design optimizes for maximum tail preservation before there is a released product or a real resume UX that proves partial-turn recovery matters.
Proposal
On load, keep only the last completed turn. A backend still tolerates and truncates a torn final record, but if the parsed durable prefix ends after an open turn/start, the canonical repair is to drop every event after the previous turn/end. No synthetic tool/result, no synthetic step/end, no turn/end { interrupted }, and no interrupted turn-end reason.
This makes the persisted turn boundary simple: a completed turn/end is the checkpoint. Anything after the last checkpoint is crash tail. The next prompt resumes from the last known-valid provider transcript, not from a partially reconstructed final turn.
Acceptance criteria
TurnEndReasonMapdrops theinterruptedvariant.interruptedTurnClosers()and its tests disappear.- The persistence coordinator's repair hook truncates backend-specific torn/open tail state without appending closers.
- Session persistence docs say load returns the last completed turn, plus no partial final turn.
- Snapshot and contract tests update together with the behavior they pin.
- The session format version and recorded fixtures are refreshed; non-current stored logs are rejected per the pre-release format policy, with no migration path.
What we give up
A crash can lose real work from the final turn: assistant text, tool calls, and tool output appended after the previous turn/end. That is the deliberate simplification. The product is unreleased, the final-turn recovery semantics are not user-proven, and a clean completed-turn checkpoint is much easier to explain, test, and implement. A future "recover partial crashed work" feature should be designed as an explicit user-facing recovery view, not as synthetic events silently inserted into the canonical transcript.
Related
This is a direct simplification of session persistence and the historical universal turn-enclosure rule. It also removes much of the motivation for durable step boundary events, making drop durable step boundary events smaller.
<!-- agent-note-format: alternatives-not-recorded (pre-format Agent Note) -->中文
问题
当前的持久化约定会保留已持久写入但从未关闭的最终轮次。加载时,interruptedTurnClosers() 扫描尾部,为未应答的工具调用合成 error tool/result 事件,在步骤处于打开状态时追加 step/end,追加 turn/end { kind: 'interrupted' },并要求后端持久提交这次修复。协调器、JSONL 后端、SQLite 后端、会话事件词汇、不变式、文档和测试都对这条合成关闭路径进行了建模。
这是一套庞大的机制,只为保留上次崩溃轮次中的部分工作。它还会凭空创造从未发生过的事件。合成的工具结果虽然有用(因为它使提供方历史保持合法),但也意味着恢复后的日志中包含了模型可见、却并非任何工具产出的文本。当前设计在尚无已发布产品、也没有真实恢复 UX 来证明部分轮次恢复确有价值的情况下,就以最大限度保留尾部为优化目标。
提案
加载时只保留最后一个已完成的轮次。后端仍然容忍并截断撕裂的最终记录,但如果解析出的持久前缀在 turn/start 之后仍有轮次未关闭,规范的修复方式是丢弃上一个 turn/end 之后的所有事件。不合成 tool/result,不合成 step/end,不追加 turn/end { interrupted },也不引入 interrupted 轮次结束原因。
这使持久化的轮次边界变得简单:一个已完成的 turn/end 就是检查点。最后一个检查点之后的内容都是崩溃尾部。下一次提示词从最后一个已知合法的提供方 transcript(文本记录)恢复,而不是从部分重建的最终轮次恢复。
验收标准
TurnEndReasonMap移除interrupted变体。interruptedTurnClosers()及其测试删除。- 持久化协调器的修复钩子截断后端特有的撕裂或未关闭的尾部状态,不追加关闭事件。
- 会话持久化文档说明加载返回最后一个已完成的轮次,不包含部分最终轮次。
- 快照与约定测试随其所固定的行为一同更新。
- 会话格式版本与记录的 fixture(测试前置数据)刷新;按预发布格式策略,非当前版本的存储日志被拒绝,不提供迁移路径。
放弃的内容
崩溃可能丢失最终轮次中的真实工作:上一个 turn/end 之后追加的助手文本、工具调用和工具输出。这是有意为之的简化。产品尚未发布,最终轮次恢复的语义未经用户验证,而一个干净的「已完成轮次即检查点」模型在解释、测试和实现上都容易得多。未来若需「恢复部分崩溃工作」功能,应设计为面向用户的显式恢复视图,而非静默插入规范 transcript 的合成事件。
相关
本提案是对会话持久化与历史上的通用轮次封闭规则的直接简化。它还移除了持久化步骤边界事件的大部分动机,使移除持久化步骤边界事件的改动更小。
<!-- agent-note-format: alternatives-not-recorded (pre-format Agent Note) -->