跳转至

07 · Heartbeat 调度与运行闭环

Durable WakeupRun QueueAdapter ExecuteLivenessRetry / Recovery

一句话结论: Heartbeat 不是一个 cron callback,而是一条持久化执行管线:请求先被规范化、抑制、合并和入队,再 claim 为 Run,组装受治理上下文,调用 Adapter,最后以终态、成本、会话、workspace 和 liveness 一起收敛。

1. 两张表回答两个不同问题

agent_wakeup_requests

回答“为什么系统想唤醒 Agent”:

  • company/agent;
  • source、trigger/reason、payload;
  • issue/run 上下文;
  • status;
  • coalescing/idempotency;
  • 最终关联 heartbeat run。

状态包括:

queued
deferred_issue_execution
claimed
coalesced
skipped
completed
failed
cancelled

heartbeat_runs

回答“实际发生的这次执行怎样了”:

queued
scheduled_retry
running
succeeded
interrupted
failed
cancelled
timed_out

它还保存 adapter、session、usage、cost、result、log/process metadata、retry 关系、liveness 和 context snapshot。

为什么不合成一张表?一次业务唤醒可能被合并、跳过或推迟,未必创建真正执行;一次执行也可能是 retry/recovery 产生的新尝试。请求身份和尝试身份不同。

2. 唤醒来源

标准 invocation source 包括:

  • timer:定时 heartbeat;
  • assignment:新任务/责任变化;
  • on_demand:人或 API 手动触发;
  • automation:routine、pipeline、completion、recovery 等系统动作。

更细的 reason/payload 还区分 mention、comment、approval resolution、task watchdog、dependency completion、retry 等。来源会影响:

  • 是否必须有 actionable issue;
  • 是否可被 coalesce;
  • 是否允许自动 checkout;
  • context 中携带哪些限制;
  • toast/activity 如何展示;
  • 幂等键如何计算。

3. enqueueWakeup() 先做大量“不要运行”的判断

入队路径大致如下:

flowchart TD
  IN["Wakeup intent"] --> CTX["规范化 company / agent / issue / responsible user"]
  CTX --> SUP{"全局调度抑制?"}
  SUP -->|yes| SKIP["持久化 skipped request"]
  SUP -->|no| ACTIVE{"company + agent active?"}
  ACTIVE -->|no| SKIP
  ACTIVE -->|yes| BUDGET{"预算 / invokability 通过?"}
  BUDGET -->|no| SKIP
  BUDGET -->|yes| ACTION{"timer 是否有 actionable work?"}
  ACTION -->|no| SKIP
  ACTION -->|yes| HOLD{"issue tree hold?"}
  HOLD -->|yes| SKIP
  HOLD -->|no| TX["transaction: lock + coalesce + create request/run"]
  TX --> QUEUE["start agent queue"]

即使跳过也持久化 request 的好处是:操作者能区分“scheduler 没触发”和“触发了但因预算/暂停/无工作被抑制”。

4. 规范化 Issue 身份

唤醒 payload 可能传 Issue UUID 或人类 identifier。入队时会解析为 canonical ID,并加载:

  • 当前 assignee;
  • execution lock;
  • project/workspace policy;
  • source trust;
  • responsible user;
  • tree hold 和 dependency 状态。

这避免把同一 Issue 的不同字符串形式当成两条独立队列键。

5. Coalescing 与排队

Agent 已有活动 run 时,新唤醒不总是再启动一个进程。事务内会根据来源和 issue context:

  • 与现有请求合并;
  • 标为 deferred_issue_execution;
  • 创建 queued run;
  • 在安全时绑定 issue execution lock;
  • 用 idempotency key 阻止重复自动化请求。
stateDiagram-v2
  [*] --> Requested
  Requested --> Skipped: suppressed / inactive / budget / no work
  Requested --> Coalesced: equivalent live request exists
  Requested --> Deferred: issue execution already occupied
  Requested --> Queued: accepted
  Deferred --> Queued: execution path becomes available
  Queued --> Claimed: agent queue selects it
  Claimed --> Completed: run terminalized

Coalescing 不是简单丢消息:相关 reason/context 要被保留或可从 activity/wakeup records 追溯。

6. Agent queue 的职责

每个 Agent 的运行要经过统一 queue,而不是各入口直接 executeRun。Claim 阶段检查:

  • run 仍为 queued;
  • 前置/依赖仍成立;
  • Agent 并发槽可用;
  • issue execution path 没被其他 run 占用;
  • 当前 run 能通过 CAS 从 queued → running。

这解决“两个不同事件同时唤醒同一个 Agent”的竞争。

7. executeRun() 的十四个阶段

heartbeat.ts 约 1.9 万行,最有效的阅读方式是把 executeRun 拆成阶段。

