第 11 章 · 零 token 事件驱动监督:watcher 与 turn-end guard
本章目标:理解 FirstMate 如何用「零 token 事件驱动监督」替代人工盯梢——bash watcher 睡眠待命、turn-end guard 兜底盲停、重启后自动 reconcile 接续。
11.1 为什么需要「零 token」监督
想象一个常见场景:你给船队下达了三个并行任务,然后切去干别的事。此时谁在盯着这些任务?
传统的做法是轮询:每隔几分钟问一次 agent「任务完成了吗?」。这种方式有两个致命缺陷:
轮询模式的代价:
1. token 浪费:每次询问都要消耗一次 LLM 调用,
即使绝大多数时候回答是"还在跑"
2. 反应迟钝:两次询问之间发生的异常只能干等被发现FirstMate 把这个模型整个翻转过来。官方架构文档的描述是:
A zero-token bash watcher sleeps on the fleet, classifies detected wakes in bash, and wakes the first mate only when something is actionable.
(一个零 token 的 bash watcher 睡眠于舰队之上,在 bash 中对唤醒事件分类,只在确有可行动事件时才唤醒第一副手。)
关键词是 zero-token:watcher 本身是一个纯 bash 脚本,不消耗任何 LLM 调用。它持续观察会话后端(tmux 窗口等)的状态信号,把「值得打扰船长的事件」和「良性噪音」区分开,只有前者才会触发 firstmate 的一次真实回合。
11.2 watcher 的工作机制:睡眠、分类、唤醒
核心脚本是 bin/fm-watch.sh。它的循环逻辑可以概括为三步:
┌─────────────────────────────────────────────────┐
│ 1. SLEEP 睡眠于舰队之上,监听状态信号 │
│ 2. CLASSIFY 在 bash 中分类唤醒事件 │
│ ├─ actionable → 写入队列并退出,唤醒 firstmate │
│ └─ benign → 吸收掉,继续睡 │
│ 3. WAKE firstmate 被唤起处理积压的可行动事件 │
└─────────────────────────────────────────────────┘什么算「可行动事件」(actionable)?官方文档列举了主要类别:
可行动唤醒(actionable wakes)包括:
- captain 相关的状态信号
- 无动词信号且无法证明船员仍在工作
- 已认证的检查输出(如 PR merge 轮询、Relay mention)
- 船员无法证明在工作时的 stale pane
- 声明中的外部等待超时未解除
- heartbeat backstop 命中
良性噪音(benign)则被吸收:
- routine watcher polling(例行轮询)
- supervision no-ops(监督空操作)
- absorbed benign wakes(被吸收的良性唤醒)关键工程细节是持久化唤醒队列。可行动事件不会直接丢进对话,而是先写入磁盘上的 state/.wake-queue:
state/
├── .wake-queue # 可行动事件的持久化队列
├── .watch-triage.log # 被吸收的良性唤醒调试日志
└── .watch-cycle-exits.log # watcher 生命周期记录这样设计的好处是:即使 watcher 或处理回合中途被打断,队列记录也不会丢失,下一次可以从磁盘恢复继续处理。此外,每次向船长展示事件时都会附带一个 fleet-wide 的 OPEN DECISIONS 区块,保证早前被后续日志「淹没」的未决决策会持续浮出水面,直到被显式解决。
11.3 各 primary harness 的接入方式
watcher 需要 primary harness 配合才能「唤醒」firstmate——不同 harness 提供的钩子机制不同。FirstMate 为每种已验证的主 harness 都准备了适配:
| Harness | 接入机制 |
|---|---|
| Claude Code | tracked Stop hook(asyncRewake),tokenless re-arm 循环 |
| Grok | background-notify 唤醒周期 |
| Pi / pi-signed | tracked primary watcher 扩展(.pi/extensions/*.ts) |
| Cursor Agent CLI | 项目级 .cursor/hooks.json 的 stop hook,停靠于 watcher |
| Codex | bounded foreground checkpoints(有界前台检查点) |
| OpenCode | TUI plugin |
以 Claude Code 为例,它在 .claude/settings.json 中注册两个 Stop hook:
{
"hooks": {
"Stop": [
{
"hooks": [
{ "type": "command", "command": "bin/fm-turnend-guard.sh --claude" },
{
"type": "command",
"command": "bin/fm-claude-stop-autoarm.sh",
"asyncRewake": true,
"timeout": 28800
}
]
}
]
}
}第一个 hook 是 turn-end guard(下一节详讲);第二个 asyncRewake 负责 tokenless re-arm——它让 Claude 会话在不消耗模型调用的情况下重新武装 watcher,形成「工作→空闲→再武装」的无缝循环。
Pi 的方式不同:.pi/extensions/fm-primary-pi-watch.ts 会在每次可行动唤醒发生时主动拆掉当前 watcher,再由自己拉起新的替换进程。因此 Pi 模式下「活着的、身份匹配的 watcher」才是健康常态。
对于未知 harness,FirstMate 提供 fallback 协议:按最保守的策略处理监督连续性,宁可多提醒也不静默失守。
11.4 turn-end guard:不让任何回合盲目结束
watcher 再可靠也有空窗期——比如刚处理完一轮、新 watcher 还没武装好的瞬间。如果恰好在这个瞬间 primary 结束了自己的回合,而舰队里还有工作进行中,会发生什么?没人监督了。
turn-end guard 就是堵住这个缺口的兜底机制,其不变量(invariant)写在官方文档里:
When work, a process-event source, or Relay polling needs supervision at that boundary and no identity-matched watcher has a fresh beacon, the harness integration must either block the turn end or force one bounded follow-up.
(当边界时刻存在需要监督的工作、过程事件源或 Relay 轮询,而没有任何身份匹配的 watcher 持有新鲜信标时,harness 集成必须阻止该回合结束,或强制发起一次有界的跟进。)
判定谓词位于 bin/fm-turnend-guard.sh,其检查链条如下:
1. 是否处于 FirstMate 主 home 范围内?
├─ 否 → 静默退出(普通项目不受影响)
└─ 是 ↓
2. 是否有在途工作?(state/*.meta 计数 + process-event 源)
├─ 无 → 静默退出
└─ 有 ↓
3. 是否有身份匹配的 watcher 且信标新鲜?
├─ 是 → 放行
└─ 否 → 阻止盲停或强制一次有界跟进注意「身份匹配」(identity-matched)这个限定词:guard 使用 PID 级别的严格检查——一个陈旧的信标即使还有活的 watcher 进程也会告警;反之一个新鲜但遗留的信标,如果锁缺失、进程已死或身份不匹配,同样会告警。信标新鲜度的宽限期由环境变量 FM_GUARD_GRACE 控制,默认 300 秒。
这套机制的精妙之处在于它与 auto-arm 协作而非互斥:在回合边界上,auto-arm 正在为即将到来的空闲期拉起新 watcher,guard 会配合这个过程,而不是把过渡期的正常状态误判为故障。
11.5 restart-proof:杀掉会话也丢不了进度
监督体系的最后一道保障是重启无恙(restart-proof)。FirstMate 的所有状态都活在持久化介质上:
Fleet 状态存放位置:
├── 会话后端本身 tmux(硬默认)/ herdr / cmux 等
├── no-mistakes 运行记录 任务执行轨迹
├── 状态事件日志 append-only status event logs
├── data/ 下的本地 markdown
│ ├── captain.md 船长偏好与指令
│ ├── captain-shared.md 共享操作记忆
│ └── learnings.md 学到的经验教训
└── 持久 secondmate homes这意味着你可以随时 kill 掉 primary 会话——下次启动时,session start 会执行 reconcile:
# 强行结束 primary 会话(模拟崩溃或主动重启)
kill <primary-session-pid>
# 重新进入 firstmate 目录启动新会话
cd firstmate && claude
# 新会话从磁盘状态恢复现场,接续未完成的工作恢复行为有明确的边界规则:确认死亡(confirmed-dead)的 secondmate agent endpoint 会被关闭并通过相同的 spawn 路径重启;而存活性不确定的读取会被原样保留,避免产生重复的监督者。对于 herdr 后端,服务器恢复布局后被确认无 agent 或已死的任务标签壳会被关闭替换,无需手工清理标签页。
官方建议的一个配套习惯是:在主动重置之前先运行 /stow(第 12 章详讲),把可能只存在于对话中的持久知识落盘。之后的新会话就能 reconcile 并继续前进。
本章小结
- 传统轮询监督既浪费 token 又反应迟钝;FirstMate 用零 token bash watcher 取代之——平时睡眠,仅在可行动事件出现时才唤醒 firstmate。
- 可行动事件先写入持久的
state/.wake-queue,被打断也能恢复;良性噪音被吸收并记入.watch-triage.log。 - 六种 primary harness 各有监督适配:Claude Code 用
Stophook +asyncRewaketokenless re-arm,Grok 用 background-notify,Pi 用 tracked extension,Cursor 停靠 stop hook,Codex 用有界前台检查点,OpenCode 用 TUI plugin。 - turn-end guard 保证「no turn ends blind」:有在途工作而无新鲜 watcher 信标时,阻止盲停或强制有界跟进;信标宽限默认 300 秒(
FM_GUARD_GRACE)。 - 整个体系 restart-proof:状态全在磁盘与会话后端中,kill 会话后新会话通过 reconcile 接续,确认死亡的 agent 重启、存疑的保持不动。
🧪 随堂测验
点击你认为正确的选项。答错时会展示正确答案与原因解析。
1. FirstMate 的 watcher 被称为「零 token」,根本原因是?
2. 可行动唤醒事件为什么先写入 state/.wake-queue 而不是直接交给 firstmate?
3. turn-end guard 的不变量是什么?
4. 关于 restart-proof 的恢复行为,正确的说法是?
🛠️ 动手实践
- 在你的 firstmate 克隆中找到
bin/fm-watch.sh和bin/fm-turnend-guard.sh,通读两者的头部注释(header comment),用自己的话写出 watcher 的睡眠—分类—唤醒循环与 guard 的判定谓词,对照本章验证理解是否一致。 - 检查你所选 harness 的钩子注册文件:若使用 Claude Code,打开
.claude/settings.json找到两个Stophook 条目;若使用 Pi,列出.pi/extensions/下的 tracked 扩展文件。确认每个条目对应本章表格中的哪一种接入机制。 - 人为制造一次「重启恢复」:在有 crewmate 任务运行时 kill 掉 primary 会话,重新启动后观察 session start 的 reconcile 输出——哪些任务被接续?是否有 confirmed-dead 的 endpoint 被重启?把观察到的时间线记录成一份简报。