跳到主要内容

第 17 章:Autopilot、调度器与 Webhook

17.1 Autopilot 是持久规则,不是 Cron 包装

一条 Autopilot 同时定义:

  • 做什么;
  • 由哪个 Agent/Squad 做;
  • 结果创建 Issue 还是直接 Run;
  • 什么时候或被什么事件触发;
  • 谁发布并对规则负责;
  • 每次运行产生了什么下游对象;
  • 暂停、归档、失败和重试怎样表现。

因此它横跨配置、权限、调度、任务队列和审计。

17.2 核心数据模型

查询入口在 server/pkg/db/queries/autopilot.sql,业务服务在 server/internal/service/autopilot.go

主要对象:

  • Autopilot:规则主体、目标、状态、执行模式;
  • Autopilot Trigger:Schedule/Webhook 等触发配置;
  • Autopilot Rule Version:不可变发布快照与 Publisher;
  • Autopilot Run:一次触发的持久结果;
  • Collaborator:规则写权限;
  • Webhook Delivery:外部投递、去重、租约和尝试。

17.3 两种 Execution Mode

模式下游对象适用场景
create_issue先创建并分配 Issue,再由正常任务流执行需要人类可见工作项、讨论、Review
run_only直接创建 Agent Task,结果写回 Autopilot Run巡检、同步、汇总、无需 Issue 的动作

两者不能只用“是否填 Issue ID”区分;Admission、Prompt、终态同步与失败恢复都有不同路径。

17.4 create_issue 的审计优先

create_issue 的主要契约是留下一个持久、可见的工作项。即便 Runtime 暂时 Offline,系统也可以允许 Issue 创建:

  • Issue 记录为什么产生;
  • Assignee 与 Project 已确定;
  • Runtime 恢复后普通任务机制可继续;
  • 人类不会看不到一次计划触发。

run_only 没有这个中间载体,Agent 不可运行时通常应记录 Skipped/Failed,而不是生成一个永远无人消费的 Task。

17.5 Run 状态

Autopilot Run 大致经历:

实际字段以 Migration/SQL Check 为准。重点是 Run 持久化连接 Trigger、Issue/Task、Source、Payload、Planned At 和 Failure Reason。

17.6 四类 Source

服务接受:

  • manual;
  • schedule;
  • webhook;
  • api。

Source 不只是标签,它影响:

  • 是否有直接 Human Actor;
  • Attribution 取 Manual Clicker 还是 Rule/Trigger Owner;
  • 是否有 Canonical Planned Time;
  • 是否关联 Webhook Delivery;
  • HTTP 是否需要同步返回可读 Outcome。

17.7 Admission Gate

Dispatch 前检查:

  • Autopilot 是否 Active;
  • Trigger 是否 Enabled;
  • Assignee Agent/Squad Leader 是否可解析;
  • Agent 是否 Archived;
  • Runtime Binding/Readiness;
  • Manual Actor 或 Rule Owner 是否有 Invoke 权;
  • Private Agent 是否只由允许的 Owner 使用;
  • Project/Workspace 关系是否一致。

Skipped 也是一个 Run,带明确 Reason;不能静默 No-op,否则 Scheduler 会把“没有执行”误当成功。

17.8 Manual 与自动化归因

Manual Run 有当前 Member:

  • originator_user_id 是 Clicker;
  • Accountable User 也优先是这位直接发起者;
  • 权限按 Clicker 实时检查。

Schedule/Webhook/API 没有现场 Human:

  • 优先用 Trigger Publisher;
  • 无法解析时降级到 Active Rule Version Publisher;
  • 再按明确策略使用 Owner Fallback 或拒绝;
  • 不能伪造一个当前用户。

17.9 Rule Version 为什么不可变

Autopilot 的 Title、Description、Target、Mode 以后会变化。若 Run 只指向当前 Row,半年后无法回答“当时是谁发布了怎样的规则”。

autopilot_rule_version 保存:

  • Autopilot/Workspace;
  • Published By Type/ID;
  • 有效配置摘要;
  • 发布时间。

创建、Enable/Resume、Target/Instructions/Mode 变更、Trigger 变更、Archive 等实质性发布会追加新版本。

17.10 Trigger Publisher 的更细归因

同一 Autopilot 可以有多个 Trigger。编辑某个 Cron/Filter/Webhook Security 时,只应转移该 Trigger 的责任。

所以 Trigger 自己也有 published_by_*

  • 单 Trigger 实质编辑 → 只 Re-stamp 该 Trigger;
  • Autopilot 级实质编辑 → Re-stamp 全部 Trigger;
  • 单纯展示性改动可不转移问责。

Run 同时保留 Originator、Accountable User 和 Rule Version ID。

17.11 写权限与问责权限不同

Autopilot Creator、Workspace Owner/Admin、显式 Collaborator 可以拥有写权限;但只有 Creator/Owner/Admin 能管理 Collaborator 列表。

