云端 goal/merge/review 三事件:goal 循环 + PR 事件通知

此前 goal-unmet/merge-failed/review-requested 只有允许列表,
云端没有生产者(概念只在本地 host.py)。本轮给三者接真生产者:
- goal:POST /tasks 接受 goal/verify_command/goal_max_rounds
  (迁移补列,SQLite 与 Postgres 两支);带 verify_command 的
  任务走 host.py goal 循环(Local/Docker 执行器 goal_task,
  turn→验测→喂回重试,缺省 3 轮);达标 completed,未达标
  failed 且只发 goal-unmet、不重复发 failed;goal 模式下 worker
  级重试/fallback 关闭,同任务 /retry 会撞 host 任务名已存在
  (已在文档写明,须新建任务);CloudExecutor 无 goal 概念,
  如实失败不降级
- review-requested:POST /tasks/{id}/pr 建 PR 成功时发
  (payload 含 pr_url/pr_number)
- merge-failed:PR 落盘(push/建 PR)失败 502 时发(payload
  含 step/error),任务保持 merged、可重调 /pr;跳过时不发
测试:host 29、cloud 469 全过。
This commit is contained in:
host committed 2026-10-26 18:11:12 +08:00
1 parent 26e73124fb
commit 5ee9d291dd
8 files changed
+965 -27

No files matched your search

