云端 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:
1 parent
26e73124fb
commit
5ee9d291dd
8 files changed
+965
-27
No files matched your search
+55
-14
@@ -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:
|
||||
|
||||
Reference in new issue
Block a user