一次自动修复协议的调整:没完成,不等于做不出来。

1. 换个模型,有时只是换个人重新排队⌗

设想一个自动修复任务:代码已经改好,针对问题的测试也已准备好,接下来需要等待测试资源。轮次结束了,最终验证却没完成。

调度器看见一个失败状态,启动下一个候选模型。新模型重新读问题、重新定位、重新实现,然后走到同一条资源队列前。

看起来系统在积极尝试,实际上只是把一次执行阻塞包装成了模型选拔。

这里至少混着三个不同判断:

观察到的事实能得出的结论不能直接得出的结论
测试环境不可用当前无法完成验证方案错误、模型无能
落地检查拒绝提交当前提交尚不能落地原作者已无法修正
一轮任务结束但没有交付交付协议尚未闭环候选方案应该被淘汰

这次改动的重点不是让重试更勤快,而是让系统不再把这三件事误记成同一件事。

恢复执行和更换候选,应该是两条控制流。

2. 「还是同一个模型」不够⌗

改动前,环境故障已经有特殊待遇:记录故障原因,退避后由同一个模型再试。

问题在于,再试调用的是启动新修复的路径。保住模型名称,没有保住原任务及其工作现场。

新实现保留的是一组身份:

同一修复 run
    ├── 同一任务线程及其历史
    ├── 同一模型
    ├── 同一 worktree
    └── 同一分支与已有提交

原 run 保持 running,对应问题保持 fixing。等待恢复通过动作记录中的 pendingResume 表达,而不是先把修复结案,再启动一份相似任务。

这很像编译失败之后继续修改原工程,而不是再找一位工程师,让他从需求说明重新建项目。

路径新建修复任务原现场是否是继续工作的起点
旧的环境重试是不再直接续跑原任务
新的原地续跑否是,线程、工作区、分支一起保留
明确放弃后的候选交棒是进入下一个候选的修复流程

差别不是命名。已有代码、排查历史和验证进度,都是修复中的资产。

3. 失败日志之外,还需要一个继续执行的协议⌗

源码将两条路径区分为两种意图。

下面是对状态含义的简化表示,不是完整数据结构:

执行中断 / 环境失败 / 落地拒绝
    pendingResume = true
    pendingNext   = false
    → 原任务继续

明确报告无法完成候选方案
    pendingNext   = true
    retryMode     = "next-model"
    → 下一个候选

显式拒绝任务也沿用候选交棒策略。相反,任务仅仅结束、停止,或者没有提交最终报告,不再自动证明应该换模型。

续跑时,系统不是只发一句「请重试」。消息包含:

  • 原修复身份、工作区和分支;
  • 上次为什么中断;
  • 已记录的诊断信息;
  • 保留现有实现、从验证现场继续的要求;
  • 再次提交仍使用原修复身份的要求。

若失败原因是合并冲突,则要求在原分支合入最新主线并解决冲突。

这比「失败了,换个模型看看」多了一项责任划分:落地通道负责给出拒绝证据,原修复任务先负责消化证据。红灯是反馈,不是自动解雇通知。

还有一个容易遗漏的细节:旧实现保存落地失败步骤时,将每一步整理成短字符串,并截取诊断内容。新实现保留失败步骤对象,再将其送回原任务。只有「验证失败」四个字的反馈,不能指望模型凭空知道该从哪里接着修。

4. 不要让清理器把等待中的任务扫掉⌗

只改续跑入口不够。自动化系统里往往还有另一位勤劳同事:清理超时任务的生命周期扫描器。

如果它只看最初启动时间,任务等待环境恢复之后,刚被唤醒就可能再次被判定为陈旧。

这次改动让相关扫描认识续跑状态:

  1. 正在等待续跑的修复,不按普通失联任务处理。
  2. 已恢复执行的修复,超时判断参考最近的续跑记录时间,而不只看最初启动时间。
  3. 一个失联修复被识别为基础设施中断时,先排入原任务续跑,而不是直接结束并另开修复。

调度侧也补上对应约束:原任务还活跃时不重复续跑,同一运行实例中重叠的推进调用不重复派发。人工暂停的记录不会被自动调度重新激活。

旧记录还有一个保守处理:只有旧式 pendingNext、没有新协议 retryMode 的记录,不会被分钟调度器直接当成换模型指令。

恢复能力不是增加一个 resume 函数,而是让启动、回收、暂停和失败记账对同一份状态达成一致。

5. 环境没好,就先别叫醒模型⌗

