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.jsonstop 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 并继续前进。

本章小结

  • 传统轮询监督既浪费 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 重启、存疑的保持不动。

🧪 随堂测验

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

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

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

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

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

🛠️ 动手实践

  1. 在你的 firstmate 克隆中找到 bin/fm-watch.shbin/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 被重启?把观察到的时间线记录成一份简报。