news 2026/9/24 16:25:19

Multi-Loop Collision:当 CI Sweeper 与 PR Babysitter 同时修改同一个 PR——来自 loop-engineering 的真实事故复盘与分支锁解决方案

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Multi-Loop Collision:当 CI Sweeper 与 PR Babysitter 同时修改同一个 PR——来自 loop-engineering 的真实事故复盘与分支锁解决方案
  • 人工智能
  • AI Agent
  • Agent 工作流
  • CLI
  • 研发协作
  • AI 技能
  • MCP 服务

【免费下载链接】loop-engineering

Practical patterns, starters & CLI tools for loop engineering with AI coding agents. Design systems that prompt and orchestrate agents (inspired by Addy Osmani and Boris Cherny). Includes loop-audit, loop-init, loop-cost.

项目地址:https://gitcode.com/gh_mirrors/lo/loop-engineering
点击查看免费下载

本文基于 loop-engineering 仓库中的真实生产事故记录 stories/multi-loop-collision.md 展开。它记录了两个独立调度的 AI 行动循环(CI Sweeper 与 PR Babysitter)在同一分支上各自生成互相冲突的修复提交,最终造成约 5 倍 token 消耗和 45 分钟人工解耦的完整经过。读完本文,你将掌握:行动循环为什么需要"分支锁"(branch lock)、acting_on状态约定如何在多个循环之间实现碰撞检测、以及 loop-worktree 的lock/unlock命令如何把这一约定从"靠人自觉"升级为"机械化的咨询式锁"。

事故背景:两个循环盯上了同一条分支

事故场景非常简单,却极具代表性。团队同时在同一个仓库里运行着两个具备"自动修复"能力的行动循环(L2):

循环配置职责
CI Sweeper/loop 15m,最多重试 3 次监控失败 CI,诊断并提交最小修复
PR Babysitter/loop 10m,同一仓库盯守活跃 PR 的 CI、review、rebase 与合并

两者共享的目标是fix/auth-token-refresh分支,对应 PR #318。按照 patterns/ci-sweeper.md 的定位,CI Sweeper 负责"对 main 或活跃分支上的失败 CI 快速响应";而按照 patterns/pr-babysitter.md 的定位,PR Babysitter 负责"把 PR 从开题推到可合并状态"。当一条 PR 的分支上同时出现失败的 CI 测试时,两个循环的职责范围天然重叠——这正是事故的温床。

什么在正常工作:两个循环的判断都没错

复盘首先要诚实记录做得对的部分。根据 stories/multi-loop-collision.md 的记录:

  • 两个循环都独立且正确地定位到了失败的auth测试test_refresh_token_expiry一类的鉴权令牌刷新用例),说明各自的 triage 分类逻辑工作正常;
  • 周二早上审计时,两个循环的状态文件都如实记录了这次重叠ci-sweeper-state.mdpr-babysitter-state.md),说明状态文件作为可观测性载体是有效的。

问题从来不是"循环不会发现问题",而是"两个循环没有互相感知"。

什么坏了:两套互相冲突的修复提交

事故链条如下,时间戳来自原文档:

  1. 14:02CI Sweeper 在fix/auth-token-refresh分支上拉起一个 worktree,开始实施它认为的最小修复;
  2. 14:07PR Babysitter 在同一个 PR上拉起了另一个不同的最小修复方案;
  3. 结果是两个提交、两种互斥的修复思路同时出现在 PR 上,reviewer 完全被搞糊涂了,不知道哪个是"真正的修复";
  4. 更糟的是成本失控:该 PR 的合并 token 消耗约 40 万 token,而正常水平约 8 万 token——一个 PR 花了平时 5 倍的预算。

这两个循环各自遵循了自己的模式文档:CI Sweeper 打开 worktree、起草修复、等待 verifier 与人工确认(见 patterns/ci-sweeper.md 的"Typical Cycle");PR Babysitter 对失败检查项拉起minimal-fix子代理(见 patterns/pr-babysitter.md 的典型循环)。单个循环看,每一步都是规范动作;整体看,缺了一道"互斥"检查

量化指标

原文档给出了完整的事故度量表:

指标数值
重复修复尝试2 次
人工解耦耗时45 分钟
根因缺少acting_on碰撞检查

