opencode 我们为什么删除了看门狗机制
本文基于 opencode-goal 插件(开源,SDK 1.18)真实实现编写。主题:旧版独立 watchdog 定时器的删除理由,以及卡死救护如何内聚进 TICK 门链。
1. 旧架构
旧版存在两套后台定时器:
- TICK(1 秒):驱动自动续推;
- 独立 watchdog(按配置时限):自动中止卡死的子代理。
watchdog 的删除(2026-08-29 TICK 重构)不是能力削减,而是职责重分配:卡死救护仍存在,但决策移入 TICK 内部。
2. 删除理由
2.1 多驱动路径 = 多故障源
watchdog 的 abort 是独立于 TICK 的派发路径。会话存在两条驱动路径(TICK 续推 + watchdog 中止)时,日志、竞态、错误处理全部翻倍。单一驱动器原则下,任何"自动"动作都应当由同一个循环决策。
2.2 计时与事实脱节
watchdog 按自身计时器计数,而会话活动由事件刷新。两者不同步时,"看似卡死、实则刚活动过"的会话会被误中止。误杀子代理的代价(上下文丢失)远高于等待。
2.3 定时器生命周期
watchdog 需要按会话启动/重置/取消,并与压缩、重启门、中止判定交错。多定时器时序难以穷举验证,边界 bug 集中于此。
2.4 与既有原则冲突
系统原则为"事件只记录状态,TICK 才做决策"。watchdog 是事件/时间驱动的兜底,违背该原则,保留即持续引入复杂度。
3. 替代设计:卡死救护内聚 TICK
删除 watchdog 后,救护逻辑成为drive_session门链的一环:
快照显示 busy/retry? → session_busy_since 记录首次出现时刻(单调 TICK 时钟) → 超过 STUCK_RESCUE_STALL_MS(默认 90 秒,env GOAL_STUCK_STALL_MS) 且期间无任何事件(逐事件刷新窗口) 且无进行中工具(tool_inflight 非空则豁免) → session.abort 一次解除卡死;每会话每停滞窗口至多一次 → 否则跳过关键性质:
- 判定基于权威快照 + 事件印记,而非独立计数器(无脱节);
- 单一时钟、单一循环、单一状态源;
- 工具豁免:长工具调用(如 5 分钟 bash)期间不误杀;
- 合法长生成持续产生
part.updated事件,自动续刷窗口,不会触发。
3.1 救护指纹归属
救护 abort 产生的错误与用户 ESC 同为MessageAbortedError/"Aborted"。救护发起时记录时间戳;指纹 5 秒窗口内到达归因救护、不暂停目标。完整判定链见《opencode 用户操作中断取消判定》。
3.2 子代理边界
- 主会话卡死:TICK 自动 abort;
- 子代理:不设自动中止(自动杀子代理的进度损失风险大于收益),由 TUI 伴侣插件提供手动入口(命令面板 → Poke session → Abort)。
4. 收益与边界
| 维度 | 删除前 | 删除后 |
|---|---|---|
| 驱动路径 | TICK + watchdog | 单一 TICK |
| 卡死判定依据 | 独立计时器(可与事实脱节) | 权威快照 + 事件窗口 |
| 定时器 | 多套交错 | 一套,dispose 一次清理 |
| 子代理救护 | 定时自动中止 | 仅手动(TUI) |
边界:若会话在 90 秒内不产生事件且无进行中工具,会被视为停滞(合法静默场景罕见,且每停滞窗口仅 abort 一次,不无限重试)。指纹归因窗口 5 秒,救护 abort 事件本地即时到达,窗口内可收。
约束要点:任何自动动作(续推、压缩、救护中止)都必须由单一循环决策,禁止为单个动作引入独立定时器或事件触发路径;卡死判定必须以权威快照与事件窗口为据,禁止以独立计数器代替。
5. 验证
goal-noprogress-test.mjs/goal-user-abort-test.mjs:停滞救护与指纹归属;goal-fault-injection-test.mjs:SDK 挂起/重放注入后救护触发且目标不暂停;goal-regression-gates-test.mjs(13 断言):门链回归。
6. 结论
删除 watchdoog 的实质:救护不是"TICK 之外的另一个看守",而是"TICK 自己决策该救什么"。单一驱动循环 + 权威状态 + 确定性门序,使卡死处理成为可验证的一环,而非散布的补丁。