Collaborator 修改规则后,会成为后续对应 Trigger/Rule 的 Publisher。权限不是匿名共享,编辑动作会转移未来运行的责任。

17.12 通用 DB-backed Scheduler

调度器位于 server/internal/scheduler。它不是 Autopilot 专用,核心表 sys_cron_executions 同时充当:

  • Distributed Lock;
  • Execution Audit Log;
  • Retry State;
  • Lease/Heartbeat;
  • Catch-up Watermark。

所有 Server Replica 都可以 Tick,同一 (job, scope, plan_time) 只有一个 Claim Winner。

17.13 JobSpec

scheduler/spec.go 定义:

  • 稳定 Job Name;
  • Cadence 或自定义 PlansForScope;
  • Scope Provider;
  • Schedule Delay;
  • Catch-up Mode/Window;
  • 每 Tick 最大 Plans;
  • Run/Stale/Heartbeat Timeout;
  • 是否允许 Stale Reentry;
  • Max Attempts 与 Retry Backoff;
  • Handler。

校验要求 RunTimeout < StaleTimeoutHeartbeatInterval < StaleTimeout,否则健康 Handler 也会被其他节点误偷。

17.14 Scheduler Claim 协议

旧 Owner 在 Lease 被 Steal 后的 Terminal Update 会因 Token 不匹配被拒绝,不能覆盖新 Owner。

17.15 Catch-up 模式

通用 Scheduler 支持:

  • latest_only:只处理最近到期 Plan;
  • every_plan:按从旧到新逐个补偿,受 Window/Max 限制;
  • 自定义 PlansForScope:适合不规则 Cron Occurrence。

Autopilot Schedule 使用第三种,并实现 Latest-only 意图。

17.16 Autopilot Schedule Job 参数

jobs_autopilot.go 当前配置:

  • Job Name:autopilot_schedule_dispatch
  • 每个 Enabled Schedule Trigger 是一个 Scope;
  • Run Timeout 2 分钟;
  • Stale Timeout 5 分钟;
  • Heartbeat 30 秒;
  • 允许 Stale Reentry;
  • 最多 3 次尝试;
  • Backoff 1、5、15 分钟;
  • Catch-up Window 24 小时;
  • 每 Tick 最多 5 个 Plan(保护上限)。

参数是当前快照的实现细节,不是外部 SLA。

17.17 Cron 与 Timezone

service/cron.go 在 Trigger Timezone 中解析表达式,再返回 Canonical UTC Occurrence。

规则:

  • Timezone 必须是有效 IANA 名称;
  • 缺失默认 UTC;
  • Preview 与执行使用同一解析逻辑;
  • Plan Time 以 UTC 持久化;
  • 人类可见描述按 Trigger Timezone 格式化;
  • Schedule Trigger 才接受 Cron/Timezone,Webhook 明确拒绝这些字段。

17.18 Latest-only 与 Lateness

Planner 枚举 (lastPlan, dbNow] 中的 Cron Occurrences,只保留最新一个;如果它比计划时间晚超过 5 分钟,则不在任意恢复时刻补跑。

意义:

  • Server 停机数小时后不会一次创建几十个重复日报;
  • 新启用的旧 Trigger 不会立刻补发历史任务;
  • 正常几十秒 Scheduler Jitter 仍可接受。

这是一种业务选择:Autopilot Schedule 偏“下一次保持新鲜”,不是财务流水那种每个 Bucket 必须重放。

17.19 双层幂等

一次 Schedule Occurrence 有两层键:

  1. sys_cron_executions(job, scope, plan_time) 防多个 Scheduler 同时跑;
  2. autopilot_run(trigger_id, planned_at) Partial Unique 防 Dispatch Crash Window 重复生成 Run。

如果先创建 Run、后创建下游 Issue/Task 时崩溃:

  • 已有完整下游链接 → 复用原 Run;
  • 只有半成品 Run → 标 Failed,清空 Planned At 释放唯一槽;
  • 新尝试创建干净 Run;
  • 旧失败记录保留供审计。

17.20 Stale Lease

Manager 每 Tick 先清理遗留 Running Lease:

  • Heartbeat 超时 → 标 Failed/stale_timeout;
  • 允许重入且尝试未耗尽 → 同 Plan 可再次 Claim;
  • Steal 会轮换 Lease Token;
  • 旧 Runner 的 Heartbeat/Finish 失效。

Autopilot Planner 必须在 Latest Row 是 Retry-eligible Failed 时返回 同一个 Plan Time;若直接向新 Cron Occurrence 前进,这次失败重试会永久搁浅。

17.21 Webhook Token 与 Secret