结合 patterns/ci-sweeper.md 的成本画像可以进一步理解"为什么这么贵":一次典型的 L2 修复尝试(worktree + implementer + verifier)本身就接近 20 万 token,两次独立修复叠加后,token 消耗落到 40 万量级完全符合成本模型的推算;而 45 分钟的人工解耦时间,本质上是把两个 AI 循环各自产生的错误上下文重新对齐的代价。

教训:行动循环需要状态层面的"分支锁"

原文档给出的教训只有一句话,但它是整套 multi-loop 协调机制的核心:

Action loops need abranch lockin state.

行动循环(会实际改动分支、提交代码的循环,L2 及以上)必须在状态文件中声明自己"正在处理哪条分支/哪个 PR",即写入acting_on: branch-or-pr-id。随后这一约定被固化为 docs/multi-loop.md 的碰撞检测流程:

  1. 每个行动循环在状态文件中写入acting_on: 分支名或 PR id
  2. 在拉起 worktree 修复之前,先读取所有其他模式的状态文件
  3. 如果发现另一个循环的acting_on与自己的目标匹配,则跳过本次执行,并把跳过原因记入 loop-run-log.md

落到这次事故的具体职责划分是:CI Sweeper 拥有"红色 CI"的处理权;PR Babysitter 在读到ci-sweeper-state.md显示某个 PR 已被acting_on标记时,跳过对该 PR 的修复动作。

多循环协调的五条原则

docs/multi-loop.md 把上述事故上升为通用原则。在一个仓库里跑多个循环是常态,但没有边界的多循环就是"循环互殴"。协调原则如下:

  1. 每条分支一个属主(One owner per branch)——最多只有一个循环在一小时内改动某条分支;
  2. 状态文件相互分离——STATE.md用于 triage(优先级与人工收件箱),行动循环使用各自专属的状态文件;
  3. Triage 只报告,行动循环才执行——Daily Triage 在 L1 阶段永不与 CI Sweeper 的修复竞争;
  4. 共享 denylist——把同一份路径 denylist 复制进每个LOOP.md
  5. 聚合 token 预算——参见 templates/loop-budget.md.template。

推荐的仓库根目录状态布局如下(来自原文档):

STATE.md # Daily Triage(优先级、人工收件箱) pr-babysitter-state.md # PR 观察者 ci-sweeper-state.md # 活跃 CI 失败 + 尝试次数 dependency-sweeper-state.md # 进行中的依赖更新 post-merge-state.md # 合入后清理积压 loop-run-log.md # 只追加的可观测性日志

循环冲突时的优先级栈

当多个循环目标重叠时,docs/multi-loop.md 定义了明确的优先级栈,冲突时由高优先级循环先执行:

优先级循环理由
1CI Sweeper红色 main 阻塞一切
2PR Babysitter活跃 PR 对时间敏感
3Dependency SweeperCI 红色时暂停
4Post-Merge Cleanup非高峰、紧急度最低
5Daily TriageL1 只出报告,负责调度其他循环

调度协调应在根目录LOOP.md中显式文档化。原文档给出了可直接复制的示例:

## Multi-loop schedule - CI Sweeper: /loop 15m (active hours) - PR Babysitter: /loop 10m (active hours, skip if CI Sweeper acting on same PR) - Daily Triage: /loop 1d 08:00 - Dependency Sweeper: /loop 6h (skip if main CI red) - Post-Merge: /loop 1d 22:00

注意 PR Babysitter 一行的括号注释"skip if CI Sweeper acting on same PR"——这就是本次事故后补上的调度契约,对应本仓库实际运行的多循环调度表见 LOOP.md 的 "Multi-loop coordination" 一节。

从状态约定到机械化锁:loop-worktree lock/unlock

纯靠"状态文件 + 人工阅读"的碰撞检测有一个致命弱点:它依赖循环的控制脚本自觉去读别人的状态文件。仓库给出的更强方案是用工具把约定机械化——tools/loop-worktree 的lock/unlock命令把docs/multi-loop.mdacting_on约定实现为一个"咨询式锁"(advisory lock):

loop-worktree lock --paths package.json,package-lock.json --owner dependency-sweeper --ttl 6h \ || exit 2 # 另一个属主持有重叠路径的锁 —— 跳过本次运行 loop-worktree create --run-id "$RUN_ID" --pattern dependency-sweeper # ... 执行修复工作 ... loop-worktree unlock --owner dependency-sweeper

