跳转至

06 · Issue 状态机与原子认领

Issue LifecycleAtomic CheckoutCASDependencyLiveness Contract

一句话结论: Paperclip 把 Issue 的业务状态、责任人、checkout run 和 execution run 分开保存,并用数据库条件更新竞争所有权;冲突、陈旧锁和释放都有显式语义,而不是靠 Agent 之间“礼貌协商”。

1. 状态不是看板颜色

Issue 状态集合:

backlog → todo → in_progress → in_review → done
                       ↘ blocked
任意合适状态 → cancelled

更准确的业务语义如下:

状态 系统含义 不应被解释为
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() 在尝试写数据库前先验证:

  1. 加载 Issue 和 Company;
  2. 目标 Agent 仍可被分配;
  3. Issue 所在树没有 pause hold;
  4. 清理已确认 terminal 的陈旧 checkout/execution lock;
  5. 解析 blocker readiness;
  6. 若上游 done 但 workspace finalize 尚未 settled,仍不能认领下游;
  7. 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 必须至少有一条:

  1. live run(queued/running/scheduled retry);
  2. durable wakeup request;
  3. 明确的等待条件与可触发 continuation;
  4. recovery action / recovery issue;
  5. 可路由的 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. 可复用经验

  1. “单一 assignee”要由条件写和数据库事实保证。
  2. 把责任所有权、checkout 与 execution lock 分字段建模。
  3. 锁应引用 execution attempt,而不仅是 worker/agent。
  4. CAS 失败后重新读取事实,再区分幂等、可接管和真冲突。
  5. Dependency readiness 应包含交付同步完成,而不只看业务状态。
  6. 活动任务必须有 machine-checkable live path;文本承诺不是 liveness。

源码入口

  • packages/db/src/schema/issues.ts
  • packages/db/src/schema/issue_relations.ts
  • packages/shared/src/constants.ts:Issue 状态、work mode、origin
  • server/src/services/issues.ts :: checkout / release / adminForceRelease
  • server/src/routes/issues.ts :: POST /issues/:id/checkout
  • server/src/services/issue-liveness.ts
  • doc/execution-semantics.md:正式执行语义

Issue 被安全认领后,下一步是如何真正运行:07 · Heartbeat 调度与运行闭环。