复核后,上一轮的主要结论成立,但清单不完整,部分表述和修复方案需要收紧。 本轮又确认了四个问题,其中一个涉及结果丢失。下面先修正 review,再给出具体实施计划。
仍未修改代码。本轮新增验证均为本地故障注入和确定性探针;上一轮全量测试基线是 2,315 passed、37 skipped。这不是对框架无遗漏的形式化证明,但现在覆盖了更多“执行成功、持久化失败、恢复运行”之间的交界。
一、对上一轮 review 的修正
| 意见 | 复核结论与需要补充的边界 |
|---|---|
| handoff 恢复丢失 | 成立,P1。 不能只增加一个目标字段:恢复后还必须维持“同轮剩余工具由原 agent 执行、首个 handoff 获胜”的语义。 |
| 摘要请求超窗 | 成立,P2。 5,133 / 4,096 是受限测试 provider 的估算复现,不是真实 tokenizer 测量。应表述为“完整请求没有纳入预算”,不能承诺修复后所有未知 endpoint 都绝不 overflow。 |
| 摘要 usage 遗漏 | 成立,P2。 预算只能限制后续调用,无法撤销已经发生的支出;provider 没报告 usage 时,也不能伪造精确费用。 |
| 初始化缺少失败事件 | 成立,P2。 适用于已经返回 RunHandle 后发生的失败。Runner.stream() 构造阶段的参数校验仍可以同步抛错。 |
| 指纹漏检改写 | 成立,但属于条件性 P2。 正常 append-only 历史不直接触发。工具结果长度被忽略是现有刻意设计,不能简单改为“对所有原始数据哈希”。 |
| token 缓存断崖 | 成立。 约 25 倍是局部微基准;不能当作整体运行提速。固定容量下也不能承诺任意长历史都是 O(new entries)。 |
| checkpoint 双重切片 | 成立,低风险优化。 复制的是列表引用,收益主要在长运行、多次保存;优先级低于正确性修复。 |
上一轮“加一个轻量上报通道”“改缓存淘汰策略”等建议还不够具体,下面的计划会补充约束与验收标准。
二、本轮补充发现
1. [P1] 删除 checkpoint 早于 Session 持久化,失败后可能丢失结果
[checkpoint.py (line 85)](/Users/neo/repo/lovia/lovia/runtime/checkpoint.py:85) 在 delete_on_success=True 时直接删除 checkpoint;[loop.py (line 432)](/Users/neo/repo/lovia/lovia/runtime/loop.py:432) 随后才写 Session。
故障注入结果:
Session.append 抛出 OSError
delete_on_success=False:
checkpoint=completed,Session 为空,可通过 replay 补写
delete_on_success=True:
checkpoint 不存在,Session 为空,无法通过 checkpoint 恢复结果重新提交相同 run ID 会变成新运行,可能重复调用模型或执行工具。这个选项的文档恰好推荐用于“结果持久化在 Session 等其他位置”的场景,因此应该优先修复。
2. [P2] checkpoint 完成写入失败,子运行 usage 会向父运行累计两次
[loop.py (line 1195)](/Users/neo/repo/lovia/lovia/runtime/loop.py:1195) 的 _finalize_run() 已累计父 usage;随后的 checkpoint completion 失败时,异常路径又累计一次。
实测子运行使用 2 tokens,父 usage 增加 4 tokens。
应修成单次运行退出时只结算一次,并明确 completed replay 的既有计费语义,避免顺便改出另一套行为。
3. [P2] handoff 图中不同 agent 同名,恢复可能静默选错对象
[resume.py (line 78)](/Users/neo/repo/lovia/lovia/runtime/resume.py:78) 用 agent 名称同时承担图遍历去重和恢复定位。
两个分支各有一个名为 worker 的不同 agent 时,snapshot 只保存 "worker"。探针中,来自左分支的 snapshot 被解析成右分支 worker。局部工具名冲突检查抓不到这种跨分支重复。
恢复不能靠遍历顺序决定执行哪个 agent。至少在启用 checkpoint 时,需要拒绝这种歧义,并让错误包含 .hint。
4. [P2] 最后一轮文本生成期间取消,运行仍然报告成功
[RunHandle.cancel() (line 109)](/Users/neo/repo/lovia/lovia/runtime/result.py:109) 承诺在下一安全点终止,但最终文本轮次没有经过后续取消检查。
实测在收到第一个 TextDelta 时调用 handle.cancel():
最后事件:RunCompleted
result():正常返回完整回答不要求协作式取消立即打断网络等待,但模型返回后、运行被标记 completed 前,应有一个明确的取消检查点。
三、详细修改计划
建议拆成 六个可独立 review 的修改批次。每批附带对应测试和文档,避免把核心逻辑与性能优化混进一个大 PR。
批次 1:修复完成结果的持久化顺序
范围:runtime/checkpoint.py、runtime/loop.py,以及 checkpoint/session 集成测试。
目标顺序为:
保存 completed checkpoint
→ 幂等追加 Session
→ 按 delete_on_success 删除 checkpoint
→ 发送成功终止事件具体工作:
将“记录完成”与“完成后清理”分开,由 loop 编排顺序。
Session 写入失败时保留 completed checkpoint。
replay 补写成功后再执行删除。
删除失败不能把 completed checkpoint 降级成可重新执行工具的状态。
单独处理无 Session、不可序列化 output 的兼容路径,保留该选项现有用途。
验收测试:
Session 首次写入失败,重试只补写,不新增模型或工具调用。
Session 已写入、checkpoint 尚未删除时重试,不产生重复 segment。
删除失败后再次运行,仍走 completed replay。
默认保留 checkpoint、无 Session、不可序列化输出均保持明确行为。
内存与 SQLite 两种 store 都验证持久化边界。
批次 2:修复 handoff 的恢复状态
范围:checkpointer.py、runtime/run_state.py、runtime/checkpoint.py、runtime/resume.py、runtime/loop.py。
具体工作:
在 checkpoint head 增加可选的 pending handoff 元数据,记录足以定位转交的稳定信息;不序列化 Agent 或回调对象。
转交工具结果与 pending 状态在同次 checkpoint append 中保存。
resume 先恢复 pending 状态,再处理原 agent 尚未完成的工具。
保留“首个 handoff 获胜”的去重行为。
目标 agent 激活失败时保留待转交状态,不能像现在一样提前清空。
目标资源和 system prompt 准备成功后,再一致地切换 active agent、transcript head 与边界。
checkpoint 模式下校验可达图:同一对象形成环允许,不同对象同名报错。
验收测试:
转交后、剩余工具前取消或超预算。
转交结果已经保存,但尚未应用转交时恢复。
目标插件初始化失败后恢复。
同一轮请求两个 handoff,恢复后仍只采用第一个。
on_handoff在已持久化完成的情况下不重复执行。A→B→A、显式 handoff 工具名、跨分支同名 agent。
老 checkpoint 缺少新增字段时可正常读取。
兼容边界:旧 checkpoint 若已经丢失 pending 信息,不能凭“Transferred to …”自然语言结果保证重建。修复也不能提供外部副作用与数据库之间的通用 exactly-once 保证。
批次 3:统一终止、取消和父 usage 结算
范围:runtime/result.py、runtime/loop.py 及生命周期测试。
具体工作:
已创建 handle 的普通运行失败,确保只出现一个
RunFailed。保留同步参数校验、
asyncio.CancelledError、消费者关闭流的不同语义。在模型输出及 usage 安全记录后、标记 completed 前检查取消。
父 usage 收敛到一个明确的结算点,成功与失败都只累计一次。
将 bootstrap、checkpoint load、Session 写入等失败纳入终止事件测试。
验收测试:
插件 setup 失败时,流包含
RunFailed,result()抛出原异常。已有
RunFailed后 producer 再抛错,不重复发送。最后一轮文本期间取消,最终不发送
RunCompleted。checkpoint completion 失败,父 usage 与子运行实际累计一致。
正常完成、工具失败、预算中断、任务取消、completed replay。
不让错误事件补发改变资源清理语义或导致
result()等待不结束。
批次 4:让摘要请求纳入预算、usage 和运行控制
范围:context/policy.py、context/stages.py、context/summarizer.py,以及 runtime 的对接处。
这是设计风险最高的一批,应保持接口增量兼容。
具体工作:
提取摘要请求构造逻辑,使预算计算与实际发送使用同一份请求结构。
扣除 system prompt、模板、旧摘要、输出预留后,再决定新增历史块。
独立摘要 provider 使用自己的窗口及估算能力,不能直接套用主模型的校准比例。
章节纠正重试同样重新检查完整请求大小。
对超大单条内容,仅在摘要输入投影中分块;完整折叠成功后才能推进对应覆盖范围。
无法可靠压缩时保留历史和原 coverage,给出明确失败原因,不以静默截断换取“成功”。
添加带默认值的 usage 上报与运行控制对接,现有自定义
Summarizer.summarize()不必更改签名。每次摘要调用、纠正重试和后续 fold 前检查取消及预算。
摘要 usage 并入总量,但不影响主模型
last_input_tokens。取消和预算异常必须穿过 stage 的 fail-open 异常处理,不能被算成普通摘要失败。
增加摘要调用 span 和简洁日志,不默认记录完整历史文本。
验收测试:
小窗口、长旧摘要、较小的独立摘要模型。
章节重试撑大请求、单条超大输入、不可拆开的工具配对。
中途失败或取消,已提交 coverage 保持配对安全。
多 fold、纠正重试的 usage 全部累计,主调用校准保持不变。
预算耗尽后不再开始下一次摘要或主模型调用。
provider 未报告 usage 时,不生成虚假的精确用量。
未知窗口保留 reactive fallback,不承诺完全消灭 overflow。
批次 5:修复历史指纹,并做两项局部性能优化
范围:context/state.py、context/tokens.py、context/compaction.py、runtime/checkpoint.py。
指纹部分:
明确哪些内容变化应使摘要失效:用户/助手文本、工具名称与参数、多模态引用等。
区分已被 marker 替代的结果和实际参与摘要的原文,避免工具输出清理触发无意义的摘要失效。
使用可复用的内容摘要,避免每轮重新编码所有大文本。
给新指纹语义增加版本;老摘要安全失效重建,尽量保留仍有效的 offload、clear 和 learned-window 状态。
缓存部分:
避免一次顺序扫描中,新 miss 淘汰本轮稍后仍要访问的缓存项。
保留容量限制、weakref 与 ID 重用防护。
不简单扩大上限,也不把普通 LRU 当作解决方案。
checkpoint 直接切新增尾部,消除整段 run 的中间列表。
验收测试与基准:
相同长度文本替换、裁剪后相同结构前缀,必须使旧摘要失效。
只 append 新内容,不使已有摘要失效。
JSON/checkpoint 往返前后指纹一致。
缓存容量以下、略超容量、两倍容量,以及多 session/provider 场景。
用
_measure调用次数做稳定断言;耗时仅作 benchmark,不设容易抖动的 CI 毫秒阈值。输出 tokens、view 与 checkpoint delta 的内容不因优化改变。
批次 6:同步 DX、文档和发布说明
修正默认水位、session meta 状态延续、复杂度等架构说明。
明确
tokens_before的既有语义;修复“context was …% full”实际展示压缩后水位的问题。若新增“本次压缩前 view tokens”,采用可选字段,保留旧字段,避免静默改变消费者含义。
记录取消安全点、摘要计费、checkpoint 删除顺序与同名 agent 限制。
公共行为变化同步
README.md、README-zh.md和相关中英文文档。版本与发布说明集中处理。当前 CI 会在合并版本更新后触发发布,因此不在每个修复 PR 中机械递增版本。
四、实施与最终验收
开始实施时遵循仓库工作流:新建 codex/… 分支,先补能失败的回归测试,再修实现;每批开 PR、获取并处理 review 反馈,通过 required checks 后才合并。
本地验证统一通过 .venv/bin/python -m … 执行 pytest、ruff、mypy;文档修改验证构建及链接,Python 3.10–3.12 兼容性由 CI 矩阵覆盖。
实施后的对抗式复核重点放在:
每个持久化边界前后中断,恢复是否选对 agent、重复执行或丢失结果。
取消/预算异常会不会被摘要的容错逻辑吞掉。
usage 是否漏算或重复累计。
旧 snapshot、自定义 ContextPolicy/Summarizer 是否仍可使用。
性能优化是否破坏工具配对、sticky 决策或原始 transcript。
优先落地批次 1–3,再处理摘要链路。 它们直接决定运行能否可靠完成、恢复和报告结果;性能优化可以随后独立交付。