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:
1 parent
ac553509ea
commit
5883169c82
8 files changed
+958
-21
No files matched your search
@@ -449,6 +449,26 @@ class LocalExecutor(Executor):
|
||||
return self._goal_result_from_runner(
|
||||
task["id"], self._run_local(cmd, extra_env))
|
||||
|
||||
def goal_resume_breakpoint_task(self, task) -> dict:
|
||||
"""G10:调 host.py goal-resume(无 --goal-text)断点续跑。
|
||||
|
||||
命令:host.py goal-resume --task <host 名>。不传
|
||||
--goal-text、不传 --max-rounds:host 侧按原任务的
|
||||
goal/verify_command/goal_max_rounds,从
|
||||
goal_rounds_done 的下一轮接着跑同一循环,不开新循环、
|
||||
不新建 worktree、不重置轮次。结论解析与 goal_task 同款。
|
||||
(worker 只以 (task) 一个参数调本地断点方法,与 Docker
|
||||
断点方法的签名不同,不要顺手统一。)
|
||||
"""
|
||||
if not isinstance(task, dict) or task.get("id") is None:
|
||||
raise ValueError("任务缺少 id")
|
||||
cmd = self._base_command(model=task.get("model"),
|
||||
effort=task.get("effort"),
|
||||
approval=task.get("approval"))
|
||||
cmd += ["goal-resume", "--task", self._host_name(task)]
|
||||
return self._goal_result_from_runner(
|
||||
task["id"], self.runner(cmd))
|
||||
|
||||
def goal_resume_task(self, task, goal_text=None, extra_env=None,
|
||||
model_provider=None, max_rounds=None) -> dict:
|
||||
"""调 host.py goal-resume,分两种调用(P1,与 worker、
|
||||
@@ -584,6 +604,12 @@ class LocalExecutor(Executor):
|
||||
cmd += ["--repo", repo]
|
||||
if seed_dir:
|
||||
cmd += ["--seed-dir", seed_dir]
|
||||
elif kind == "goal-resume-breakpoint":
|
||||
# G10 断点:无 --goal-text,不开新循环;host 侧按
|
||||
# goal_rounds_done 的下一轮接着跑,轮次上限沿用原
|
||||
# 任务(不传 --max-rounds 时 host.py 取原任务值,
|
||||
# 新语义原值 0 即不限轮次)。
|
||||
cmd += ["goal-resume", "--task", self._host_name(task)]
|
||||
elif kind == "goal-resume":
|
||||
# P1 分流(与 goal_resume_task 同款契约):
|
||||
# _resume_kind="breakpoint" 时不传 --goal-text(host
|
||||
@@ -1305,6 +1331,77 @@ class DockerExecutor(Executor):
|
||||
"usage": _parse_goal_usage(stdout_full),
|
||||
}
|
||||
|
||||
def goal_resume_breakpoint_task(self, task, timeout=None,
|
||||
extra_mounts=None, network=None,
|
||||
extra_env=None) -> dict:
|
||||
"""G10:容器内调 host.py goal-resume(无 --goal-text)
|
||||
断点续跑,与 LocalExecutor.goal_resume_breakpoint_task
|
||||
同语义(从 goal_rounds_done 下一轮接着跑,不开新循环)。
|
||||
解析与容器内 goal_resume_task 同款;真 Docker 实弹
|
||||
未做,命令形状由单测覆盖。
|
||||
"""
|
||||
if not isinstance(task, dict) or task.get("id") is None:
|
||||
raise ValueError("任务缺少 id")
|
||||
inner = self._inner_host_command(
|
||||
"goal-resume", "--task", self._host_name(task),
|
||||
model=task.get("model"), effort=task.get("effort"),
|
||||
approval=task.get("approval"))
|
||||
cmd = self.build_run_command(task, inner,
|
||||
extra_mounts=extra_mounts,
|
||||
network=network,
|
||||
extra_env=extra_env)
|
||||
try:
|
||||
result = self._call_runner(cmd, timeout=timeout)
|
||||
except Exception as exc:
|
||||
if exc.__class__.__name__ == "TimeoutExpired":
|
||||
self.kill_container(task)
|
||||
return {
|
||||
"executor": "docker", "task_id": task["id"],
|
||||
"status": "failed", "stdout": "docker run timeout",
|
||||
"returncode": None, "host_status": None, "stderr": "",
|
||||
"container": self.container_name(task),
|
||||
"goal_met": None, "goal_error": "docker run timeout",
|
||||
}
|
||||
return {
|
||||
"executor": "docker", "task_id": task["id"],
|
||||
"status": "failed",
|
||||
"stdout": _truncate_stdout(str(exc)),
|
||||
"returncode": None, "host_status": None, "stderr": "",
|
||||
"container": self.container_name(task),
|
||||
"goal_met": None, "goal_error": str(exc)[:500],
|
||||
}
|
||||
returncode = getattr(result, "returncode", 1)
|
||||
stdout_full = getattr(result, "stdout", "") or ""
|
||||
stderr = getattr(result, "stderr", "") or ""
|
||||
met, goal_error = _parse_goal_output(stdout_full)
|
||||
if returncode != 0:
|
||||
status = "failed"
|
||||
if met is None:
|
||||
goal_error = ((stderr.strip()[-500:] if stderr.strip()
|
||||
else stdout_full.strip()[-500:])
|
||||
or "host goal-resume failed")
|
||||
elif met is True:
|
||||
status = "completed"
|
||||
elif met is False:
|
||||
status = "failed"
|
||||
else:
|
||||
status = "failed"
|
||||
goal_error = goal_error or "host goal-resume 输出无结论"
|
||||
return {
|
||||
"executor": "docker",
|
||||
"task_id": task["id"],
|
||||
"status": status,
|
||||
"stdout": _goal_stdout_tail(stdout_full),
|
||||
"stderr": stderr,
|
||||
"returncode": returncode,
|
||||
"host_status": ("goal-met" if met is True else
|
||||
"goal-unmet" if met is False else None),
|
||||
"container": self.container_name(task),
|
||||
"goal_met": met,
|
||||
"goal_error": goal_error,
|
||||
"usage": _parse_goal_usage(stdout_full),
|
||||
}
|
||||
|
||||
def network_gateway(self, network_name):
|
||||
"""某 Docker 网络的网关 IP(宿主在该网桥上的地址)。
|
||||
|
||||
|
||||
Reference in new issue
Block a user