作者设计了一套夜间批跑框架,为无值守 Agent 配备 claim 心跳机制、工作树隔离、始终释放锁等五项铁律,避免多 Agent 并发冲突与资源泄漏。
每天晚上,一个定时任务会启动,读取我各个项目中待处理工作的队列,然后为每个目标启动一个无头 agent 会话来消耗队列中的任务。早上我边喝咖啡边读一份报告。这就是大致思路,而调度本身确实是最简单的部分——任何 cron 行都能做到。真正需要设计的是周围的策略,因为一个拥有写权限的无人值守 agent 是一个非常快速的方式来让你一觉醒来面对一团糟。五条定律由此产生。
无头会话无法自行维持秩序——它不会维护锁,不会感知兄弟节点,也不会在退出时清理。因此 runner 代表它完成所有这些工作。它在生成 child 之前获取目标的声明权,在 child 运行期间定时刷新心跳,并在最后释放该声明——无论正常退出还是崩溃。"礼貌"不再需要由 child 负责,这意味着它不再只是一种期望。
runner: take the claim on the target
│
├─▶ spawn the child, headless, in its own worktree
│ │ heartbeat renewed on a timer while it runs
│ ▼
│ child commits, or finds nothing, or dies, or hits the watchdog
│ │
└───────┴─▶ release the claim, always: exit, crash and kill alike
有五种情况会让 runner 放弃目标,每一种都有日志记录的原因:
夜晚不与白天竞争。竞争是早晨的事。
Children 向自己的分支提交工作,而在启用推送的地方,runner 将当晚的工作推送到一个独立的每日分支,然后恢复之前检出的分支。我像审查同事的分支一样审查消耗的工作,而不是等到发现它已被合并。
从根本上说,都是天花板。每个夜晚的 child 数量上限、重试轮数上限、每个 child 的看门狗。我在第一次无人值守运行之前就设置好这些,而不是在第一次失控之后——我建议按这个顺序来。
这是我最坚决维护的规则。即使没有任何事情可做,报告也会被写入。这听起来很琐碎,但实际上改变了一切,因为它把沉默转化成了信号。一份缺失的报告只能意味着任务失败了。在此规则之前,一个安静的早晨意味着要么是平静的夜晚,要么是调度器坏了——我必须去检查是哪种情况。
早晨的阅读是整个系统的验收测试。报告的框架只有三个标题:
# Night report 2026-08-14
## what happened
## what was skipped, and why
## what needs me
如果这些内容在我两分钟的注意力内无法完成,那么报告本身就是问题所在。经历了五个夜晚的报告,虽然还很年轻,但每个早晨都有一份在等待——这是我拒绝失去的特性。咖啡有帮助。