feat: goal 断点续跑端点(G10)+ 摘要持久化与推送 outbox(G11/G12)

G10:POST /tasks/{id}/goal-resume,仅 interrupted 的 goal 任务,
走 host.py goal-resume(无 --goal-text)从 goal_rounds_done 下一轮
断点续跑,保留原线程与 worktree,状态回 queued 并置
goal_resume_pending(worker 认领后清掉),写 task_goal_resumed
审计;goal-met/unmet、failed、非 goal 一律 409;/continue 开新
循环语义不动。Local/Docker 两路新增 goal_resume_breakpoint_task,
不支持断点的执行器明确失败,绝不回落成 goal 或开新循环。
G11:digest 非失败事件持久化进 push_digest_events,worker 循环
每小时按(项目、订阅)分组汇总发一条,失败类仍即时发。
G12:webhook/slack 投递失败写 push_outbox,worker 循环后台重试
(最多 3 次、默认 60 秒退避),3 次仍失败删行并记日志,不丢
任务状态;缺凭证与无 sender 不进 outbox;文档写明 at-least-once
下 webhook/slack 可能重复,接收方须去重。
测试:cloud 769(4 跳过)、host 68、deploy 19 全过。
This commit is contained in:
host committed 2026-10-28 11:09:29 +08:00
1 parent ac553509ea
commit 5883169c82
8 files changed
+958 -21

No files matched your search

+38 -12
View File
@@ -226,7 +226,7 @@ webhook 实测)。
### 推送凭证清单
- webhook / slack:已有内置真实发送端(标准库 urllib POST,
单次 10 秒超时,不重试,失败记入分发结果的 errors):
单次 10 秒超时,失败记入分发结果的 errors):
webhook 投 JSON(event_type、project_id、task_id、payload),
slack 投 Slack incoming webhook 格式的一行文本(任务 id +
事件 + 项目,失败附 error 摘要)。任务终态(completed /
@@ -235,6 +235,23 @@ webhook 实测)。
落盘失败时发 merge-failed;手动分发走
`POST /admin/push/dispatch`,同样经内置发送端。
显式注入的 sender 优先于内置端。
- 摘要(digest,G11):非失败事件在 digest 偏好下不再只在
返回值里列入 skipped_for_digest 就完事——事件持久化进
`push_digest_events` 表,worker 循环每小时按(项目、订阅)
分组汇总发一条(payload 为 `{"digest": True, "events": [...],
"count": n}`),成功后整组标记已发送,失败整组保留待下
一次重发。失败类事件(failed/goal-unmet/merge-failed)不
进摘要,仍即时发。摘要节奏是“worker 循环每小时一批”,
不是“每条事件满 1 小时才发”——一批刚发完、紧接着到的
事件要等下一批(最长约 1 小时)。
- 推送失败重试(outbox,G12):webhook/slack 发送端抛异常
时,事件写进 `push_outbox` 表,worker 循环后台重试(最多
3 次、默认 60 秒退避);3 次仍失败则删行并记
worker_push_outbox_dropped 日志,不丢任务状态、不炸循环。
缺凭证(webpush/apns/fcm)与无 sender 不是瞬时故障,不
进 outbox,仍明确报错。at-least-once:webhook/slack 的重
试可能重复送达同一事件,接收方必须按 event/task 去重,
本模块不保证 exactly-once。
- webpush:需要 VAPID 密钥对(私钥绝不进仓库),且需要
浏览器端订阅(endpoint + p256dh + auth,现有订阅模型只有
一个 target 文本字段,装不下)与标准库没有的 P-256 ECDH +
@@ -956,17 +973,26 @@ interrupted 且终态任务 cancel 返回 400、心跳后 sweep 不扫刚心跳
NULL,SUM 自动跳过),按任务创建者 `created_by` 归属——一个
用户在多个项目里花的钱都算进他自己的月度账单;项目维度的
拆分由项目成本接口另行提供。
- 准入:建任务、会话分支、重试、续跑在权限校验通过后、写库
之前查配额,本月用量 >= 配额时回 402 `quota_exceeded`(附
month、quota_usd_micros、used_usd_micros),不建任务、不改
状态。老用户与未设配额的用户默认不限额,行为与过去一致。但
OAuth 新用户不再是“建号即不限额”:建号(created=True)时
写一行默认配额(`CLOUD_DEFAULT_QUOTA_USD_MICROS`,默认
10_000_000,即 $10/月;`INSERT OR IGNORE` 幂等,不覆盖管理
员后来改过的额度),并记 `quota_default` 审计。重试与续跑
按任务创建者查配额(费用本来就记在创建者名下;维护者替
他人重试/续跑时,操作人超限不应拦、创建者超限才应拦——
旧版重试按操作人查是错位,本轮已改)。后续消息
- 准入:建任务、会话分支、重试、续跑、goal 断点续跑在权限
校验通过后、写库之前查配额,本月用量 >= 配额时回 402
`quota_exceeded`(附 month、quota_usd_micros、used_usd_micros),
不建任务、不改状态。老用户与未设配额的用户默认不限额,
行为与过去一致。但 OAuth 新用户不再是“建号即不限额”:
建号(created=True)时写一行默认配额
(`CLOUD_DEFAULT_QUOTA_USD_MICROS`,默认 10_000_000,
即 $10/月;`INSERT OR IGNORE` 幂等,不覆盖管理员后来改过
的额度),并记 `quota_default` 审计。goal 断点续跑端点
(G10):`POST /tasks/{id}/goal-resume`(权限 approve_merge,
同重试),仅 interrupted 的 goal 任务可续(goal-met/goal-unmet
已终局、failed 按旧规则新建、非 goal 任务一律 409),走
host.py goal-resume(无 --goal-text)的断点语义——从
goal_rounds_done 的下一轮接着跑同一循环,保留原线程与
worktree,状态回 queued 并置 goal_resume_pending(worker
认领后清掉),写 `task_goal_resumed` 审计;`/continue`
开新循环的语义不动,不要把两者混用。重试与续跑按任务
创建者查配额(费用本来就记在创建者名下;维护者替他人
重试/续跑时,操作人超限不应拦、创建者超限才应拦——旧版
重试按操作人查是错位,本轮已改)。后续消息
(followup)创建不查配额:排队本身不花钱,执行时轮次边界
的配额门照常生效,不要把 followup 也改成准入校验。系统
任务(GitHub webhook 的 /fix-review,created_by 是 'system')