阶段 1:再次确认调度条件

入队与执行之间可能经过等待,因此运行前重新检查 suppression、Company/Agent 状态、预算与 run status。

阶段 2:Claim run

用条件更新把 queued run 变为 running,并注册 active execution。失败说明其他 worker/流程已处理,当前调用安全退出。

阶段 3:加载任务与会话

读取:

  • Agent 配置与 runtime state;
  • Issue、项目、目标祖先和 wake comment;
  • task session 及 adapter session state;
  • run context snapshot;
  • issue adapter/model/runtime override。

阶段 4:可选的自动 checkout

由 assignment 等可信 wake 触发,且当前 Issue/Agent 满足规则时,Heartbeat 可调用同一 Issue service 自动 checkout。它不会绕过第 06 章的原子竞争。

阶段 5:解析信任与责任主体

合并 input source trust、project/issue preset、low-trust review policy,解析 responsible user,并生成运行授权 envelope。

阶段 6:选择 Workspace / Environment

结合 project policy、issue preference、已有 execution workspace 和 trust preset:

  • shared / isolated / reuse;
  • local / SSH / sandbox / plugin provider;
  • 需要时获取 environment lease;
  • 低信任来源可被强制放入隔离 workspace。

阶段 7:准备运行时能力

包括:

  • scratch directory;
  • workspace prepare operation;
  • runtime services;
  • materialized skills;
  • secrets 与 tool/MCP config;
  • 短期 Paperclip run JWT;
  • adapter config 和 instructions bundle。

阶段 8:构造任务 payload

上下文不是简单拼接 title:它可能含目标祖先、项目、Issue 线程、wake reason、工作区信息、权限/治理提示、当前 run 身份和 session continuation。

不可信内容在进入 Prompt/环境前执行 redaction/quarantine/边界标注。

阶段 9:调用 Adapter

Server 从 registry 取得 adapter module,调用 execute(context)。Context 提供 callbacks:

  • log/event/progress;
  • process metadata;
  • session updates;
  • usage/cost;
  • runtime status。

阶段 10:流式记录

Adapter 输出持续写入 run log store、structured events 和 live event bus;process PID/group/provider reference 也在早期持久化,以便 stop/restart/recovery。

阶段 11:收集结果

Adapter result 被规范化为:

  • success/failure/interrupted;
  • model/usage/cost;
  • session state;
  • final output / structured metadata;
  • provider/runtime diagnostics。

阶段 12:Liveness 分类

Run 不只分成功失败,还被分类:

Liveness 含义
completed 任务达到合法完成/终态
advanced 有真实进展,仍可能需后续
plan_only 只产出计划,需确认或继续
empty_response 没有可用推进证据
blocked 明确等待外部条件
failed 执行失败
needs_followup 本轮结束但必须续跑/转交

阶段 13:Continuation / retry / recovery

根据 run outcome、liveness、attempt count、provider failure 分类和 Issue 状态:

  • 立即/延迟 retry;
  • scheduled retry;
  • completion wake;
  • manager/reviewer handoff;
  • recovery notice/action/issue;
  • 释放或保留 Issue lock;
  • 更新 Agent 状态。

阶段 14:终态与清理

最终保存 run result、usage/cost、session、workspace operation,并清理 lease、runtime service、scratch、active registry,然后启动下一个 queued run。

8. 关键顺序:先终态化 Run,再释放 Lease

这是一个细小但重要的不变量:

sequenceDiagram
  participant R as Run
  participant DB as Database
  participant L as Environment Lease
  participant Q as Next Queue Item

  R->>DB: persist terminal status/result/liveness
  DB-->>R: committed
  R->>L: finalize workspace + release lease
  L-->>R: released/cleanup recorded
  R->>Q: start next queued run

如果先释放 lease,再写 run 终态,另一个执行可能复用环境,而 recovery 仍看到旧 run running,造成错误接管或清理。

9. 状态写入使用 CAS 防晚到事件

子进程 exit、timeout、cancel 和 provider callback 可能竞相写终态。setRunStatusFromLive 一类操作只允许从 live 状态转换:

UPDATE heartbeat_runs
SET status = :terminal
WHERE id = :id AND status IN ('queued', 'scheduled_retry', 'running')

一旦某个路径先写入 terminal,晚到的“failed”不能覆盖已确认的“cancelled”或“succeeded”。

10. 日志、事件、进度不是同一层

Heartbeat 发布多类 live event:

类型 用途
heartbeat.run.queued 新 run 进入队列
heartbeat.run.status 生命周期状态变化
heartbeat.run.progress 当前工具、摘要、状态消息
heartbeat.run.event adapter 结构化事件
heartbeat.run.log 高频日志片段
agent.status Agent 控制状态
activity.logged 业务实体变更