控制脚本在拉起 worktree之前执行loop-worktree lock,工作完成后执行loop-worktree unlock。关键设计是:loop-worktree create本身不检查锁,两者靠控制脚本中的配对约定来保持同步——这正与loop-context --checkloop-worktree mark --status escalated的配对方式一致(见 docs/multi-loop.md 与 tools/loop-worktree/README.md)。同样的约定也通过--lock-paths选项存在于 tools/loop-sandbox(一次性沙箱 agent 运行也属于可能撞车的控制脚本),但它是可选开启的——不带--lock-pathsloop-sandbox run不受锁保护。

锁的源码级原理:路径重叠、TTL、等待与死锁检测

在 tools/loop-worktree/src/lock.ts 中可以读到这个锁的完整实现,几个关键设计值得注意:

  • 锁文件即状态:每个属主一个 JSON 文件,存放在.loop-worktrees/locks/<owner>.json,字段包括ownerpathslockedAt与可选的expiresAt(不传--ttl则永不过期,必须显式unlock,见 lock.ts)。这个文件的物理存在形式,就是acting_on约定的机械化版本——只不过它锁定的是路径 glob而非分支名,因此还能捕捉跨模式的冲突(比如 CI Sweeper 与 Dependency Sweeper 同时要改package.json)。
  • 按路径段比较的重叠判断pathsOverlap把两个 glob 按/切分成段,逐段比较;通配段(*/**)与该位置任意内容兼容,因此src/**src/foo.ts重叠,而docs/apidocs/apidocs.md重叠(不同字面段,不是简单的前缀匹配)。实现注释明确说明这是"故意简单的咨询式锁,而非完整 glob 引擎"(lock.ts)。
  • 跨进程互斥lock的"检查-再写入"临界区通过一个独占创建的 mutex 文件(.loop-worktrees/locks/.mutex)串行化,5 秒超时,避免两个同时发起的lock调用都在对方写入前通过重叠检查——这正是本功能要防的竞态本身(lock.ts)。
  • 等待与死锁检测:带--wait 15m时,锁被占用会进入等待队列并写入.wait.json;等待图一旦成环立即抛出Deadlock detected: A -> B -> A(lock.ts),不会无限挂起。
  • 过期清理loop-worktree locks --sweep报告过期锁,加--force才删除;孤立锁(属主崩溃未解锁)会被显式暴露而非静默忽略,与gc命令"默认只报告"的仓库惯例一致(lock.ts)。

这些行为都有测试用例背书。tools/loop-worktree/test/lock.test.mjs 覆盖了:路径段边界(docs/apivsdocs/apidocs.md不重叠)、相同属主重复加锁会替换而非叠加、过期锁不再阻塞新锁、非法--owner(含路径分隔符、可逃逸锁目录)被拒绝、并发lock竞争时恰好只有一个获胜,以及--wait排队与死锁环检测(如Deadlock detected: B -> A -> B)。

落地示例:把碰撞检测写进两个循环的状态文件

对于不引入锁工具的场景,原文档与模式文档给出了纯状态文件的做法。

CI Sweeper(patterns/ci-sweeper.md)在ci-sweeper-state.md中跟踪:commit SHA、失败 job、尝试次数、worktree/PR 链接、结果。将acting_on写入后,形如:

## CI Sweeper — Active Failures Last run: 2026-06-09 14:30 UTC ### fix/auth-token-refresh (PR #318) — acting_on - Job: test-auth - Failure: AssertionError in test_refresh_token_expiry - Attempts: 1/3 - Last action: Minimal fix proposed in worktree fix/ci-auth-refresh - Status: Waiting for verifier + human

PR Babysitter(patterns/pr-babysitter.md)在pr-babysitter-state.md中记录每个被盯守 PR 的状态,并在读到ci-sweeper-state.md中对应acting_on标记后跳过修复:

- #318 (fix/auth-token-refresh) Checks: test-auth FAILING (owned by CI Sweeper — skipping fix) Last action: no-op, logged skip to loop-run-log.md

每次跳过都要写入 loop-run-log.md(只追加、按 run 一条 JSON,字段含patternoutcometokens_estimate),保证"这次为什么没动手"对人工完全透明。对于无法自动判定属主的歧义情况,docs/multi-loop.md 建议在STATE.md中开一个共享的"Human Inbox":

## Human Inbox (ambiguous / cross-loop) - [ ] PR #42: CI Sweeper and PR Babysitter both flagged — human pick owner

同类事故佐证:优先级锁与预检

PR #318 并非孤例。仓库里的姊妹篇事故 stories/dependency-vs-ci-sweeper-collision.md 记录了一次跨模式碰撞:CI Sweeper 正在 main 上修复回归时,Dependency Sweeper 把一次"安全"的 minor 依赖升级直接合入红色 main,引入传递性回归,CI Sweeper 在环境已经变化的情况下继续修原 bug,一小时内烧掉约 150 万 token,且两个循环同时写loop-run-log.md导致 git push 冲突。它的教训与 PR #318 互补:高优先级修复活跃时,低优先级变更循环不得执行。据此 docs/multi-loop.md 补上了优先级栈,Dependency Sweeper 现在运行预检——先读ci-sweeper-state.md确认 main CI 是否绿色,非绿则跳过并在 2 小时后重试;同时用错峰的 cron 调度避免状态文件写冲突。

两起事故合起来给出完整的防碰撞策略:状态层的acting_on声明 + 工具层的咨询式锁 + 优先级栈 + 预检(pre-flight)+ 错峰调度

实操清单:给仓库新增一个行动循环前

结合原文档与本次复盘,部署任何新的 L2 行动循环前建议逐项核对:

  1. 声明分支属主:状态文件写入acting_on: branch-or-pr-id,行动前读取所有其他模式的状态文件;
  2. 用锁机械化约定:控制脚本在createloop-worktree lock --paths <globs> --owner <pattern>,完成后unlock,冲突时exit 2跳过并记入loop-run-log.md
  3. 遵守优先级栈:确认自己的循环在冲突时的优先级(CI Sweeper > PR Babysitter > Dependency Sweeper > Post-Merge Cleanup > Daily Triage);
  4. 设置预算与熔断:行动循环要配loop-guard熔断(loop-context --check)与尝试上限,防止同一条失败反复重试烧 token(成本画像与每日上限见 patterns/ci-sweeper.md 与 patterns/pr-babysitter.md,安全细则见 docs/safety.md);
  5. 共享 denylist 与聚合预算:把同一份路径 denylist 复制到每个LOOP.md,token 预算按 templates/loop-budget.md.template 聚合;
  6. 保持可观测:跳过、等待、升级都要落 loop-run-log.md,歧义冲突进STATE.md的 Human Inbox。

参考

  • 事故原始记录:stories/multi-loop-collision.md
  • 协调规范:docs/multi-loop.md
  • 工具实现:tools/loop-worktree(lock.ts、cli.ts、lock.test.mjs)
  • 循环模式:patterns/ci-sweeper.md、patterns/pr-babysitter.md
  • 同类事故:stories/dependency-vs-ci-sweeper-collision.md
  • 本仓库实际调度与优先级:LOOP.md
  • 人工智能
  • AI Agent
  • Agent 工作流
  • CLI
  • 研发协作
  • AI 技能
  • MCP 服务

【免费下载链接】loop-engineering

Practical patterns, starters & CLI tools for loop engineering with AI coding agents. Design systems that prompt and orchestrate agents (inspired by Addy Osmani and Boris Cherny). Includes loop-audit, loop-init, loop-cost.

项目地址:https://gitcode.com/gh_mirrors/lo/loop-engineering
点击查看免费下载

相关推荐

上一篇:Buzz:如何在本地实现完全离线的专业级语音转文字?
下一篇:探索Glean:Facebook Incubator中的高效数据收集框架

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/24 16:20:55

【Dv2Admin】mounted实现定时请求和消息通知

在现代应用开发中,消息通知功能在提升用户体验中发挥着至关重要的作用,特别是当用户有未读消息时,如何及时提醒用户成为一个挑战。本文将通过实际的代码示例和应用场景,介绍如何在Vue.js项目中集成一个智能的消息通知系统,使用户能够周期性地收到未读消息提醒,并通过点击…

作者头像 李华