Skip to content

第 11 章 · 零 token 事件驱动监督:watcher 与 turn-end guard ​

本章目标:理解 FirstMate 如何用「零 token 事件驱动监督」替代人工盯梢——bash watcher 睡眠待命、turn-end guard 兜底盲停、重启后自动 reconcile 接续。

11.1 为什么需要「零 token」监督 ​

想象一个常见场景:你给船队下达了三个并行任务,然后切去干别的事。此时谁在盯着这些任务?

传统的做法是轮询:每隔几分钟问一次 agent「任务完成了吗?」。这种方式有两个致命缺陷:

text
轮询模式的代价:

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。它的循环逻辑可以概括为三步:

text
┌─────────────────────────────────────────────────┐
│  1. SLEEP    睡眠于舰队之上,监听状态信号          │
│  2. CLASSIFY 在 bash 中分类唤醒事件               │
│     ├─ actionable → 写入队列并退出,唤醒 firstmate │
│     └─ benign     → 吸收掉,继续睡                │
│  3. WAKE     firstmate 被唤起处理积压的可行动事件   │
└─────────────────────────────────────────────────┘

什么算「可行动事件」(actionable)?官方文档列举了主要类别:

text
可行动唤醒(actionable wakes)包括:
- captain 相关的状态信号
- 无动词信号且无法证明船员仍在工作
- 已认证的检查输出(如 PR merge 轮询、Relay mention)
- 船员无法证明在工作时的 stale pane
- 声明中的外部等待超时未解除
- heartbeat backstop 命中

良性噪音(benign)则被吸收:
- routine watcher polling(例行轮询)
- supervision no-ops(监督空操作)
- absorbed benign wakes(被吸收的良性唤醒)

关键工程细节是持久化唤醒队列。可行动事件不会直接丢进对话,而是先写入磁盘上的 state/.wake-queue:

