06 · Issue 状态机与原子认领¶
一句话结论: Paperclip 把 Issue 的业务状态、责任人、checkout run 和 execution run 分开保存,并用数据库条件更新竞争所有权;冲突、陈旧锁和释放都有显式语义,而不是靠 Agent 之间“礼貌协商”。
1. 状态不是看板颜色¶
Issue 状态集合:
更准确的业务语义如下:
| 状态 | 系统含义 | 不应被解释为 |
|---|---|---|
backlog |
尚未承诺进入执行队列 | 只是低优先级 todo |
todo |
已准备,可被受让人/调度执行 | 已被某个 run 占有 |
in_progress |
存在明确 owner,应有 live execution/wait/recovery | 只要曾经启动过 |
in_review |
执行产出等待定义好的 review participant/path | Agent 写了“请 review” |
blocked |
当前不能推进,并有可路由的 unblock owner/action | 无信息地停止 |
done |
工作与所需证据已完成 | 某次 run exit code 为 0 |
cancelled |
明确终止,不再期待完成 | 等价于依赖满足 |
done 和 cancelled 都是 terminal,但在 blocker 语义中不同:被取消的上游不能自动证明下游所需前置条件已经成立。
2. 四个“谁”字段¶
理解并发前,先区分:
| 字段 | 回答的问题 | 生命周期 |
|---|---|---|
assigneeAgentId / assigneeUserId |
谁对任务负责 | 可以跨多个 run 保持 |
checkoutRunId |
哪次 run 取得任务 checkout | 通常随当前执行尝试 |
executionRunId |
哪次 run 拥有执行锁 | 绑定 live/terminal run 状态 |
executionAgentNameKey + executionLockedAt |
锁归属辅助信息和陈旧判断 | recovery 可清理/接管 |
把它们压成一个 ownerId 会失去关键状态:责任仍归 Agent A,但旧 run 已结束,新 run 需要安全续接。
3. Checkout 的前置检查¶
issueService.checkout() 在尝试写数据库前先验证:
- 加载 Issue 和 Company;
- 目标 Agent 仍可被分配;
- Issue 所在树没有 pause hold;
- 清理已确认 terminal 的陈旧 checkout/execution lock;
- 解析 blocker readiness;
- 若上游
done但 workspace finalize 尚未 settled,仍不能认领下游; - cancelled/pending dependency 等情况返回明确阻断。
这说明“看状态是 todo 就 UPDATE”远远不够。认领是对任务图、运行状态与 workspace 生命周期的联合判断。
4. 原子条件更新¶
核心竞争可以简化为:
UPDATE issues
SET
assignee_agent_id = :agent,
assignee_user_id = NULL,
checkout_run_id = :run,
execution_run_id = :run,
status = 'in_progress',
started_at = COALESCE(started_at, now())
WHERE id = :issue
AND status IN (:expected_statuses)
AND (assignee_agent_id IS NULL OR assignee_agent_id = :agent)
AND (checkout_run_id IS NULL OR checkout_run_id = :run)
AND (execution_run_id IS NULL OR execution_run_id = :run)
RETURNING *;
真实表达式还会处理 nullable/当前锁,但关键点是:读到的前置条件和写入在同一条 UPDATE 中竞争。
sequenceDiagram
participant A as Agent Run A
participant B as Agent Run B
participant DB as PostgreSQL
par simultaneous checkout
A->>DB: conditional UPDATE (run=A)
B->>DB: conditional UPDATE (run=B)
end
DB-->>A: 1 row returned
DB-->>B: 0 rows returned
A->>A: owns checkout
B->>DB: load current fact
DB-->>B: locked by A
B->>B: 409 Conflict / explicit adoption branch
如果先 SELECT 再无条件 UPDATE,两个请求都可能读到“空闲”并随后覆盖彼此。
5. 0 行更新后不是立即报错¶
条件更新失败只说明“数据库事实已变化”,不说明一定是非法。服务会重新加载当前值,依次判断几个合法情况。
5.1 同 run 幂等重试¶
如果 assignee 和锁已经是当前 Agent/Run,说明上一次请求可能已成功但响应丢失;直接返回当前 Issue,而不是 409。
5.2 接管无 owner 的 checkout¶
某些中间状态可能存在 checkout/execution 信息但缺少责任人。只有满足严格条件时,当前 Agent 才能补全归属。
5.3 接管 stale checkout¶
系统检查旧 checkoutRunId 对应 run:
- 不存在;或
- 已经 terminal;或
- 经 recovery 确认不再 live。
才允许 adoption。仅凭 executionLockedAt 很久以前并不足够,因为长任务也可能真实运行。
5.4 接管 stale execution lock¶
类似地,旧 execution run 必须被证明 terminal/missing,不能因为新请求更“新”就抢占。
5.5 真冲突¶
剩余情况返回 409,并携带可用的当前状态,让调用方刷新/等待,而不是盲重试。
6. 为什么锁要引用 Run¶
如果锁只写 Agent ID:
- Agent 同时有多个 wakeup 时无法区分哪次执行拥有任务;
- 旧进程结束后,新 run 不能判断是否可接管;
- server restart 后无法把进程证据和锁对应;
- stop/cancel 可能杀错一轮执行。
Run ID 是一次执行的稳定事务身份,可以连接 process metadata、日志、cost、workspace lease 和最终状态。
7. Release 的保守语义¶
普通 release 使用 transaction 和 SELECT ... FOR UPDATE 锁住 Issue,再检查:
- 发起者确实是 owner;
- run ID 匹配;
- 不会释放其他仍 live 的执行。
典型结果是:
in_progress → todo
assignee → null(依据释放语义)
checkoutRunId → null
executionRunId → null
execution lock metadata → clear
另有 admin force release,用于操作者在已确认异常时清除锁,并可选择是否清 assignee。它是治理工具,不应成为普通 recovery 快捷方式。
8. 依赖不是父子关系¶
Issue relation 当前核心类型是 blocks。判断下游是否 ready 时,不只看上游状态:
flowchart TD
U["上游 Issue"] --> S{"status?"}
S -->|done| W{"workspace finalize settled?"}
S -->|cancelled| C["不满足依赖:需要显式处置"]
S -->|other| B["仍被阻塞"]
W -->|yes| R["dependency ready"]
W -->|no| P["等待 finalize"]
为什么 done 后仍等 finalize?因为代码或文件同步可能在 Agent 结束后才完成。过早启动下游会读取到未同步的 workspace。
9. Pause hold 是树级控制面¶
Issue tree hold 可以暂停一个子树,防止新 checkout/wakeup 继续推进。它与把每个 Issue 改成 blocked 不同:
- 保留原有业务状态;
- 可统一释放;
- 对 scheduler/checkout/recovery 都是明确抑制条件;
- hold membership 记录受影响节点。
这是“运维暂停”与“业务阻塞”分离的例子。
10. in_progress 的 liveness 合约¶
一个非 terminal、Agent-owned 的活动 Issue 必须至少有一条:
- live run(queued/running/scheduled retry);
- durable wakeup request;
- 明确的等待条件与可触发 continuation;
- recovery action / recovery issue;
- 可路由的 blocked/review path。
以下都不算 liveness:
- 日志最后一行写着“仍在处理”;
- 数据库留有 PID,但进程已死;
- comment 写“等待”;
- monitor 曾经触发过但没有重新 arm;
- checkoutRunId 非空但 run 已 terminal。
这套合约让 recovery scanner 可以从数据库事实判断“任务是不是被遗忘”。
11. Review 与 Blocked 都要求“下一跳”¶
in_review 不是任意休眠状态。Execution review policy 会记录谁是 participant、什么条件可以离开 review。
blocked 则应包含 unblock descriptor:
- owner 是谁;
- 需要什么动作/事件;
- 完成后怎样唤醒或重试;
- 是否属于人类审批、上游任务、环境修复或外部回调。
无下一跳的状态会被 liveness/recovery 视为问题,而不是“合法停止”。
12. Comments 与 completion wake¶
评论既是沟通,也可能是调度事件:
- @mention 可触发目标 Agent wake;
- direct-report 的特定汇报可能触发 manager;
- 任务完成可以产生 first-class completion wake;
- interaction resolution 按 continuation policy 唤醒 assignee。
服务不会仅凭评论文本授予权限。来源、actor、run context 和 trust policy 仍要校验,防止不可信输入伪造“老板批准”。
13. Monitor 是 one-shot,不是永久看守¶
Issue monitor 被设计为一次性等待:条件触发后,要继续监控必须显式 rearm。这样可以避免旧监控条件在后续状态变化中反复触发。
与之相对:
- task watchdog 关注整棵停止子树;
- silent-run watchdog 关注运行中进程无输出;
- liveness recovery 关注 agent-owned issue 无 live path。
三个机制名字接近,触发条件却完全不同。
14. 典型竞态与处理¶
| 竞态 | 防线 |
|---|---|
| 两个 Agent 同时 checkout | 单条条件 UPDATE,只有一个返回行 |
| 同一请求响应丢失后重试 | same-run 幂等成功 |
| 旧 run terminal 但锁未清 | 加载 run 事实后 stale adoption |
| 旧 run 仍活跃,新 run 想抢 | 409,不按时间强抢 |
| release 与状态更新竞争 | transaction + row lock + owner/run 校验 |
| blocker done 但文件未同步 | 等 workspace finalize settled |
| cancelled 被误当成功依赖 | 单独语义,要求显式处置 |
server 崩溃留下 in_progress |
recovery 扫描、重试/恢复、显式 notice |
15. 可复用经验¶
- “单一 assignee”要由条件写和数据库事实保证。
- 把责任所有权、checkout 与 execution lock 分字段建模。
- 锁应引用 execution attempt,而不仅是 worker/agent。
- CAS 失败后重新读取事实,再区分幂等、可接管和真冲突。
- Dependency readiness 应包含交付同步完成,而不只看业务状态。
- 活动任务必须有 machine-checkable live path;文本承诺不是 liveness。
源码入口¶
packages/db/src/schema/issues.tspackages/db/src/schema/issue_relations.tspackages/shared/src/constants.ts:Issue 状态、work mode、originserver/src/services/issues.ts :: checkout / release / adminForceReleaseserver/src/routes/issues.ts :: POST /issues/:id/checkoutserver/src/services/issue-liveness.tsdoc/execution-semantics.md:正式执行语义
Issue 被安全认领后,下一步是如何真正运行:07 · Heartbeat 调度与运行闭环。