UI 对 log 只做局部 transcript 消费,对 status/activity 才进行更广缓存失效,避免事件风暴。

11. Session 如何跨 Heartbeat 延续

agent_task_sessions 按 company、agent、adapter、task key 唯一。执行前:

  1. 找到当前 task session;
  2. 用 adapter session codec 解码 provider-specific state;
  3. 若 workspace/cwd 与 session 不兼容,按策略重置或迁移;
  4. 传入 adapter resume 参数;
  5. 执行后原子保存新的 session state。

这让 Agent 跨 heartbeat 保留对话,而每轮 run 仍有独立成本、日志和故障边界。

12. Retry 不是一刀切

系统会区分:

  • provider 暂时失败;
  • adapter/config 永久错误;
  • process 被中断;
  • silent timeout;
  • responsible user denial;
  • workspace/environment provisioning 失败;
  • Agent 有输出但 liveness 不足。

Scheduled retry 保存 retryOfRunId、时间、attempt 和原因。到期后由 promotion tick 转回可 claim 状态,而不是在内存里 setTimeout 几分钟。

13. Outer error 与 inner error

executeRun 有两圈错误处理:

  • inner:Adapter 已启动后的执行错误,通常已有 process/log/workspace,需正常 finalization;
  • outer:在 Adapter 前的 setup/claim/context/workspace 错误,仍必须把 run 终态化并释放已经获取的部分资源。

Finally 按“获得过什么就释放什么”工作,避免 setup 半途失败留下 lease 或 scratch。

14. 服务器重启后的恢复

持久化 run/requests 允许启动时:

  • adopt 仍活着且身份可验证的本地进程;
  • terminalize 停机期间自然完成的进程;
  • 标记 lost 并生成恢复路径;
  • promote 到期 retry;
  • resume queued run;
  • 释放无 live run 对应的 issue lock;
  • 为 stranded issue 建立 continuation。

仅内存队列无法做到这一点。

15. Heartbeat 的端到端时序

sequenceDiagram
  participant T as Trigger
  participant H as Heartbeat Service
  participant DB as PostgreSQL
  participant W as Workspace/Environment
  participant A as Adapter
  participant E as Events/UI
  participant R as Recovery

  T->>H: enqueueWakeup(intent)
  H->>DB: persist request + queued run / coalesce / skip
  H->>H: startAgentQueue(agent)
  H->>DB: claim queued → running
  H->>DB: auto-checkout Issue (optional)
  H->>W: realize workspace + acquire lease
  H->>A: execute(context, session, secrets, tools)
  loop streaming
    A->>H: log / progress / event / process meta
    H->>DB: persist evidence
    H->>E: publish company live event
  end
  A-->>H: result + usage + session
  H->>DB: cost + session + liveness + run terminal
  H->>W: finalize/sync + release lease
  alt needs continuation
    H->>DB: retry / wakeup / recovery action
    H->>R: schedule next path
  else complete
    H->>DB: release locks / final Agent state
  end

16. 设计代价

优点

  • 每个决策有 durable evidence;
  • 多触发源共享同一执行不变量;
  • restart/retry/cancel 都能对账;
  • adapter 差异被 contract 吸收;
  • liveness 与 exit code 解耦。

代价

  • 单个 service 极大,阶段之间共享大量局部状态;
  • 高频 DB 更新与日志持久化带来写放大;
  • coalescing、Issue lock、Agent queue、workspace lease 是多重并发层,测试矩阵庞大;
  • 当前进程内 live event 不能提供跨节点回放。

17. 可复用经验

  1. 把 wake intent 和 execution attempt 分表。
  2. 跳过的调度也要留记录,才能解释“为什么没运行”。
  3. Queue claim 与 run status 用 CAS,防多 worker 重复执行。
  4. Session 用于连续性,Run 用于账本和故障边界。
  5. succeeded 只说明执行成功;另建 liveness 表达工作是否仍需推进。
  6. 终态写入应早于资源复用,finally 要按已获取资源做幂等清理。
  7. 延迟重试应持久化时间和来源,不依赖进程内 timer。

源码入口

  • server/src/services/heartbeat.ts :: enqueueWakeup / executeRun
  • packages/db/src/schema/agent_wakeup_requests.ts
  • packages/db/src/schema/heartbeat_runs.ts
  • packages/db/src/schema/agent_task_sessions.ts
  • packages/shared/src/constants.ts:wakeup/run/liveness/live event 状态
  • server/src/services/run-liveness.ts
  • server/src/services/recovery/service.ts
  • server/src/services/hot-restart.ts

下一章打开执行黑盒:08 · Adapter、会话与上下文组装。