+55 -14
View File
@@ -214,6 +214,12 @@ def _task_from_row(row):
# 老库行缺 executor 列时给 None(表示任意 worker 可认领),
# 不炸;列本身由 _ensure_result_columns 在写回时补。
"executor": _col("executor"),
# goal 字段同款 _col 容错:老库行与 load_tasks 的显式列清单
# 里没有时给 None(= 普通任务),不炸;run_one 认领后
# get_task 重读 SELECT *,goal 任务一定能拿到完整字段。
"goal": _col("goal"),
"verify_command": _col("verify_command"),
"goal_max_rounds": _col("goal_max_rounds"),
}
@@ -422,7 +428,8 @@ class Worker:
rows = conn.execute(
"SELECT id, project_id, prompt, status, idempotency_key,"
" depends_on, priority, created_by, created_at, updated_at,"
" last_heartbeat, executor FROM tasks"
" last_heartbeat, executor, goal, verify_command,"
" goal_max_rounds FROM tasks"
" WHERE status IN ('queued', 'completed', 'merged')"
" ORDER BY created_at, id").fetchall()
finally:
@@ -511,7 +518,9 @@ class Worker:
"actual_model": "TEXT", "started_at": "REAL",
"finished_at": "REAL", "child_pid": "INTEGER",
"executor": "TEXT", "pr_url": "TEXT",
"pr_number": "INTEGER"}.items():
"pr_number": "INTEGER", "goal": "TEXT",
"verify_command": "TEXT",
"goal_max_rounds": "INTEGER"}.items():
if name not in cols:
try:
conn.execute(
@@ -644,8 +653,9 @@ class Worker:
error=None):
"""任务终态推送:best-effort,绝不因通知失败影响任务状态。
只发 completed/failed(push.ALLOWED_PUSH_EVENTS 的子集;
interrupted 不在事件集合里,取消不通知)。无订阅时
只发终态事件:completed/failed/goal-unmet
(push.ALLOWED_PUSH_EVENTS 的子集;interrupted 不在
事件集合里,取消不通知)。无订阅时
dispatch 直接返回空 sent,不联网。同步发送、单次 10 秒
超时:慢订阅会拖住 worker 循环最多几秒,换取投递结果
可观测(失败记日志,不静默丢);后台队列是后续优化,
@@ -921,6 +931,9 @@ class Worker:
"host_status": None}
fake = SimpleNamespace(returncode=proc.returncode,
stdout=out or "", stderr=err or "")
if kind == "goal" and hasattr(
self.executor, "_goal_result_from_runner"):
return self.executor._goal_result_from_runner(task_id, fake)
if hasattr(self.executor, "_result_from_runner"):
return self.executor._result_from_runner(task_id, fake)
return {"executor": "local", "task_id": task_id,
@@ -1114,6 +1127,10 @@ class Worker:
task_id = task["id"]
result = None
error = None
# goal 模式:任务带 verify_command 时走 host.py goal 循环,
# 不走普通 dispatch。goal 循环内部已多轮重试,worker 级
# 的已存在续跑(run)对它没有意义(goal 没有 run 语义)。
goal_mode = bool(task.get("verify_command"))
# Docker 同步路径:dispatch 阻塞在 docker run 里时,取消
# 线程靠容器名杀容器(见 cancel())。容器名按任务派生,
# 与执行器本身无关,所以这里只记名不碰执行器状态。
@@ -1142,10 +1159,11 @@ class Worker:
try:
if self._use_popen():
result = self._execute_via_process(
task, deadline_seconds=self.task_timeout)
task, kind="goal" if goal_mode else "dispatch",
deadline_seconds=self.task_timeout)
combined = (result.get("stdout", "") or "") + \
(result.get("stderr", "") or "")
if result["status"] not in (
if not goal_mode and result["status"] not in (
"completed", "cancelled", "timeout") and \
_ALREADY_EXISTS_MARKER in combined:
result = self._execute_via_process(
@@ -1209,16 +1227,26 @@ class Worker:
"NO_PROXY": "localhost,127.0.0.1",
"no_proxy": "localhost,127.0.0.1",
}
result = self.executor.dispatch_task(
task, timeout=effective_docker_timeout,
extra_mounts=tenant_mounts,
network=tenant_network,
extra_env=proxy_env)
if goal_mode:
result = self.executor.goal_task(
task, timeout=effective_docker_timeout,
extra_mounts=tenant_mounts,
network=tenant_network,
extra_env=proxy_env)
else:
result = self.executor.dispatch_task(
task, timeout=effective_docker_timeout,
extra_mounts=tenant_mounts,
network=tenant_network,
extra_env=proxy_env)
else:
result = self.executor.dispatch_task(task)
if goal_mode:
result = self.executor.goal_task(task)
else:
result = self.executor.dispatch_task(task)
combined = (result.get("stdout", "") or "") + \
(result.get("stderr", "") or "")
if result["status"] != "completed" and \
if not goal_mode and result["status"] != "completed" and \
_ALREADY_EXISTS_MARKER in combined:
if is_docker:
result = self.executor.run_task(
@@ -1432,6 +1460,11 @@ class Worker:
if self._was_cancelled(task_id) or \
result.get("status") == "cancelled":
break
if task.get("verify_command"):
# goal 模式只跑一次:循环内部已多轮重试(turn→验测→
# 喂回),worker 级重试/fallback 不再叠加——同任务重跑
# 会撞 host 任务名已存在,第二次尝试必失败且污染错误文本。
break
if result.get("status") == "completed":
break
host_status_try = self._fallback_token(result, error)
@@ -1517,6 +1550,8 @@ class Worker:
% (self.model, first_error,
fallback_model, second_error))
else:
if result.get("goal_error"):
error = result["goal_error"]
if error is None:
stderr = (result or {}).get("stderr", "") or ""
error = stdout.strip() or \
@@ -1569,7 +1604,13 @@ class Worker:
else "worker_failed",
task_id=task_id, project_id=task["project_id"],
status=final)
if final in ("completed", "failed"):
if result.get("goal_met") is False:
# goal 未达只发 goal-unmet,不再重复发 failed:
# 同一结局两条通知会刷屏,digest 偏好下两者又都是
# 即时事件,更不能双发。
self._notify_task_finished(
task_id, task["project_id"], "goal-unmet", error=error)
elif final in ("completed", "failed"):
self._notify_task_finished(
task_id, task["project_id"], final, error=error)
else: