解密 Claude Code 协作机制:Inbox 注入让队友异步通信
🎯 学习目标
- 一次性子 Agent 和持续 Teammate 在生命周期/历史/通信上的差别?
- 为什么不实现 MessageBus + read_text()/unlink()?崩溃窗口在哪?
- 每消息一文件的 ready→processing→done/quarantine 解决什么?
- spawn_teammate 和 send_message 各做什么?
- Teammate 的四种状态(running/idle/failed/shutdown)?
- Mailbox 消息如何回到公共 Agent Loop?
🧠 核心概念
1 · 一次性子 Agent vs 持续 Teammate ▸
| 边界 | 一次性子 Agent | 持续 Teammate |
|---|---|---|
| 生命周期 | 完成一次委派后返回 | 完成后进入 idle,可接收后续消息 |
| 对话历史 | 每次委派独立 | 每个名字绑定独立 AgentRunner,后续复用 |
| 通信 | 返回最终结果 | 通过持久 Mailbox 双向发送文本消息 |
| 公开工具 | Lead 的 task | Lead 的 spawn_teammate、send_message |
| 状态 | 运行或结束 | running、idle、failed、shutdown |
2 · 为什么不实现 MessageBus + read_text/unlink ▸
持续存在不能只靠进程内 dict。如果消息读取后直接删除,Agent 在「读到消息」和「写入历史」之间崩溃,工作就消失了。多个 writer 追加同一 .jsonl 还要解决并发写、部分行和读取游标。
3 · 每消息一文件 + 状态机 ▸
FileMailboxStore:每消息一个文件,状态机 ready → processing → done/quarantine。
- ready:消息已持久写入,等待队友读取。
- processing:队友已读取正在处理(原子改状态,崩溃后重启能看到 processing 重新决策)。
- done:处理完成,消息已写入队友历史。
- quarantine:处理异常,隔离不丢,供排查。
不读就删:只有 done/quarantine 后才清理。崩溃窗口靠状态机收窄——processing 状态明确表示「正在处理」。
4 · 两个 Lead 工具 ▸
| 工具 | 必填字段 | 可观察结果 |
|---|---|---|
| spawn_teammate | name、role、prompt | 创建独立 Runner,把首个 task 消息持久写入队友 Mailbox,异步启动 worker |
| send_message | to、content | 把 message 持久写入 Lead 或已存在队友的 Mailbox |
Teammate 状态:running(工作中)、idle(完成一轮等消息)、failed(异常)、shutdown(优雅关闭)。idle 时新消息进入 Mailbox 会唤醒 worker 继续。idle 是被动的:它只表示 Runner 还留着、等下一条显式消息,不会自己找活干——空闲自驱认领是第 17 章的事。
6 · claim 租约与至少一次投递 ▸
claim 把消息从 ready 原子搬到 processing,等于取得租约:未确认前不会被别人拿走。双锁保护:进程内 promise 队列 + 跨进程 proper-lockfile,12 个并发 claim 拿到 12 条互不重复的消息。权威 FIFO 键是 (created_at_utc, id),不是文件名顺序。ack = processing→done;release = 退回 ready 保留重放;坏消息进 quarantine 留证据不堵队列。
投递语义是至少一次:崩溃可能重放(重启把 processing 恢复为 ready),但不会静默丢;「读+删」的替代方案是至多一次,有永久丢失窗口。重复由稳定 ID 去重。队友执行失败:输入隔离入 quarantine、状态改 failed,并向 Lead 发布可观察的失败 result。关闭顺序 Teammate → Cron → supervisor → model,单个资源关闭失败不跳过其余。
5 · 四个边界 + 回到公共 Loop ▸
Lead 工具:spawn_teammate / send_message
↓
FileMailboxStore
每消息一个文件,ready→processing→done/quarantine
↓
TeammateRuntime
持有队友 Runner、状态和 worker,组合 Mailbox/Cron event
↓
AgentRunner.runEvents()
把 typed message 注入 canonical history,再执行 event-only turn
Mailbox 消息通过 typed event 通道回到公共 Agent Loop:TeammateRuntime 把 ready 消息转 typed message event → EventInbox → AgentRunner.runEvents() 注入 canonical history → event-only turn。工具仍经过 Hook 与权限。不创建第二套 Loop 或第二个 EventInbox。
先入 history,再 ack:ack 失败要回滚弹出刚追加的 history,顺序反了崩在中间就永久丢工作——宁可重复不可丢失。mailbox 是唯一保留半状态(已进 history、ack 未完成)的事件类型,ack 失败重试不重复追加。sender 不是工具输入字段,由 ToolContext 注入可信身份;idempotencyKey 就是消息 UUID。