旧机制使用退避时间;新路径在基础设施或测试资源故障后,先做环境就绪探测。

探测失败,继续等待,不启动新的模型轮次。之后的分钟调度再检查。

这不是保证下一轮测试一定成功。生产接线中的探测调用远端测试环境探针;它只提供一个就绪信号,实际资源等待仍由原任务承担。合并冲突也不靠这个探针解决,而是直接交回原分支处理。

不过,它避免了一个明显浪费:让模型消耗一轮上下文,只为了再次得知环境依然不可用。

续跑也不是无限的。源码中的 FIX_RESUME_LIMIT 为 3,计数针对实际发起的这类续跑;环境探测未通过,不会提前消耗次数。

达到上限后,修复退回分诊,保留工作区和证据,不自动进入下一个模型。原任务身份缺失时,同样保留现场交回分诊,而不是悄悄从头开工。

三次是本实现的策略值,不是可靠性的理论常数。它表达的是一个取舍:允许有限的原地恢复,但不让自动调度无限占着修复;次数耗尽也不能被偷换成「模型能力已经被证伪」。

6. 验证分工,不是「所有测试永远只跑一次」⌗

这次改动还收拢了验证职责。修复任务的提示明确要求:

  • 运行复现问题的明确测试文件;
  • 运行直接修改模块的测试;
  • 不在工作区重复运行整套测试和工作区类型检查;
  • 最终验证交给落地通道。

这解决的是重复劳动,不是降低验收标准。

修复任务
    修改 → 针对性测试 → 提交
                         │
                         ▼
                    落地通道
                         │
              失败证据 ──┘
                  │
                  ▼
              原任务继续修

如果落地验证暴露新错误,修复代码后仍然需要重新验证。因此,「只验一次」不应理解成某个测试进程只能启动一次,更不意味着跳过回归检查。

真正值得保留的规则是:不要让每一层都为了自证认真,再把同一套昂贵检查完整跑一遍。

同时,修复任务的这部分范围约束是通过提示传达的,不是某种能禁止一切额外测试命令的执行沙箱。调度机制和模型遵循指令,仍是两种不同保证。

7. 测试证明了什么⌗

以下依据是该提交中的测试代码断言,写作期间没有重新执行测试,也没有生产性能测量。

测试场景检查的性质
落地失败,返回较长诊断,再次提交诊断进入原任务消息;两次提交使用同一分支,最终可转为解决
重复收到同一落地失败记录不重复排入续跑;旧失败也不干扰后续提交
冲突与重叠推进调用原分支合入主线;重叠调用只派发一次续跑
测试环境未就绪不发起续跑任务
环境恢复后多次续跑只有一份初始任务;续跑始终使用同一修复身份与工作区
达到续跑上限不自动切换候选;已有提交仍可从工作区读到
等待很久后恢复,再做生命周期扫描不因最初启动时间久远而立即误杀
暂停记录与旧协议记录分钟调度不会将其自动重新启动
明确报告无法完成下一候选模型仍正常接手

最后一行同样重要。新设计不是「永远不换模型」,而是要求换模型有对应证据。

测试使用可控的任务运行与续跑依赖,能检查状态、调用次数、消息和分支关系;它们不能证明真实模型一定会正确继承全部思路,也没有证明跨进程故障下的端到端恰好执行一次。

因此,可以说这次改动减少了机制上不必要的重新开工,不能据此宣称节省了多少 token、提高了多少成功率。没有测量,就别给收益编一个漂亮百分比。

8. 别把调度问题写进模型成绩单⌗

「失败就重试,再失败就换模型」很容易实现,也很容易把系统自身的问题藏起来。

真正的进步是把两个轴拆开:

  • 执行轴:环境是否可用、任务是否中断、提交是否进入下一阶段;
  • 求解轴:当前候选是否仍能修正方案、是否明确放弃。

执行轴暂时走不动,优先保留现场;求解轴明确交棒,再更换候选。

保留原任务也有代价:它会继续携带旧上下文,原作者可能困在原先的思路里。这正是仍然保留显式交棒和有限续跑的原因。恢复不是无限信任原方案,而是先给它处理新证据的机会。

自动修复不缺另起炉灶的能力。缺的是知道什么时候根本不该另起炉灶。


源码依据:提交 b08cce8 的 src/improve/autofix.ts、src/bootstrap/improve.ts、src/improve/triage.ts 与 src/stability/immediate.ts。测试依据:src/improve/autofix.test.ts、src/improve/fix-wait.test.ts 与 src/stability/immediate.test.ts。