Webhook Trigger 的公开路径含 Bearer Token:

  • 前缀 awt_
  • 32 随机字节做 URL-safe Base64;
  • Token 可旋转,旧 Token 立即失效;
  • 只有有权管理 Autopilot 的成员才能看到完整 Token/URL。

可选 Signing Secret 额外验证 HMAC-SHA256。无 Secret 是显式 Bearer-only 模式,不代表跳过所有认证。

17.22 Webhook Ingress

autopilot_webhook.go 位于普通登录路由之外,路径 Token 就是 Credential。

固定顺序:

  1. IP 绝对上限与 Bad-credential Debt;
  2. Token Lookup;
  3. 256 KiB Body Cap;
  4. Autopilot/Workspace Cross-check;
  5. JSON Envelope Normalize;
  6. Provider Dedupe Key 与 Signature;
  7. 先插 Webhook Delivery;
  8. Invalid Signature 标 Rejected;
  9. Disabled/Paused/Archived 标 Ignored;
  10. 幂等 Admit Run;
  11. 唤醒持久 Worker;
  12. 同步返回 Accepted/Skipped/Duplicate。

未知 Token 一律 404,不泄露“差一点正确”的信息。

17.23 为什么先持久化 Delivery

Webhook Provider 会重试,Server 也会重启。若在 HTTP Handler 里直接创建 Issue:

  • 超时后 Provider 不知道是否成功;
  • Server Crash 产生不确定副作用;
  • 重放容易重复;
  • 慢 Agent Dispatch 占住 Ingress。

Delivery Row 保存 Raw Body、精选 Header、Signature Status、Dedupe Key、Response 与 Run Link,使 HTTP 可以快速确认,Worker 再异步执行。

Signature Header 只保存“是否存在”,不保存 HMAC 值。

17.24 Webhook Dedup

Provider-specific Header 或 Idempotency Key 归一成 (trigger_id, dedupe_key)

碰撞时:

  • 不新建 Delivery;
  • 增加 Attempt Count;
  • 返回原 Delivery ID;
  • 若已有 Run,返回 Run ID;
  • 原 Delivery 仍 Queued 时再次 Wake Worker。

重复请求返回 200,避免 Provider 因错误码继续无意义重试。

17.25 Webhook Worker

webhook_delivery_worker.go 使用 PostgreSQL 队列:

  • 单进程并发 4;
  • Recovery Poll 每秒;
  • 本地 Notify 只是低延迟提示;
  • SKIP LOCKED 跨 Replica 分配;
  • Lease Token 保护 Complete/Retry;
  • 最多 5 次 Dispatch;
  • 指数 Backoff;
  • Trigger Rate Limit 可把 Delivery 释放到未来 Available At。

进程崩溃后其他 Replica 可回收过期 Lease。

17.26 Filter 与 Admission 时点

Ingress 同步 Admit 后,决定已持久化。Worker 不应因为管理员在 200 Response 之后立刻 Pause 就把已接受 Run 悄悄丢掉。

只有恢复“尚未 Admit 的 Crash Window”时,Worker 才重新检查:

  • Trigger Enabled;
  • Autopilot Active;
  • Event Filter;
  • Ownership 一致。

这让外部调用方收到的 Accepted 具有稳定含义。

17.27 Run 怎样收尾

  • create_issue:Issue/Linked Task 状态通过 SyncRunFromIssueSyncRunFromLinkedIssueTask 更新;
  • run_only:Task Terminal 通过 SyncRunFromTask 更新;
  • Skipped/Admission Error:Dispatch 当场终结;
  • Webhook Delivery 另行记录 Dispatched/Failed/Ignored;
  • Event Bus 发布 Run Started/Done,前端失效 Query。

Run 状态不是靠 Scheduler 猜 Agent 是否完成。

17.28 排障矩阵

症状先查
Cron 没触发Trigger Active、Cron/Timezone、Scope、sys_cron_executions
同一时间触发两次两层唯一键、planned_at 是否为空/不规范
Run 卡 pendingDispatch Crash Window、下游 Issue/Task Link
Schedule 失败后不重试Planner 是否返回 Retry-eligible 的旧 Plan
Webhook 200 但没任务Delivery、Admitted Run、Worker Lease、Admission Reason
Webhook 重复创建Dedupe Key 提取与 Delivery Unique Constraint
别人编辑后仍归原 OwnerRule Version/Trigger Publisher 未 Re-stamp
Squad Autopilot 不运行Leader 解析、Leader Runtime、Private Invoke Gate

17.29 本章结论

Autopilot 的可靠性来自三组持久事实:

  • Rule Version/Trigger Publisher 说明“谁发布了什么”;
  • Scheduler Lease/Plan Time 说明“什么时候由谁执行”;
  • Run/Delivery/Task Link 说明“实际产生了什么”。

Cron、Webhook 和 Agent Task 都可能重试;只有把幂等、归因和恢复写进数据库,自动化才不是一组脆弱的后台 Goroutine。