建议先阅读 #1 了解整体生命周期。本文会深入剖析 _dispatch_once_locked()。在上一篇文章中,笔者把 Dispatcher 比作 Kanban 的"心脏"——每 60 秒跳动一次,回收过期任务、晋升待办、孵化 Worker。
但"跳动"时内部发生了什么呢?
- CAS 到底是怎么做到原子领取的?
- WAL 模式为什么能保证不丢数据?
- 四层故障恢复分别解决什么问题?
- 容量控制是如何生效的?
这篇文章主要回答这些问题。
一、入口:`_kanban_dispatcher_watcher`
Dispatcher 不是一个独立进程。它作为 Gateway 的一个后台协程运行:

为什么需要 Dispatcher 锁?
两个 Gateway 同时调度会导致:
1. 回收频率翻倍——一个 Task 被 A 回收后又立即被 B 回收
2. Claim 事件翻倍——日志噪声
3. WAL checkpoint 竞态——SQLite 索引页可能损坏
文件锁是最后一道防线。 `dispatch_in_gateway` 配置是主控,文件锁是容错。
Dispatcher Tick 流程 — 8 步顺序执行,3 个阶段。
8 个步骤必须按顺序执行,不可调换。
四层恢复 vs 四种故障模式
| 故障模式 | 被哪一层捕获 | 恢复行为 |
| Worker 进程 Crash(OOM、信号) | 第三层detect_crashed_workers | PID gone → 立即回收 → ready |
| Worker 卡死(死循环、死锁) | 第二层detect_stale_running | Heartbeat 超时 → 回收 → ready |
| 慢模型响应(长时间推理) | 第一层release_stale_claims | PID 存活 → 不回收,延长 TTL |
| 远程 Worker 失联 | 第一层 + 第二层 | 无 PID 检测,TTL 过期 → ready |
| 连续孵化和 Worker 都失败 | 第四层 熔断器 | failure_limit=2 → auto-blocked |
四、容量控制详解
Dispatcher 支持 4 个层级的并发控制:
| 参数 | 默认值 | 作用域 | 行为 |
max_spawn | 无限制 | 全局 | running + 本次 spawn 总数上限 |
max_in_progress | 无限制 | 全局 | running 状态任务总数上限 |
max_in_progress_per_profile | 无限制 | 按 Profile | 单个 Profile 的 running 上限 |
Respawn Guard | 5 分钟冷却 | 按 Task | 防止同一 Task 被反复孵化 |
三者的优先级关系

注意: `max_spawn` 是实时并发容量而非"每 Tick 孵化预算"。它会统计 `status='running'` 的所有任务 + 本 Tick 新 spawned 的任务,确保总并发不超过上限。
Dispatcher 只负责调度。结果通知由 _kanban_notifier_watcher 完成:

Notifier 的订阅机制:
1. 谁创建的订阅?Agent 在 Worker 启动时自动创建
2. 什么时候取消? Task 达到 `done` 或 `archived` 后自动移除
3. 发送失败怎么办?同一订阅连续 3 次发送失败 → 丢弃该订阅(防止死 Chat 死循环)
夜雨聆风