text
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 Codetracked Stop hook(asyncRewake),tokenless re-arm 循环
Grokbackground-notify 唤醒周期
Pi / pi-signedtracked primary watcher 扩展(.pi/extensions/*.ts)
Cursor Agent CLI项目级 .cursor/hooks.json 的 stop hook,停靠于 watcher
Codexbounded foreground checkpoints(有界前台检查点)
OpenCodeTUI plugin

以 Claude Code 为例,它在 .claude/settings.json 中注册两个 Stop hook:

json
{
  "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,其检查链条如下:

text
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 的所有状态都活在持久化介质上:

text
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:

sh
# 强行结束 primary 会话(模拟崩溃或主动重启)
kill <primary-session-pid>

# 重新进入 firstmate 目录启动新会话
cd firstmate && claude
# 新会话从磁盘状态恢复现场,接续未完成的工作

恢复行为有明确的边界规则:确认死亡(confirmed-dead)的 secondmate agent endpoint 会被关闭并通过相同的 spawn 路径重启;而存活性不确定的读取会被原样保留,避免产生重复的监督者。对于 herdr 后端,服务器恢复布局后被确认无 agent 或已死的任务标签壳会被关闭替换,无需手工清理标签页。

官方建议的一个配套习惯是:在主动重置之前先运行 /stow(第 12 章详讲),把可能只存在于对话中的持久知识落盘。之后的新会话就能 reconcile 并继续前进。

11.6 控制平面与 PreToolUse 守卫 ​

watcher 解决「何时唤醒」,本节补充「如何安全地干预」与「如何防止绕过监督」两块拼图。

控制平面:fm-control.sh ​

FirstMate 与运行中的 agent 有两条不等价的通道:数据平面 bin/fm-send.sh 传对话文本(对 secondmate 自动加路由标记);控制平面 bin/fm-control.sh 传生命周期动词。拆分的原因:带路由标记的 /quit 会被当成普通聊天被模型「思考」而不是执行。

控制平面只接受白名单动词,没有任意文本入口:

动词效果后置条件
interrupt投递 harness 验证过的中断序列,agent 保持运行仅适配器自证取消才报 cancel=confirmed,否则 unconfirmed
exit停止 agent,保留端点、worktree 与全部未提交修改后端分类器确认 agent 已消失;已停止视为幂等成功
relaunch同端点同 worktree 换新 agent(可显式指定 harness/model/effort)新 agent 存活且持久记录写入实际运行的 harness

注意边界:teardown/discard 不是动词(归 fm-teardown.sh 管,负责落地验证);resume 也不是动词(各 harness 恢复语义不统一,relaunch 在所有适配器上都能覆盖同一需求,因为磁盘上的 brief 才是持久指令)。relaunch 是唯一改持久记录的动词,因此以事务方式运行:带 journal、安全检查点、ship/scout 强制要求 --note 把接续指令写进新 agent 的任务书。

子代理委派守卫:fm-subagent-pretool-check.sh ​

这是一次真实事故催生的 PreToolUse 守卫:某 primary 绕过 bin/fm-spawn.sh,用 Claude Code 内置 subagent 工具跑了四个 worker——结果 fleet 视图全程零在途(没写 state/<id>.meta 与 brief)、primary 重启时两个 worker 中途死亡工作丢失、监督循环静默停摆 73 分钟。

根因:只有 fm-spawn.sh 会写任务元数据,harness 自带的委派工具产出的工作对监督栈完全不可见。所以围栏必须拦在 harness 工具面上。守卫按「工具名形状」分类:规范化小写后包含 agent/subagent/task/workflow/cron/schedul/worktree/delegate/spawn/dispatch/handoff/remote/sendmessage/monitor 等词干的委派形工具一律拒绝,同时保留三类豁免——mcp__ 前缀(MCP 工具命名不可控)、只观察/停止型(taskoutput/taskstop/killshell 等,拒绝它们会让在途任务无法收尾)、计划型(taskcreate/taskupdate,只写会话内 todo)。

watcher 武装安全带:fm-arm-pretool-check.sh ​

primary 必须通过可观察的 harness 调用来武装 fm-watch-arm.sh。若命令被包进后台操作符、管道、重定向或无关命令列表,watcher 子进程可能随工具调用一起死掉、失败被隐藏。安全带在执行前拒绝这些命令形状:分类器只做词法分词与执行位置分析,绝不执行/展开命令的任何部分;快路径先做字节归一化预判(含 ANSI-C $'...' 与 locale $"..." 编码标记检测),任何 fm-watch 痕迹或编码标记都交给完整分类器裁决。

本章小结 ​

  • 传统轮询监督既浪费 token 又反应迟钝;FirstMate 用零 token bash watcher 取代之——平时睡眠,仅在可行动事件出现时才唤醒 firstmate。
  • 可行动事件先写入持久的 state/.wake-queue,被打断也能恢复;良性噪音被吸收并记入 .watch-triage.log。
  • 六种 primary harness 各有监督适配:Claude Code 用 Stop hook + asyncRewake tokenless 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 重启、存疑的保持不动。
  • 干预走控制平面(fm-control.sh 白名单动词 interrupt/exit/relaunch),与数据平面严格分离;subagent-guard 拦截绕过 fm-spawn.sh 的委派形工具,arm-pretool-check 拒绝会隐藏失败的后台/管道/编码混淆命令。

🧪 随堂测验

点击你认为正确的选项。答错时会展示正确答案与原因解析。

1. FirstMate 的 watcher 被称为「零 token」,根本原因是?

2. 可行动唤醒事件为什么先写入 state/.wake-queue 而不是直接交给 firstmate?

3. turn-end guard 的不变量是什么?

4. 关于 restart-proof 的恢复行为,正确的说法是?

🛠️ 动手实践 ​

  1. 在你的 firstmate 克隆中找到 bin/fm-watch.sh 和 bin/fm-turnend-guard.sh,通读两者的头部注释(header comment),用自己的话写出 watcher 的睡眠—分类—唤醒循环与 guard 的判定谓词,对照本章验证理解是否一致。
  2. 检查你所选 harness 的钩子注册文件:若使用 Claude Code,打开 .claude/settings.json 找到两个 Stop hook 条目;若使用 Pi,列出 .pi/extensions/ 下的 tracked 扩展文件。确认每个条目对应本章表格中的哪一种接入机制。
  3. 人为制造一次「重启恢复」:在有 crewmate 任务运行时 kill 掉 primary 会话,重新启动后观察 session start 的 reconcile 输出——哪些任务被接续?是否有 confirmed-dead 的 endpoint 被重启?把观察到的时间线记录成一份简报。