Hermes Agent Kanban 流程超深度分析
源码目录 :
github.com/hermes-agent/核心文件(25个模块,18,340行代码):
hermes_cli/kanban.py(1345行) --- CLI入口、命令分发、task-verb handlershermes_cli/kanban_db.py(4136行) --- SQLite看板核心、状态机、CRUD、worker上下文hermes_cli/kanban_db_dispatch.py(2361行) --- 调度器、崩溃/停滞/孤儿检测、熔断器、内存感知hermes_cli/kanban_db_connect.py(1204行) --- 连接生命周期、WAL、跨进程锁、dispatch tick锁hermes_cli/kanban_db_workspace.py(549行) --- 工作区解析(scratch/worktree/dir)hermes_cli/kanban_db_notify.py(429行) --- 通知订阅、投递模式hermes_cli/kanban_db_graph.py(214行) --- 任务依赖图hermes_cli/kanban_ops.py(385行) --- dispatch/daemon/tail/watch/gc/repair命令hermes_cli/kanban_swarm.py(300行) --- Swarm拓扑(root→workers→verifier→synthesizer)hermes_cli/kanban_parser.py(451行) --- argparse构建器hermes_cli/kanban_decompose.py(348行) --- 任务分解hermes_cli/kanban_specify.py(231行) --- 任务规格化hermes_cli/kanban_boards.py(214行) --- 多看板管理hermes_cli/kanban_transfer.py(374行) --- 跨看板迁移hermes_cli/kanban_diagnostics.py(794行) --- 诊断引擎hermes_cli/kanban_pr_acceptance.py(117行) --- PR验收hermes_cli/kanban_pr_acceptance_store.py(45行) --- PR验收存储hermes_cli/kanban_output.py(88行) --- 文本/JSON输出格式化tools/kanban_tools.py(992行) --- Agent工具调用面(14个工具)tools/kanban_tools_schemas.py(515行) --- 工具Schema定义agent/kanban_stop.py(79行) --- Kanban停止门控gateway/kanban_watchers.py(333行) --- 网关看板监听gateway/kanban_watchers_dispatcher.py(310行) --- 网关嵌入式调度器gateway/kanban_watchers_notifier.py(649行) --- 终态通知投递gateway/kanban_watchers_common.py(150行) --- 看板监听通用plugins/kanban/dashboard/plugin_api.py(1727行) --- Dashboard插件API
目录
- 模块定位与系统位置
- 整体架构总览
- 任务状态机与生命周期
- [CLI入口层:kanban_command() 完整解析](#CLI入口层:kanban_command() 完整解析)
- SQLite看板核心:kanban_db.py
- 连接层:WAL/锁/迁移/损坏检测
- [调度器核心:dispatch_once() 逐阶段解析](#调度器核心:dispatch_once() 逐阶段解析)
- [Worker生成:_default_spawn() 逐行解析](#Worker生成:_default_spawn() 逐行解析)
- 崩溃/停滞/孤儿检测与回收
- 熔断器与重试控制
- Agent工具调用面:14个Kanban工具
- 工作区隔离:scratch/worktree/dir
- 通知订阅与终态投递
- Swarm拓扑:多代理协作
- 内存感知并发控制
- 运行时心跳桥接
- 设计亮点与关键决策
1. 模块定位与系统位置
1.1 业务职责
Kanban是Hermes Agent的跨Profile任务编排原语:一个SQLite持久化的看板,让多个Profile(代理身份)通过任务卡片协作。核心能力包括:
- 任务管理:创建/分配/完成/阻塞/审查任务,带依赖图、评论、附件
- 自动调度:网关内嵌调度器每60秒tick一次,自动spawn worker进程执行ready任务
- Worker生命周期:spawn→运行→完成/阻塞/崩溃/超时/停滞,全链路检测与回收
- 多代理协作:Swarm拓扑(planning root→parallel workers→verifier→synthesizer)
- 通知投递:终态事件推送到Telegram/TUI/API等平台
1.2 在系统中的位置
#mermaid-svg-7EL1ZebZNYZEYprg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-7EL1ZebZNYZEYprg .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-7EL1ZebZNYZEYprg .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-7EL1ZebZNYZEYprg .error-icon{fill:#552222;}#mermaid-svg-7EL1ZebZNYZEYprg .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-7EL1ZebZNYZEYprg .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-7EL1ZebZNYZEYprg .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-7EL1ZebZNYZEYprg .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-7EL1ZebZNYZEYprg .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-7EL1ZebZNYZEYprg .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-7EL1ZebZNYZEYprg .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-7EL1ZebZNYZEYprg .marker{fill:#333333;stroke:#333333;}#mermaid-svg-7EL1ZebZNYZEYprg .marker.cross{stroke:#333333;}#mermaid-svg-7EL1ZebZNYZEYprg svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-7EL1ZebZNYZEYprg p{margin:0;}#mermaid-svg-7EL1ZebZNYZEYprg .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-7EL1ZebZNYZEYprg .cluster-label text{fill:#333;}#mermaid-svg-7EL1ZebZNYZEYprg .cluster-label span{color:#333;}#mermaid-svg-7EL1ZebZNYZEYprg .cluster-label span p{background-color:transparent;}#mermaid-svg-7EL1ZebZNYZEYprg .label text,#mermaid-svg-7EL1ZebZNYZEYprg span{fill:#333;color:#333;}#mermaid-svg-7EL1ZebZNYZEYprg .node rect,#mermaid-svg-7EL1ZebZNYZEYprg .node circle,#mermaid-svg-7EL1ZebZNYZEYprg .node ellipse,#mermaid-svg-7EL1ZebZNYZEYprg .node polygon,#mermaid-svg-7EL1ZebZNYZEYprg .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-7EL1ZebZNYZEYprg .rough-node .label text,#mermaid-svg-7EL1ZebZNYZEYprg .node .label text,#mermaid-svg-7EL1ZebZNYZEYprg .image-shape .label,#mermaid-svg-7EL1ZebZNYZEYprg .icon-shape .label{text-anchor:middle;}#mermaid-svg-7EL1ZebZNYZEYprg .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-7EL1ZebZNYZEYprg .rough-node .label,#mermaid-svg-7EL1ZebZNYZEYprg .node .label,#mermaid-svg-7EL1ZebZNYZEYprg .image-shape .label,#mermaid-svg-7EL1ZebZNYZEYprg .icon-shape .label{text-align:center;}#mermaid-svg-7EL1ZebZNYZEYprg .node.clickable{cursor:pointer;}#mermaid-svg-7EL1ZebZNYZEYprg .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-7EL1ZebZNYZEYprg .arrowheadPath{fill:#333333;}#mermaid-svg-7EL1ZebZNYZEYprg .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-7EL1ZebZNYZEYprg .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-7EL1ZebZNYZEYprg .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-7EL1ZebZNYZEYprg .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-7EL1ZebZNYZEYprg .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-7EL1ZebZNYZEYprg .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-7EL1ZebZNYZEYprg .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-7EL1ZebZNYZEYprg .cluster text{fill:#333;}#mermaid-svg-7EL1ZebZNYZEYprg .cluster span{color:#333;}#mermaid-svg-7EL1ZebZNYZEYprg div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-7EL1ZebZNYZEYprg .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-7EL1ZebZNYZEYprg rect.text{fill:none;stroke-width:0;}#mermaid-svg-7EL1ZebZNYZEYprg .icon-shape,#mermaid-svg-7EL1ZebZNYZEYprg .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-7EL1ZebZNYZEYprg .icon-shape p,#mermaid-svg-7EL1ZebZNYZEYprg .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-7EL1ZebZNYZEYprg .icon-shape .label rect,#mermaid-svg-7EL1ZebZNYZEYprg .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-7EL1ZebZNYZEYprg .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-7EL1ZebZNYZEYprg .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-7EL1ZebZNYZEYprg :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 外部依赖
Kanban子系统 (本次分析)
用户交互层
hermes kanban CLI
create/list/show/assign...
TUI /agents 面板
Dashboard插件
plugin_api.py
kanban_db.py
SQLite看板核心
kanban_db_dispatch.py
调度器/检测/熔断
kanban_db_connect.py
连接/WAL/锁
kanban_db_workspace.py
工作区
kanban_db_notify.py
通知订阅
kanban_tools.py
Agent工具面
kanban_swarm.py
Swarm拓扑
gateway/kanban_watchers_dispatcher.py
网关嵌入式调度
gateway/kanban_watchers_notifier.py
终态通知投递
gateway/run.py
网关进程
hermes_cli/profiles.py
Profile管理
subprocess.Popen
Worker子进程
state.db
会话数据库
hermes_cli/plugins
生命周期钩子
1.3 核心设计原则
| 原则 | 实现 |
|---|---|
| 跨Profile共享 | kanban.db在共享Hermes root下,不按Profile分叉 |
| 单写者调度 | dispatch_tick_lock防止两个调度器同时写一个看板 |
| 持久化一切 | 任务/运行/事件/评论/附件/通知全部SQLite |
| 崩溃恢复 | PID检测+心跳+claim TTL+orphan调和+SQLite账本 |
| 隔离性 | Worker获得独立工作区(scratch/worktree)、独立session_id |
| 安全边界 | delegate_task子代理不能mutate看板、worker只能操作自己的task |
2. 整体架构总览
2.1 完整端到端流程
#mermaid-svg-gHPOm0LrnxRJSBKb{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-gHPOm0LrnxRJSBKb .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-gHPOm0LrnxRJSBKb .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-gHPOm0LrnxRJSBKb .error-icon{fill:#552222;}#mermaid-svg-gHPOm0LrnxRJSBKb .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-gHPOm0LrnxRJSBKb .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-gHPOm0LrnxRJSBKb .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-gHPOm0LrnxRJSBKb .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-gHPOm0LrnxRJSBKb .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-gHPOm0LrnxRJSBKb .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-gHPOm0LrnxRJSBKb .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-gHPOm0LrnxRJSBKb .marker{fill:#333333;stroke:#333333;}#mermaid-svg-gHPOm0LrnxRJSBKb .marker.cross{stroke:#333333;}#mermaid-svg-gHPOm0LrnxRJSBKb svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-gHPOm0LrnxRJSBKb p{margin:0;}#mermaid-svg-gHPOm0LrnxRJSBKb .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-gHPOm0LrnxRJSBKb .cluster-label text{fill:#333;}#mermaid-svg-gHPOm0LrnxRJSBKb .cluster-label span{color:#333;}#mermaid-svg-gHPOm0LrnxRJSBKb .cluster-label span p{background-color:transparent;}#mermaid-svg-gHPOm0LrnxRJSBKb .label text,#mermaid-svg-gHPOm0LrnxRJSBKb span{fill:#333;color:#333;}#mermaid-svg-gHPOm0LrnxRJSBKb .node rect,#mermaid-svg-gHPOm0LrnxRJSBKb .node circle,#mermaid-svg-gHPOm0LrnxRJSBKb .node ellipse,#mermaid-svg-gHPOm0LrnxRJSBKb .node polygon,#mermaid-svg-gHPOm0LrnxRJSBKb .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-gHPOm0LrnxRJSBKb .rough-node .label text,#mermaid-svg-gHPOm0LrnxRJSBKb .node .label text,#mermaid-svg-gHPOm0LrnxRJSBKb .image-shape .label,#mermaid-svg-gHPOm0LrnxRJSBKb .icon-shape .label{text-anchor:middle;}#mermaid-svg-gHPOm0LrnxRJSBKb .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-gHPOm0LrnxRJSBKb .rough-node .label,#mermaid-svg-gHPOm0LrnxRJSBKb .node .label,#mermaid-svg-gHPOm0LrnxRJSBKb .image-shape .label,#mermaid-svg-gHPOm0LrnxRJSBKb .icon-shape .label{text-align:center;}#mermaid-svg-gHPOm0LrnxRJSBKb .node.clickable{cursor:pointer;}#mermaid-svg-gHPOm0LrnxRJSBKb .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-gHPOm0LrnxRJSBKb .arrowheadPath{fill:#333333;}#mermaid-svg-gHPOm0LrnxRJSBKb .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-gHPOm0LrnxRJSBKb .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-gHPOm0LrnxRJSBKb .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-gHPOm0LrnxRJSBKb .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-gHPOm0LrnxRJSBKb .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-gHPOm0LrnxRJSBKb .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-gHPOm0LrnxRJSBKb .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-gHPOm0LrnxRJSBKb .cluster text{fill:#333;}#mermaid-svg-gHPOm0LrnxRJSBKb .cluster span{color:#333;}#mermaid-svg-gHPOm0LrnxRJSBKb div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-gHPOm0LrnxRJSBKb .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-gHPOm0LrnxRJSBKb rect.text{fill:none;stroke-width:0;}#mermaid-svg-gHPOm0LrnxRJSBKb .icon-shape,#mermaid-svg-gHPOm0LrnxRJSBKb .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-gHPOm0LrnxRJSBKb .icon-shape p,#mermaid-svg-gHPOm0LrnxRJSBKb .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-gHPOm0LrnxRJSBKb .icon-shape .label rect,#mermaid-svg-gHPOm0LrnxRJSBKb .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-gHPOm0LrnxRJSBKb .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-gHPOm0LrnxRJSBKb .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-gHPOm0LrnxRJSBKb :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} hermes kanban create --title X --assignee alice
status=ready
每60s tick
否
是
是
否
rate_limit_cooldown
blocker_auth
recent_success
active_pr
None (通过)
kanban_complete()
kanban_block()
kanban_heartbeat()
kanban_comment()
退出rc=0但未调用complete
退出rc=75
崩溃/信号
用户
kanban_command()
kanban_db.create_task()
kanban.db
tasks表
ready队列
网关进程
kanban_watchers_dispatcher
dispatch_once()
阶段1: 回收
reap_worker_zombies()
回收僵尸子进程
release_stale_claims()
释放过期claim
reconcile_orphaned_running()
调和孤儿running
detect_stale_running()
检测无心跳running
detect_crashed_workers()
检测死亡PID
enforce_max_runtime()
超时终止
recompute_ready()
todo→ready提升
阶段2: 预算
_memory_pressure_level()
内存压力检查
_tick_spawn_budget()
spawn预算计算
阶段3: spawn
_lane_rows(ready)
按优先级排序ready行
_lane_rows(review)
review行
_dispatch_lane_task()
逐行处理
assignee是
真实Profile?
skipped_nonspawnable
per-profile
cap已满?
skipped_per_profile_capped
check_respawn_guard()
guard_reason?
延迟到下次tick
claim_task()
CAS claim
_resolve_worktree_workspace()
or resolve_workspace()
_default_spawn(task, workspace)
构建子进程环境变量
HERMES_KANBAN_TASK/WORKSPACE/DB...
subprocess.Popen(hermes -p alice --cli chat -q 'work kanban task X')
_set_worker_pid()
_fire_worker_spawned_hook()
Worker进程运行
task→done
task→blocked
刷新claim+heartbeat
追加评论
protocol_violation
→ready+错误消息
rate_limited
→ready不计数failure
crashed
→ready+计数failure
kanban_watchers_notifier
通知订阅者
Telegram/TUI/API
2.2 三层架构
#mermaid-svg-uro8M0cEqhr1O4uU{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-uro8M0cEqhr1O4uU .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-uro8M0cEqhr1O4uU .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-uro8M0cEqhr1O4uU .error-icon{fill:#552222;}#mermaid-svg-uro8M0cEqhr1O4uU .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-uro8M0cEqhr1O4uU .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-uro8M0cEqhr1O4uU .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-uro8M0cEqhr1O4uU .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-uro8M0cEqhr1O4uU .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-uro8M0cEqhr1O4uU .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-uro8M0cEqhr1O4uU .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-uro8M0cEqhr1O4uU .marker{fill:#333333;stroke:#333333;}#mermaid-svg-uro8M0cEqhr1O4uU .marker.cross{stroke:#333333;}#mermaid-svg-uro8M0cEqhr1O4uU svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-uro8M0cEqhr1O4uU p{margin:0;}#mermaid-svg-uro8M0cEqhr1O4uU .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-uro8M0cEqhr1O4uU .cluster-label text{fill:#333;}#mermaid-svg-uro8M0cEqhr1O4uU .cluster-label span{color:#333;}#mermaid-svg-uro8M0cEqhr1O4uU .cluster-label span p{background-color:transparent;}#mermaid-svg-uro8M0cEqhr1O4uU .label text,#mermaid-svg-uro8M0cEqhr1O4uU span{fill:#333;color:#333;}#mermaid-svg-uro8M0cEqhr1O4uU .node rect,#mermaid-svg-uro8M0cEqhr1O4uU .node circle,#mermaid-svg-uro8M0cEqhr1O4uU .node ellipse,#mermaid-svg-uro8M0cEqhr1O4uU .node polygon,#mermaid-svg-uro8M0cEqhr1O4uU .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-uro8M0cEqhr1O4uU .rough-node .label text,#mermaid-svg-uro8M0cEqhr1O4uU .node .label text,#mermaid-svg-uro8M0cEqhr1O4uU .image-shape .label,#mermaid-svg-uro8M0cEqhr1O4uU .icon-shape .label{text-anchor:middle;}#mermaid-svg-uro8M0cEqhr1O4uU .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-uro8M0cEqhr1O4uU .rough-node .label,#mermaid-svg-uro8M0cEqhr1O4uU .node .label,#mermaid-svg-uro8M0cEqhr1O4uU .image-shape .label,#mermaid-svg-uro8M0cEqhr1O4uU .icon-shape .label{text-align:center;}#mermaid-svg-uro8M0cEqhr1O4uU .node.clickable{cursor:pointer;}#mermaid-svg-uro8M0cEqhr1O4uU .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-uro8M0cEqhr1O4uU .arrowheadPath{fill:#333333;}#mermaid-svg-uro8M0cEqhr1O4uU .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-uro8M0cEqhr1O4uU .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-uro8M0cEqhr1O4uU .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-uro8M0cEqhr1O4uU .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-uro8M0cEqhr1O4uU .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-uro8M0cEqhr1O4uU .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-uro8M0cEqhr1O4uU .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-uro8M0cEqhr1O4uU .cluster text{fill:#333;}#mermaid-svg-uro8M0cEqhr1O4uU .cluster span{color:#333;}#mermaid-svg-uro8M0cEqhr1O4uU div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-uro8M0cEqhr1O4uU .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-uro8M0cEqhr1O4uU rect.text{fill:none;stroke-width:0;}#mermaid-svg-uro8M0cEqhr1O4uU .icon-shape,#mermaid-svg-uro8M0cEqhr1O4uU .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-uro8M0cEqhr1O4uU .icon-shape p,#mermaid-svg-uro8M0cEqhr1O4uU .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-uro8M0cEqhr1O4uU .icon-shape .label rect,#mermaid-svg-uro8M0cEqhr1O4uU .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-uro8M0cEqhr1O4uU .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-uro8M0cEqhr1O4uU .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-uro8M0cEqhr1O4uU :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 层3: 调度层
层2: 核心层
层1: 接口层
CLI
hermes kanban ...
Agent工具
kanban_complete/block...
Dashboard API
plugin_api.py
kanban_db.py
CRUD+状态机+依赖图
kanban_db_connect.py
连接+WAL+锁
kanban_db_workspace.py
工作区解析
kanban_db_notify.py
订阅管理
kanban_db_dispatch.py
tick+回收+spawn+熔断
gateway/kanban_watchers_*
网关集成
kanban_swarm.py
Swarm拓扑
3. 任务状态机与生命周期
3.1 九态状态机
#mermaid-svg-3mp5UtKiGq42fyHt{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-3mp5UtKiGq42fyHt .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-3mp5UtKiGq42fyHt .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-3mp5UtKiGq42fyHt .error-icon{fill:#552222;}#mermaid-svg-3mp5UtKiGq42fyHt .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-3mp5UtKiGq42fyHt .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-3mp5UtKiGq42fyHt .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-3mp5UtKiGq42fyHt .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-3mp5UtKiGq42fyHt .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-3mp5UtKiGq42fyHt .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-3mp5UtKiGq42fyHt .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-3mp5UtKiGq42fyHt .marker{fill:#333333;stroke:#333333;}#mermaid-svg-3mp5UtKiGq42fyHt .marker.cross{stroke:#333333;}#mermaid-svg-3mp5UtKiGq42fyHt svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-3mp5UtKiGq42fyHt p{margin:0;}#mermaid-svg-3mp5UtKiGq42fyHt defs #statediagram-barbEnd{fill:#333333;stroke:#333333;}#mermaid-svg-3mp5UtKiGq42fyHt g.stateGroup text{fill:#9370DB;stroke:none;font-size:10px;}#mermaid-svg-3mp5UtKiGq42fyHt g.stateGroup text{fill:#333;stroke:none;font-size:10px;}#mermaid-svg-3mp5UtKiGq42fyHt g.stateGroup .state-title{font-weight:bolder;fill:#131300;}#mermaid-svg-3mp5UtKiGq42fyHt g.stateGroup rect{fill:#ECECFF;stroke:#9370DB;}#mermaid-svg-3mp5UtKiGq42fyHt g.stateGroup line{stroke:#333333;stroke-width:1;}#mermaid-svg-3mp5UtKiGq42fyHt .transition{stroke:#333333;stroke-width:1;fill:none;}#mermaid-svg-3mp5UtKiGq42fyHt .stateGroup .composit{fill:white;border-bottom:1px;}#mermaid-svg-3mp5UtKiGq42fyHt .stateGroup .alt-composit{fill:#e0e0e0;border-bottom:1px;}#mermaid-svg-3mp5UtKiGq42fyHt .state-note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-3mp5UtKiGq42fyHt .state-note text{fill:black;stroke:none;font-size:10px;}#mermaid-svg-3mp5UtKiGq42fyHt .stateLabel .box{stroke:none;stroke-width:0;fill:#ECECFF;opacity:0.5;}#mermaid-svg-3mp5UtKiGq42fyHt .edgeLabel .label rect{fill:#ECECFF;opacity:0.5;}#mermaid-svg-3mp5UtKiGq42fyHt .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-3mp5UtKiGq42fyHt .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-3mp5UtKiGq42fyHt .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-3mp5UtKiGq42fyHt .edgeLabel .label text{fill:#333;}#mermaid-svg-3mp5UtKiGq42fyHt .label div .edgeLabel{color:#333;}#mermaid-svg-3mp5UtKiGq42fyHt .stateLabel text{fill:#131300;font-size:10px;font-weight:bold;}#mermaid-svg-3mp5UtKiGq42fyHt .node circle.state-start{fill:#333333;stroke:#333333;}#mermaid-svg-3mp5UtKiGq42fyHt .node .fork-join{fill:#333333;stroke:#333333;}#mermaid-svg-3mp5UtKiGq42fyHt .node circle.state-end{fill:#9370DB;stroke:white;stroke-width:1.5;}#mermaid-svg-3mp5UtKiGq42fyHt .end-state-inner{fill:white;stroke-width:1.5;}#mermaid-svg-3mp5UtKiGq42fyHt .node rect{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-3mp5UtKiGq42fyHt .node polygon{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-3mp5UtKiGq42fyHt #statediagram-barbEnd{fill:#333333;}#mermaid-svg-3mp5UtKiGq42fyHt .statediagram-cluster rect{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-3mp5UtKiGq42fyHt .cluster-label,#mermaid-svg-3mp5UtKiGq42fyHt .nodeLabel{color:#131300;}#mermaid-svg-3mp5UtKiGq42fyHt .statediagram-cluster rect.outer{rx:5px;ry:5px;}#mermaid-svg-3mp5UtKiGq42fyHt .statediagram-state .divider{stroke:#9370DB;}#mermaid-svg-3mp5UtKiGq42fyHt .statediagram-state .title-state{rx:5px;ry:5px;}#mermaid-svg-3mp5UtKiGq42fyHt .statediagram-cluster.statediagram-cluster .inner{fill:white;}#mermaid-svg-3mp5UtKiGq42fyHt .statediagram-cluster.statediagram-cluster-alt .inner{fill:#f0f0f0;}#mermaid-svg-3mp5UtKiGq42fyHt .statediagram-cluster .inner{rx:0;ry:0;}#mermaid-svg-3mp5UtKiGq42fyHt .statediagram-state rect.basic{rx:5px;ry:5px;}#mermaid-svg-3mp5UtKiGq42fyHt .statediagram-state rect.divider{stroke-dasharray:10,10;fill:#f0f0f0;}#mermaid-svg-3mp5UtKiGq42fyHt .note-edge{stroke-dasharray:5;}#mermaid-svg-3mp5UtKiGq42fyHt .statediagram-note rect{fill:#fff5ad;stroke:#aaaa33;stroke-width:1px;rx:0;ry:0;}#mermaid-svg-3mp5UtKiGq42fyHt .statediagram-note rect{fill:#fff5ad;stroke:#aaaa33;stroke-width:1px;rx:0;ry:0;}#mermaid-svg-3mp5UtKiGq42fyHt .statediagram-note text{fill:black;}#mermaid-svg-3mp5UtKiGq42fyHt .statediagram-note .nodeLabel{color:black;}#mermaid-svg-3mp5UtKiGq42fyHt .statediagram .edgeLabel{color:red;}#mermaid-svg-3mp5UtKiGq42fyHt #dependencyStart,#mermaid-svg-3mp5UtKiGq42fyHt #dependencyEnd{fill:#333333;stroke:#333333;stroke-width:1;}#mermaid-svg-3mp5UtKiGq42fyHt .statediagramTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-3mp5UtKiGq42fyHt :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} create --triage
create (默认)
create --initial-status running
create --initial-status blocked
assign + promote
assign + promote
recompute_ready()
所有parent done
claim_task()
spawn worker
kanban_complete()
kanban_block()
kanban_request_review()
回收(crash/timeout/stale)
reviewer kanban_complete()
kanban_request_changes()
kanban_unblock()
重复block≥2次
kanban archive
手动drag back
kanban_heartbeat()
(刷新claim)
triage
todo
running
blocked
ready
done
review
archived
VALID_STATUSES = {triage, todo, scheduled,
ready, running, blocked, review, done, archived}
VALID_INITIAL_STATUSES = {running, blocked}
3.2 状态定义 (kanban_db.py L89-98)
python
VALID_STATUSES = {"triage", "todo", "scheduled", "ready", "running", "blocked", "review", "done", "archived"}
VALID_INITIAL_STATUSES = {"running", "blocked"} # 只有这两个可以用initial_status直接创建
VALID_BLOCK_KINDS = {"dependency", "needs_input", "capability", "transient"}
BLOCK_RECURRENCE_LIMIT = 2 # 同reason block→unblock→re-block 2次后路由到triage
VALID_WORKSPACE_KINDS = {"scratch", "worktree", "dir"}
3.3 Run(运行)生命周期
每个running状态对应一个task_runs行:
#mermaid-svg-OPPuFsXu0DFgeSUH{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-OPPuFsXu0DFgeSUH .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-OPPuFsXu0DFgeSUH .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-OPPuFsXu0DFgeSUH .error-icon{fill:#552222;}#mermaid-svg-OPPuFsXu0DFgeSUH .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-OPPuFsXu0DFgeSUH .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-OPPuFsXu0DFgeSUH .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-OPPuFsXu0DFgeSUH .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-OPPuFsXu0DFgeSUH .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-OPPuFsXu0DFgeSUH .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-OPPuFsXu0DFgeSUH .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-OPPuFsXu0DFgeSUH .marker{fill:#333333;stroke:#333333;}#mermaid-svg-OPPuFsXu0DFgeSUH .marker.cross{stroke:#333333;}#mermaid-svg-OPPuFsXu0DFgeSUH svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-OPPuFsXu0DFgeSUH p{margin:0;}#mermaid-svg-OPPuFsXu0DFgeSUH defs #statediagram-barbEnd{fill:#333333;stroke:#333333;}#mermaid-svg-OPPuFsXu0DFgeSUH g.stateGroup text{fill:#9370DB;stroke:none;font-size:10px;}#mermaid-svg-OPPuFsXu0DFgeSUH g.stateGroup text{fill:#333;stroke:none;font-size:10px;}#mermaid-svg-OPPuFsXu0DFgeSUH g.stateGroup .state-title{font-weight:bolder;fill:#131300;}#mermaid-svg-OPPuFsXu0DFgeSUH g.stateGroup rect{fill:#ECECFF;stroke:#9370DB;}#mermaid-svg-OPPuFsXu0DFgeSUH g.stateGroup line{stroke:#333333;stroke-width:1;}#mermaid-svg-OPPuFsXu0DFgeSUH .transition{stroke:#333333;stroke-width:1;fill:none;}#mermaid-svg-OPPuFsXu0DFgeSUH .stateGroup .composit{fill:white;border-bottom:1px;}#mermaid-svg-OPPuFsXu0DFgeSUH .stateGroup .alt-composit{fill:#e0e0e0;border-bottom:1px;}#mermaid-svg-OPPuFsXu0DFgeSUH .state-note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-OPPuFsXu0DFgeSUH .state-note text{fill:black;stroke:none;font-size:10px;}#mermaid-svg-OPPuFsXu0DFgeSUH .stateLabel .box{stroke:none;stroke-width:0;fill:#ECECFF;opacity:0.5;}#mermaid-svg-OPPuFsXu0DFgeSUH .edgeLabel .label rect{fill:#ECECFF;opacity:0.5;}#mermaid-svg-OPPuFsXu0DFgeSUH .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-OPPuFsXu0DFgeSUH .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-OPPuFsXu0DFgeSUH .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-OPPuFsXu0DFgeSUH .edgeLabel .label text{fill:#333;}#mermaid-svg-OPPuFsXu0DFgeSUH .label div .edgeLabel{color:#333;}#mermaid-svg-OPPuFsXu0DFgeSUH .stateLabel text{fill:#131300;font-size:10px;font-weight:bold;}#mermaid-svg-OPPuFsXu0DFgeSUH .node circle.state-start{fill:#333333;stroke:#333333;}#mermaid-svg-OPPuFsXu0DFgeSUH .node .fork-join{fill:#333333;stroke:#333333;}#mermaid-svg-OPPuFsXu0DFgeSUH .node circle.state-end{fill:#9370DB;stroke:white;stroke-width:1.5;}#mermaid-svg-OPPuFsXu0DFgeSUH .end-state-inner{fill:white;stroke-width:1.5;}#mermaid-svg-OPPuFsXu0DFgeSUH .node rect{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-OPPuFsXu0DFgeSUH .node polygon{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-OPPuFsXu0DFgeSUH #statediagram-barbEnd{fill:#333333;}#mermaid-svg-OPPuFsXu0DFgeSUH .statediagram-cluster rect{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-OPPuFsXu0DFgeSUH .cluster-label,#mermaid-svg-OPPuFsXu0DFgeSUH .nodeLabel{color:#131300;}#mermaid-svg-OPPuFsXu0DFgeSUH .statediagram-cluster rect.outer{rx:5px;ry:5px;}#mermaid-svg-OPPuFsXu0DFgeSUH .statediagram-state .divider{stroke:#9370DB;}#mermaid-svg-OPPuFsXu0DFgeSUH .statediagram-state .title-state{rx:5px;ry:5px;}#mermaid-svg-OPPuFsXu0DFgeSUH .statediagram-cluster.statediagram-cluster .inner{fill:white;}#mermaid-svg-OPPuFsXu0DFgeSUH .statediagram-cluster.statediagram-cluster-alt .inner{fill:#f0f0f0;}#mermaid-svg-OPPuFsXu0DFgeSUH .statediagram-cluster .inner{rx:0;ry:0;}#mermaid-svg-OPPuFsXu0DFgeSUH .statediagram-state rect.basic{rx:5px;ry:5px;}#mermaid-svg-OPPuFsXu0DFgeSUH .statediagram-state rect.divider{stroke-dasharray:10,10;fill:#f0f0f0;}#mermaid-svg-OPPuFsXu0DFgeSUH .note-edge{stroke-dasharray:5;}#mermaid-svg-OPPuFsXu0DFgeSUH .statediagram-note rect{fill:#fff5ad;stroke:#aaaa33;stroke-width:1px;rx:0;ry:0;}#mermaid-svg-OPPuFsXu0DFgeSUH .statediagram-note rect{fill:#fff5ad;stroke:#aaaa33;stroke-width:1px;rx:0;ry:0;}#mermaid-svg-OPPuFsXu0DFgeSUH .statediagram-note text{fill:black;}#mermaid-svg-OPPuFsXu0DFgeSUH .statediagram-note .nodeLabel{color:black;}#mermaid-svg-OPPuFsXu0DFgeSUH .statediagram .edgeLabel{color:red;}#mermaid-svg-OPPuFsXu0DFgeSUH #dependencyStart,#mermaid-svg-OPPuFsXu0DFgeSUH #dependencyEnd{fill:#333333;stroke:#333333;stroke-width:1;}#mermaid-svg-OPPuFsXu0DFgeSUH .statediagramTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-OPPuFsXu0DFgeSUH :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} claim_task() → INSERT task_runs
kanban_complete
outcome=completed
PID死亡
outcome=crashed
max_runtime超限
outcome=timed_out
无心跳
outcome=stale
exit 75
outcome=rate_limited
熔断器trip
outcome=gave_up
claim过期/orphan
outcome=reclaimed
exit 0
未调用complete
outcome=crashed
kanban_request_changes
outcome=changes_requested
active
completed
crashed
timed_out
stale
rate_limited
gave_up
reclaimed
protocol_violation
changes_requested
4. CLI入口层:kanban_command() 完整解析
4.1 命令分发流程
#mermaid-svg-RAXIxpZWWl3LDus0{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-RAXIxpZWWl3LDus0 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-RAXIxpZWWl3LDus0 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-RAXIxpZWWl3LDus0 .error-icon{fill:#552222;}#mermaid-svg-RAXIxpZWWl3LDus0 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-RAXIxpZWWl3LDus0 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-RAXIxpZWWl3LDus0 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-RAXIxpZWWl3LDus0 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-RAXIxpZWWl3LDus0 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-RAXIxpZWWl3LDus0 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-RAXIxpZWWl3LDus0 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-RAXIxpZWWl3LDus0 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-RAXIxpZWWl3LDus0 .marker.cross{stroke:#333333;}#mermaid-svg-RAXIxpZWWl3LDus0 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-RAXIxpZWWl3LDus0 p{margin:0;}#mermaid-svg-RAXIxpZWWl3LDus0 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-RAXIxpZWWl3LDus0 .cluster-label text{fill:#333;}#mermaid-svg-RAXIxpZWWl3LDus0 .cluster-label span{color:#333;}#mermaid-svg-RAXIxpZWWl3LDus0 .cluster-label span p{background-color:transparent;}#mermaid-svg-RAXIxpZWWl3LDus0 .label text,#mermaid-svg-RAXIxpZWWl3LDus0 span{fill:#333;color:#333;}#mermaid-svg-RAXIxpZWWl3LDus0 .node rect,#mermaid-svg-RAXIxpZWWl3LDus0 .node circle,#mermaid-svg-RAXIxpZWWl3LDus0 .node ellipse,#mermaid-svg-RAXIxpZWWl3LDus0 .node polygon,#mermaid-svg-RAXIxpZWWl3LDus0 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-RAXIxpZWWl3LDus0 .rough-node .label text,#mermaid-svg-RAXIxpZWWl3LDus0 .node .label text,#mermaid-svg-RAXIxpZWWl3LDus0 .image-shape .label,#mermaid-svg-RAXIxpZWWl3LDus0 .icon-shape .label{text-anchor:middle;}#mermaid-svg-RAXIxpZWWl3LDus0 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-RAXIxpZWWl3LDus0 .rough-node .label,#mermaid-svg-RAXIxpZWWl3LDus0 .node .label,#mermaid-svg-RAXIxpZWWl3LDus0 .image-shape .label,#mermaid-svg-RAXIxpZWWl3LDus0 .icon-shape .label{text-align:center;}#mermaid-svg-RAXIxpZWWl3LDus0 .node.clickable{cursor:pointer;}#mermaid-svg-RAXIxpZWWl3LDus0 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-RAXIxpZWWl3LDus0 .arrowheadPath{fill:#333333;}#mermaid-svg-RAXIxpZWWl3LDus0 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-RAXIxpZWWl3LDus0 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-RAXIxpZWWl3LDus0 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-RAXIxpZWWl3LDus0 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-RAXIxpZWWl3LDus0 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-RAXIxpZWWl3LDus0 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-RAXIxpZWWl3LDus0 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-RAXIxpZWWl3LDus0 .cluster text{fill:#333;}#mermaid-svg-RAXIxpZWWl3LDus0 .cluster span{color:#333;}#mermaid-svg-RAXIxpZWWl3LDus0 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-RAXIxpZWWl3LDus0 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-RAXIxpZWWl3LDus0 rect.text{fill:none;stroke-width:0;}#mermaid-svg-RAXIxpZWWl3LDus0 .icon-shape,#mermaid-svg-RAXIxpZWWl3LDus0 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-RAXIxpZWWl3LDus0 .icon-shape p,#mermaid-svg-RAXIxpZWWl3LDus0 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-RAXIxpZWWl3LDus0 .icon-shape .label rect,#mermaid-svg-RAXIxpZWWl3LDus0 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-RAXIxpZWWl3LDus0 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-RAXIxpZWWl3LDus0 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-RAXIxpZWWl3LDus0 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 空
有action
是
否
是
否
有
不存在
存在
无
是
否
kanban_command(args)
kanban_action?
打印帮助 / 返回0
is_delegated_child
CLI mutation?
返回错误:
子代理不能通过CLI mutate
action == 'boards'?
_dispatch_boards(args)
看板元数据管理
--board 覆盖?
_normalize_board_slug()
验证slug格式
slug != default
且board_exists?
错误:board不存在
scoped_current_board(slug)
kb.init_db()
幂等初始化
action == 'repair'?
_cmd_repair(args)
在init前分发
_HANDLERS.get(action)
handler(args)
返回exit code
4.2 子代理变异防护
python
_DELEGATED_CHILD_DENIED_ACTIONS: frozenset[str] = frozenset({
"init", "create", "swarm", "assign", "reclaim", "reassign", "link", "unlink",
"claim", "comment", "attach", "attach-rm", "complete", "edit", "block",
"schedule", "unblock", "promote", "archive", "dispatch", "daemon", "repair",
"heartbeat", "notify-subscribe", "notify-unsubscribe", "specify", "decompose",
"request-review", "request-changes", "reopen-review", "gc",
})
双层防护:
- CLI层(UX):
_is_delegated_child_cli_mutation()在kanban_command入口快速拒绝 - DB层(信任边界):
_assert_not_delegated_child_mutation()在每次write_txn前执行
注释明确指出:"The tool/CLI fast-fail guards are UX, not a trust boundary"------真正的信任边界在DB层。
4.3 核心CLI命令清单
| 命令 | 处理函数 | 作用 |
|---|---|---|
init |
_cmd_init |
初始化DB,列出可用Profile |
create |
_cmd_create |
创建任务(含workspace/branch/model/skills/goal_mode) |
swarm |
_cmd_swarm |
创建Swarm拓扑 |
list |
_cmd_list |
列出任务(含recompute_ready) |
show |
_cmd_show |
显示任务详情(含diagnostics) |
assign |
_cmd_assign |
分配任务给Profile |
heartbeat |
_cmd_heartbeat |
记录心跳 |
dispatch |
_cmd_dispatch |
手动执行一次调度tick |
daemon |
_cmd_daemon |
独立调度守护进程(已废弃) |
tail |
_cmd_tail |
实时跟踪事件流 |
gc |
_cmd_gc |
垃圾回收 |
repair |
_cmd_repair |
修复损坏DB |
boards |
_dispatch_boards |
多看板管理 |
5. SQLite看板核心:kanban_db.py
5.1 数据库Schema
#mermaid-svg-inFJ2GVTrFEQPr6u{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-inFJ2GVTrFEQPr6u .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-inFJ2GVTrFEQPr6u .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-inFJ2GVTrFEQPr6u .error-icon{fill:#552222;}#mermaid-svg-inFJ2GVTrFEQPr6u .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-inFJ2GVTrFEQPr6u .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-inFJ2GVTrFEQPr6u .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-inFJ2GVTrFEQPr6u .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-inFJ2GVTrFEQPr6u .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-inFJ2GVTrFEQPr6u .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-inFJ2GVTrFEQPr6u .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-inFJ2GVTrFEQPr6u .marker{fill:#333333;stroke:#333333;}#mermaid-svg-inFJ2GVTrFEQPr6u .marker.cross{stroke:#333333;}#mermaid-svg-inFJ2GVTrFEQPr6u svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-inFJ2GVTrFEQPr6u p{margin:0;}#mermaid-svg-inFJ2GVTrFEQPr6u .entityBox{fill:#ECECFF;stroke:#9370DB;}#mermaid-svg-inFJ2GVTrFEQPr6u .relationshipLabelBox{fill:hsl(80, 100%, 96.2745098039%);opacity:0.7;background-color:hsl(80, 100%, 96.2745098039%);}#mermaid-svg-inFJ2GVTrFEQPr6u .relationshipLabelBox rect{opacity:0.5;}#mermaid-svg-inFJ2GVTrFEQPr6u .labelBkg{background-color:rgba(248.6666666666, 255, 235.9999999999, 0.5);}#mermaid-svg-inFJ2GVTrFEQPr6u .edgeLabel .label{fill:#9370DB;font-size:14px;}#mermaid-svg-inFJ2GVTrFEQPr6u .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-inFJ2GVTrFEQPr6u .edge-pattern-dashed{stroke-dasharray:8,8;}#mermaid-svg-inFJ2GVTrFEQPr6u .node rect,#mermaid-svg-inFJ2GVTrFEQPr6u .node circle,#mermaid-svg-inFJ2GVTrFEQPr6u .node ellipse,#mermaid-svg-inFJ2GVTrFEQPr6u .node polygon{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-inFJ2GVTrFEQPr6u .relationshipLine{stroke:#333333;stroke-width:1;fill:none;}#mermaid-svg-inFJ2GVTrFEQPr6u .marker{fill:none!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-inFJ2GVTrFEQPr6u .edgeLabel{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-inFJ2GVTrFEQPr6u .edgeLabel .label rect{fill:rgba(232,232,232, 0.8);}#mermaid-svg-inFJ2GVTrFEQPr6u .edgeLabel .label text{fill:#333;}#mermaid-svg-inFJ2GVTrFEQPr6u :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 多次运行
评论
事件日志
附件
依赖关系
通知订阅
tasks
TEXT
id
PK
任务ID
TEXT
title
标题
TEXT
body
描述
TEXT
assignee
分配的Profile
TEXT
status
9态之一
TEXT
tenant
租户
INTEGER
priority
优先级
TEXT
workspace_kind
scratch/worktree/dir
TEXT
workspace_path
工作区路径
TEXT
branch_name
git分支
TEXT
created_by
创建者
INTEGER
created_at
创建时间
INTEGER
started_at
开始时间
INTEGER
completed_at
完成时间
TEXT
result
结果
INTEGER
current_run_id
当前运行ID
TEXT
model_override
模型覆盖
TEXT
provider_override
Provider覆盖
TEXT
completion_contract
完成契约
TEXT
last_failure_error
最后失败错误
INTEGER
consecutive_failures
连续失败计数
INTEGER
worker_pid
Worker PID
TEXT
claim_lock
Claim锁
INTEGER
claim_expires
Claim过期时间
INTEGER
last_heartbeat_at
最后心跳时间
INTEGER
max_runtime_seconds
最大运行时间
INTEGER
max_retries
最大重试次数
TEXT
skills
技能列表JSON
BOOLEAN
goal_mode
目标模式
INTEGER
goal_max_turns
目标最大轮次
TEXT
project_id
项目ID
TEXT
session_id
会话ID
TEXT
idempotency_key
幂等键
TEXT
reasoning_effort
推理力度
task_runs
INTEGER
id
PK
TEXT
task_id
FK
TEXT
profile
执行Profile
TEXT
status
运行状态
TEXT
outcome
结果outcome
TEXT
summary
摘要
TEXT
error
错误
TEXT
metadata
JSON元数据
INTEGER
started_at
INTEGER
ended_at
INTEGER
worker_pid
INTEGER
last_heartbeat_at
INTEGER
claim_expires
task_comments
INTEGER
id
PK
TEXT
task_id
FK
TEXT
author
作者
TEXT
body
内容
INTEGER
created_at
task_events
INTEGER
id
PK
TEXT
task_id
FK
TEXT
kind
事件类型
TEXT
payload
JSON载荷
INTEGER
created_at
INTEGER
run_id
关联运行
attachments
TEXT
id
PK
TEXT
task_id
FK
TEXT
filename
TEXT
content_type
INTEGER
size
TEXT
uploaded_by
TEXT
stored_path
INTEGER
created_at
task_links
TEXT
parent_id
FK
TEXT
child_id
FK
kanban_notify_subs
INTEGER
id
PK
TEXT
task_id
FK
TEXT
platform
telegram/tui/api_server
TEXT
chat_id
TEXT
thread_id
TEXT
user_id
TEXT
user_id_alt
TEXT
chat_type
TEXT
notifier_profile
TEXT
delivery_mode
notify/notify+wake/wake
TEXT
delivery_metadata
JSON
INTEGER
created_at
INTEGER
last_event_id
游标
5.2 Board解析链
python
# kanban_db.py L1-9 docstring:
# Board resolution: board= arg > HERMES_KANBAN_BOARD > HERMES_KANBAN_DB
# (pins the file path) > <root>/kanban/current > default
#mermaid-svg-XwZEGiFyCBa1Z6UL{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-XwZEGiFyCBa1Z6UL .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-XwZEGiFyCBa1Z6UL .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-XwZEGiFyCBa1Z6UL .error-icon{fill:#552222;}#mermaid-svg-XwZEGiFyCBa1Z6UL .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-XwZEGiFyCBa1Z6UL .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-XwZEGiFyCBa1Z6UL .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-XwZEGiFyCBa1Z6UL .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-XwZEGiFyCBa1Z6UL .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-XwZEGiFyCBa1Z6UL .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-XwZEGiFyCBa1Z6UL .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-XwZEGiFyCBa1Z6UL .marker{fill:#333333;stroke:#333333;}#mermaid-svg-XwZEGiFyCBa1Z6UL .marker.cross{stroke:#333333;}#mermaid-svg-XwZEGiFyCBa1Z6UL svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-XwZEGiFyCBa1Z6UL p{margin:0;}#mermaid-svg-XwZEGiFyCBa1Z6UL .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-XwZEGiFyCBa1Z6UL .cluster-label text{fill:#333;}#mermaid-svg-XwZEGiFyCBa1Z6UL .cluster-label span{color:#333;}#mermaid-svg-XwZEGiFyCBa1Z6UL .cluster-label span p{background-color:transparent;}#mermaid-svg-XwZEGiFyCBa1Z6UL .label text,#mermaid-svg-XwZEGiFyCBa1Z6UL span{fill:#333;color:#333;}#mermaid-svg-XwZEGiFyCBa1Z6UL .node rect,#mermaid-svg-XwZEGiFyCBa1Z6UL .node circle,#mermaid-svg-XwZEGiFyCBa1Z6UL .node ellipse,#mermaid-svg-XwZEGiFyCBa1Z6UL .node polygon,#mermaid-svg-XwZEGiFyCBa1Z6UL .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-XwZEGiFyCBa1Z6UL .rough-node .label text,#mermaid-svg-XwZEGiFyCBa1Z6UL .node .label text,#mermaid-svg-XwZEGiFyCBa1Z6UL .image-shape .label,#mermaid-svg-XwZEGiFyCBa1Z6UL .icon-shape .label{text-anchor:middle;}#mermaid-svg-XwZEGiFyCBa1Z6UL .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-XwZEGiFyCBa1Z6UL .rough-node .label,#mermaid-svg-XwZEGiFyCBa1Z6UL .node .label,#mermaid-svg-XwZEGiFyCBa1Z6UL .image-shape .label,#mermaid-svg-XwZEGiFyCBa1Z6UL .icon-shape .label{text-align:center;}#mermaid-svg-XwZEGiFyCBa1Z6UL .node.clickable{cursor:pointer;}#mermaid-svg-XwZEGiFyCBa1Z6UL .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-XwZEGiFyCBa1Z6UL .arrowheadPath{fill:#333333;}#mermaid-svg-XwZEGiFyCBa1Z6UL .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-XwZEGiFyCBa1Z6UL .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-XwZEGiFyCBa1Z6UL .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-XwZEGiFyCBa1Z6UL .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-XwZEGiFyCBa1Z6UL .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-XwZEGiFyCBa1Z6UL .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-XwZEGiFyCBa1Z6UL .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-XwZEGiFyCBa1Z6UL .cluster text{fill:#333;}#mermaid-svg-XwZEGiFyCBa1Z6UL .cluster span{color:#333;}#mermaid-svg-XwZEGiFyCBa1Z6UL div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-XwZEGiFyCBa1Z6UL .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-XwZEGiFyCBa1Z6UL rect.text{fill:none;stroke-width:0;}#mermaid-svg-XwZEGiFyCBa1Z6UL .icon-shape,#mermaid-svg-XwZEGiFyCBa1Z6UL .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-XwZEGiFyCBa1Z6UL .icon-shape p,#mermaid-svg-XwZEGiFyCBa1Z6UL .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-XwZEGiFyCBa1Z6UL .icon-shape .label rect,#mermaid-svg-XwZEGiFyCBa1Z6UL .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-XwZEGiFyCBa1Z6UL .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-XwZEGiFyCBa1Z6UL .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-XwZEGiFyCBa1Z6UL :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 有
无
有
无
有
无
有
无
是
否
Board解析
board= 参数?
使用参数slug
HERMES_KANBAN_BOARD
环境变量?
使用env slug
HERMES_KANBAN_DB
环境变量(pin路径)?
使用env DB路径
/kanban/current
文件存在?
使用current文件中的slug
使用'default'
kanban_db_path()
slug == 'default'?
/kanban.db
(向后兼容)
/kanban/boards//kanban.db
关键设计:
default看板DB在<root>/kanban.db(向后兼容)- 其他看板在
<root>/kanban/boards/<slug>/kanban.db - 一个看板的worker永远看不到另一个看板
- 调度器注入这些环境变量到worker进程中
5.3 Claim机制(compare-and-swap)
python
# kanban_db.py 核心claim逻辑(通过write_txn + CAS)
# SQLite序列化写入者:一个claimer赢,输者看到0行
# 无重试,无分布式锁
def claim_task(conn, task_id, *, ttl_seconds=None):
"""原子claim一个ready任务→running"""
claim_lock = _host_prefix() + secrets.token_hex(8)
claim_expires = int(time.time()) + _resolve_claim_ttl_seconds(ttl_seconds)
with write_txn(conn):
# CAS: 只在status='ready' AND claim_lock IS NULL时更新
cur = conn.execute(
"UPDATE tasks SET status='running', claim_lock=?, claim_expires=?, "
"started_at=COALESCE(started_at, ?) "
"WHERE id=? AND status='ready' AND claim_lock IS NULL",
(claim_lock, claim_expires, int(time.time()), task_id))
if cur.rowcount != 1:
return None # 被别人claim了或状态已变
# 创建新的task_runs行
run_id = _start_run(conn, task_id, ...)
return get_task(conn, task_id)
Claim TTL参数:
DEFAULT_CLAIM_TTL_SECONDS = 15 * 60(15分钟)- Worker通过
heartbeat_claim延长claim HERMES_KANBAN_CLAIM_TTL_SECONDS环境变量覆盖
5.4 build_worker_context()
调度器在spawn时为worker构建上下文文本,注入到system prompt:
python
def build_worker_context(conn, task_id) -> str:
"""构建worker的Kanban上下文文本"""
# 包含:任务标题/描述、parent/child信息、最近N次prior runs的错误、
# 最近N条comments、completion_contract、workspace信息
# 截断预算:
# _CTX_MAX_PRIOR_ATTEMPTS = 10 # 最多展示10次先前运行
# _CTX_MAX_COMMENTS = 30 # 最多展示30条评论
# _CTX_MAX_FIELD_BYTES = 4*1024 # 每个summary/error/metadata/result
# _CTX_MAX_BODY_BYTES = 8*1024 # 每个task.body
# _CTX_MAX_COMMENT_BYTES = 2*1024 # 每条评论
6. 连接层:WAL/锁/迁移/损坏检测
6.1 连接配置
python
DEFAULT_BUSY_TIMEOUT_MS = 120_000 # 2分钟busy timeout
def _sqlite_connect(path: Path) -> sqlite3.Connection:
conn = connect_tracked(path, # 注册连接,拒绝字节级文件探测
connect_fn=sqlite3.connect,
isolation_level=None, # 手动事务控制
timeout=busy_timeout_ms / 1000.0)
conn.execute(f"PRAGMA busy_timeout={busy_timeout_ms}")
return conn
关键设计:
- WAL模式 :
BEGIN IMMEDIATE序列化写入者,WAL让读取者不阻塞 - 长busy timeout:Worker stampede是预期的,120秒让WAL序列化写入者
- connect_tracked :注册连接到进程级注册表,
sqlite_safe_read的字节级探测被拒绝(因为open/close会取消POSIX advisory锁)
6.2 三层锁
#mermaid-svg-isKimh8y46TtV1GN{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-isKimh8y46TtV1GN .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-isKimh8y46TtV1GN .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-isKimh8y46TtV1GN .error-icon{fill:#552222;}#mermaid-svg-isKimh8y46TtV1GN .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-isKimh8y46TtV1GN .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-isKimh8y46TtV1GN .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-isKimh8y46TtV1GN .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-isKimh8y46TtV1GN .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-isKimh8y46TtV1GN .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-isKimh8y46TtV1GN .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-isKimh8y46TtV1GN .marker{fill:#333333;stroke:#333333;}#mermaid-svg-isKimh8y46TtV1GN .marker.cross{stroke:#333333;}#mermaid-svg-isKimh8y46TtV1GN svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-isKimh8y46TtV1GN p{margin:0;}#mermaid-svg-isKimh8y46TtV1GN .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-isKimh8y46TtV1GN .cluster-label text{fill:#333;}#mermaid-svg-isKimh8y46TtV1GN .cluster-label span{color:#333;}#mermaid-svg-isKimh8y46TtV1GN .cluster-label span p{background-color:transparent;}#mermaid-svg-isKimh8y46TtV1GN .label text,#mermaid-svg-isKimh8y46TtV1GN span{fill:#333;color:#333;}#mermaid-svg-isKimh8y46TtV1GN .node rect,#mermaid-svg-isKimh8y46TtV1GN .node circle,#mermaid-svg-isKimh8y46TtV1GN .node ellipse,#mermaid-svg-isKimh8y46TtV1GN .node polygon,#mermaid-svg-isKimh8y46TtV1GN .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-isKimh8y46TtV1GN .rough-node .label text,#mermaid-svg-isKimh8y46TtV1GN .node .label text,#mermaid-svg-isKimh8y46TtV1GN .image-shape .label,#mermaid-svg-isKimh8y46TtV1GN .icon-shape .label{text-anchor:middle;}#mermaid-svg-isKimh8y46TtV1GN .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-isKimh8y46TtV1GN .rough-node .label,#mermaid-svg-isKimh8y46TtV1GN .node .label,#mermaid-svg-isKimh8y46TtV1GN .image-shape .label,#mermaid-svg-isKimh8y46TtV1GN .icon-shape .label{text-align:center;}#mermaid-svg-isKimh8y46TtV1GN .node.clickable{cursor:pointer;}#mermaid-svg-isKimh8y46TtV1GN .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-isKimh8y46TtV1GN .arrowheadPath{fill:#333333;}#mermaid-svg-isKimh8y46TtV1GN .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-isKimh8y46TtV1GN .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-isKimh8y46TtV1GN .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-isKimh8y46TtV1GN .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-isKimh8y46TtV1GN .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-isKimh8y46TtV1GN .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-isKimh8y46TtV1GN .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-isKimh8y46TtV1GN .cluster text{fill:#333;}#mermaid-svg-isKimh8y46TtV1GN .cluster span{color:#333;}#mermaid-svg-isKimh8y46TtV1GN div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-isKimh8y46TtV1GN .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-isKimh8y46TtV1GN rect.text{fill:none;stroke-width:0;}#mermaid-svg-isKimh8y46TtV1GN .icon-shape,#mermaid-svg-isKimh8y46TtV1GN .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-isKimh8y46TtV1GN .icon-shape p,#mermaid-svg-isKimh8y46TtV1GN .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-isKimh8y46TtV1GN .icon-shape .label rect,#mermaid-svg-isKimh8y46TtV1GN .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-isKimh8y46TtV1GN .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-isKimh8y46TtV1GN .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-isKimh8y46TtV1GN :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 层3: SQLite事务
层2: dispatch tick锁
层1: 跨进程init锁
_cross_process_init_lock
.init.lock文件
序列化首次WAL/schema/integrity设置
10秒超时→不阻塞 proceed
_dispatch_tick_lock
.dispatch.lock文件
非阻塞单写者守卫
两个调度器不能同时tick一个看板
输者skip不等待
write_txn
BEGIN IMMEDIATE
SQLite序列化写入者
120秒busy timeout
dispatch tick锁的动机(#35240):
hermes gateway run --replace/gateway restart可能留下orphan gateway- orphan gateway的调度器逃离service cgroup,成为第二个长期写入者
- 启动守卫阻止常见的orphan产生方式
- tick锁是纵深防御:无论第二个调度器如何出现,都不能同时写
6.3 write_txn边界
python
@contextlib.contextmanager
def write_txn(conn):
"""BEGIN IMMEDIATE → yield → COMMIT/ROLLBACK"""
conn.execute("BEGIN IMMEDIATE")
try:
yield
conn.execute("COMMIT")
except:
conn.execute("ROLLBACK")
raise
所有DB写入都必须通过write_txn。BEGIN IMMEDIATE立即获取保留锁(reserved lock),确保只有一个写入者。
6.4 损坏检测与隔离
python
_CORRUPT_BACKUP_RETENTION = 10 # 最多保留10个.corrupt.<hash>.bak
# 检测:_sqlite_connect后执行PRAGMA integrity_check
# 检测到损坏→quarantine(重命名为.corrupt.<hash>.bak)→修复或重建
7. 调度器核心:dispatch_once() 逐阶段解析
7.1 dispatch_once() 总体结构
_fire_dispatch_tick_hook _dispatch_once_locked() _dispatch_tick_lock dispatch_once() 网关/daemon _fire_dispatch_tick_hook _dispatch_once_locked() _dispatch_tick_lock dispatch_once() 网关/daemon #mermaid-svg-YHdLbsjHV15Vh6Ga{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-YHdLbsjHV15Vh6Ga .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-YHdLbsjHV15Vh6Ga .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-YHdLbsjHV15Vh6Ga .error-icon{fill:#552222;}#mermaid-svg-YHdLbsjHV15Vh6Ga .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-YHdLbsjHV15Vh6Ga .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-YHdLbsjHV15Vh6Ga .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-YHdLbsjHV15Vh6Ga .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-YHdLbsjHV15Vh6Ga .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-YHdLbsjHV15Vh6Ga .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-YHdLbsjHV15Vh6Ga .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-YHdLbsjHV15Vh6Ga .marker{fill:#333333;stroke:#333333;}#mermaid-svg-YHdLbsjHV15Vh6Ga .marker.cross{stroke:#333333;}#mermaid-svg-YHdLbsjHV15Vh6Ga svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-YHdLbsjHV15Vh6Ga p{margin:0;}#mermaid-svg-YHdLbsjHV15Vh6Ga .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-YHdLbsjHV15Vh6Ga text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-YHdLbsjHV15Vh6Ga .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-YHdLbsjHV15Vh6Ga .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-YHdLbsjHV15Vh6Ga .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-YHdLbsjHV15Vh6Ga .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-YHdLbsjHV15Vh6Ga #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-YHdLbsjHV15Vh6Ga .sequenceNumber{fill:white;}#mermaid-svg-YHdLbsjHV15Vh6Ga #sequencenumber{fill:#333;}#mermaid-svg-YHdLbsjHV15Vh6Ga #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-YHdLbsjHV15Vh6Ga .messageText{fill:#333;stroke:none;}#mermaid-svg-YHdLbsjHV15Vh6Ga .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-YHdLbsjHV15Vh6Ga .labelText,#mermaid-svg-YHdLbsjHV15Vh6Ga .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-YHdLbsjHV15Vh6Ga .loopText,#mermaid-svg-YHdLbsjHV15Vh6Ga .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-YHdLbsjHV15Vh6Ga .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-YHdLbsjHV15Vh6Ga .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-YHdLbsjHV15Vh6Ga .noteText,#mermaid-svg-YHdLbsjHV15Vh6Ga .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-YHdLbsjHV15Vh6Ga .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-YHdLbsjHV15Vh6Ga .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-YHdLbsjHV15Vh6Ga .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-YHdLbsjHV15Vh6Ga .actorPopupMenu{position:absolute;}#mermaid-svg-YHdLbsjHV15Vh6Ga .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-YHdLbsjHV15Vh6Ga .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-YHdLbsjHV15Vh6Ga .actor-man circle,#mermaid-svg-YHdLbsjHV15Vh6Ga line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-YHdLbsjHV15Vh6Ga :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 阶段1: 回收 阶段2: 预算 阶段3: spawn ready lane loop 每个ready行 阶段4: spawn review lane loop 每个review行 alt 获得锁 未获得锁 锁释放后触发 慢订阅者不阻塞其他调度器 dispatch_once(conn, ...) _dispatch_tick_lock(db_path) held=True _dispatch_once_locked(conn, ...) reap_worker_zombies() release_stale_claims() reconcile_orphaned_running() detect_stale_running() detect_crashed_workers() enforce_max_runtime() recompute_ready() _tick_spawn_budget() _memory_pressure_level() _lane_rows("ready") _dispatch_lane_task(lane="ready") _lane_rows("review") _dispatch_lane_task(lane="review") DispatchResult _maybe_checkpoint_wal() held=False DispatchResult(skipped_locked=True) _fire_dispatch_tick_hook(result) DispatchResult
7.2 阶段1:回收 (_run_reclaim_phase)
python
def _run_reclaim_phase(conn, result, *, stale_timeout_seconds, failure_limit, reconcile_orphans):
reap_worker_zombies() # 回收僵尸子进程(os.waitpid WNOHANG)
result.reclaimed = _kb.release_stale_claims(conn) # 释放过期claim
if reconcile_orphans:
result.reconciled_orphans = reconcile_orphaned_running(conn) # 调和孤儿running
result.stale = detect_stale_running(conn, stale_timeout_seconds=stale_timeout_seconds) # 无心跳
result.crashed = detect_crashed_workers(conn) # PID死亡
result.auto_blocked.extend(getattr(detect_crashed_workers, "_last_auto_blocked", []))
result.rate_limited.extend(getattr(detect_crashed_workers, "_last_rate_limited", []))
result.timed_out = enforce_max_runtime(conn) # 超时终止
result.promoted = _kb.recompute_ready(conn, failure_limit=failure_limit) # todo→ready提升
7.3 阶段2:预算计算 (_tick_spawn_budget)
#mermaid-svg-HNLHIM77IDDTN7NU{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-HNLHIM77IDDTN7NU .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-HNLHIM77IDDTN7NU .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-HNLHIM77IDDTN7NU .error-icon{fill:#552222;}#mermaid-svg-HNLHIM77IDDTN7NU .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-HNLHIM77IDDTN7NU .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-HNLHIM77IDDTN7NU .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-HNLHIM77IDDTN7NU .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-HNLHIM77IDDTN7NU .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-HNLHIM77IDDTN7NU .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-HNLHIM77IDDTN7NU .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-HNLHIM77IDDTN7NU .marker{fill:#333333;stroke:#333333;}#mermaid-svg-HNLHIM77IDDTN7NU .marker.cross{stroke:#333333;}#mermaid-svg-HNLHIM77IDDTN7NU svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-HNLHIM77IDDTN7NU p{margin:0;}#mermaid-svg-HNLHIM77IDDTN7NU .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-HNLHIM77IDDTN7NU .cluster-label text{fill:#333;}#mermaid-svg-HNLHIM77IDDTN7NU .cluster-label span{color:#333;}#mermaid-svg-HNLHIM77IDDTN7NU .cluster-label span p{background-color:transparent;}#mermaid-svg-HNLHIM77IDDTN7NU .label text,#mermaid-svg-HNLHIM77IDDTN7NU span{fill:#333;color:#333;}#mermaid-svg-HNLHIM77IDDTN7NU .node rect,#mermaid-svg-HNLHIM77IDDTN7NU .node circle,#mermaid-svg-HNLHIM77IDDTN7NU .node ellipse,#mermaid-svg-HNLHIM77IDDTN7NU .node polygon,#mermaid-svg-HNLHIM77IDDTN7NU .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-HNLHIM77IDDTN7NU .rough-node .label text,#mermaid-svg-HNLHIM77IDDTN7NU .node .label text,#mermaid-svg-HNLHIM77IDDTN7NU .image-shape .label,#mermaid-svg-HNLHIM77IDDTN7NU .icon-shape .label{text-anchor:middle;}#mermaid-svg-HNLHIM77IDDTN7NU .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-HNLHIM77IDDTN7NU .rough-node .label,#mermaid-svg-HNLHIM77IDDTN7NU .node .label,#mermaid-svg-HNLHIM77IDDTN7NU .image-shape .label,#mermaid-svg-HNLHIM77IDDTN7NU .icon-shape .label{text-align:center;}#mermaid-svg-HNLHIM77IDDTN7NU .node.clickable{cursor:pointer;}#mermaid-svg-HNLHIM77IDDTN7NU .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-HNLHIM77IDDTN7NU .arrowheadPath{fill:#333333;}#mermaid-svg-HNLHIM77IDDTN7NU .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-HNLHIM77IDDTN7NU .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-HNLHIM77IDDTN7NU .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-HNLHIM77IDDTN7NU .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-HNLHIM77IDDTN7NU .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-HNLHIM77IDDTN7NU .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-HNLHIM77IDDTN7NU .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-HNLHIM77IDDTN7NU .cluster text{fill:#333;}#mermaid-svg-HNLHIM77IDDTN7NU .cluster span{color:#333;}#mermaid-svg-HNLHIM77IDDTN7NU div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-HNLHIM77IDDTN7NU .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-HNLHIM77IDDTN7NU rect.text{fill:none;stroke-width:0;}#mermaid-svg-HNLHIM77IDDTN7NU .icon-shape,#mermaid-svg-HNLHIM77IDDTN7NU .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-HNLHIM77IDDTN7NU .icon-shape p,#mermaid-svg-HNLHIM77IDDTN7NU .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-HNLHIM77IDDTN7NU .icon-shape .label rect,#mermaid-svg-HNLHIM77IDDTN7NU .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-HNLHIM77IDDTN7NU .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-HNLHIM77IDDTN7NU .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-HNLHIM77IDDTN7NU :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 是
是
否
否
是
是
否
否
critical
elevated
ok/unknown
_tick_spawn_budget()
count_running_tasks(conn)
当前running计数
max_spawn
已配置?
running >= max_spawn?
return (False, None)
不spawn
spawn_budget = max_spawn - running
max_in_progress
已配置?
count_running_tasks_other_boards()
其他看板的running
total = running + other_boards
total >= max_in_progress?
remaining = max_in_progress - total
spawn_budget = min(spawn_budget, remaining)
_memory_pressure_level()
pressure?
spawn_budget = min(spawn_budget, 1)
最多1个新worker
return (True, spawn_budget)
关键语义:
max_spawn是per-board的并发上限(running + 本次spawn),不是per-tick预算max_in_progress是host级上限(所有看板的running都算)- 内存critical→不spawn;elevated→最多1个
7.4 阶段3:_dispatch_lane_task() 逐行处理
python
def _dispatch_lane_task(conn, row, assignee, result, *, lane, ...):
task_id = row["id"]
# 1. Profile存在性检查
profile_exists = _profile_exists_fn()
if profile_exists and not profile_exists(assignee):
result.skipped_nonspawnable.append(task_id) # 控制面lane,不可spawn
return False
# 2. Per-profile并发上限
if per_profile_cap is not None:
current = per_profile_running.get(assignee, 0)
if current >= per_profile_cap:
result.skipped_per_profile_capped.append((task_id, assignee, current))
return False
# 3. Respawn守卫
guard_reason = check_respawn_guard(conn, task_id, lane=lane)
if guard_reason is not None:
result.respawn_guarded.append((task_id, guard_reason))
if not dry_run:
with _kb.write_txn(conn):
_kb._append_event(conn, task_id, "respawn_guarded", {"reason": guard_reason})
return False
# 4. Claim(CAS)
claim = _kb.claim_review_task if lane == "review" else _kb.claim_task
claimed = claim(conn, task_id, ttl_seconds=ttl_seconds)
if claimed is None:
return False # 被别人claim了
# 5. 工作区解析
try:
if claimed.workspace_kind == "worktree":
workspace, branch = _kbw._resolve_worktree_workspace(claimed, board=board)
else:
workspace = _kbw.resolve_workspace(claimed, board=board)
except Exception as exc:
_record_task_failure(conn, claimed.id, f"workspace: {exc}", outcome="spawn_failed", ...)
return False
# 6. 设置工作区路径
_kbw.set_workspace_path(conn, claimed.id, str(workspace))
# 7. Review lane追加sdlc-review skill
if lane == "review":
claimed.skills = list(dict.fromkeys([*(claimed.skills or []), "sdlc-review"]))
# 8. Spawn
try:
pid = _call_spawn_fn(spawn_fn or _default_spawn, claimed, str(workspace), board)
if pid:
_set_worker_pid(conn, claimed.id, int(pid))
_kb._fire_worker_spawned_hook(conn, claimed, str(workspace), pid, board=board)
result.spawned.append((claimed.id, claimed.assignee, str(workspace)))
return True
except Exception as exc:
_record_task_failure(conn, claimed.id, str(exc), outcome="spawn_failed", ...)
return False
7.5 Review lane预留
python
# _dispatch_once_locked L1786-1791:
ready_budget = spawn_budget
if spawn_budget is not None and spawn_budget > 0 and _any_spawnable_review(review_rows):
ready_budget = max(spawn_budget - 1, 0) # 为review预留1个slot
问题:ready loop先运行,会消耗整个共享预算,在有持续ready积压时饿死review。解决方案:当有可spawn的review工作且有预算时,为ready lane减1。