feat: 旧任务继续会话——终端任务带新消息 + 全部选项同一会话续跑

server:POST /tasks/{id}/continue(终端非 merged 任务重排队,
resume_message 列存本轮消息,不传选项沿用原值,plan/goal 冲突
与新建一致,goal_mode=1 时转新 goal 语义;CAS 防并发改状态,
审计 task_continued);tasks.resume_message 三处迁移 + PG 建表。
worker:认领后本轮 prompt 换成续跑消息(只换内存副本),
goal 续跑走 goal-resume 而不是 goal,跑完清 resume_message
(进程被杀时不清,下次认领重跑同一条消息)。
host:goal-resume --goal-text(旧任务接续 goal 模式:跳过状态
与记录守卫,旧线程上从第 1 轮开新目标循环,新语义不限轮次;
无线程时拒绝)。
sandbox:Local/Docker goal_resume_task + popen goal-resume kind;
附带 popen goal 命令改成新语义(无验收不传 --verify,轮次 0
原样传,此前 goal 任务到本地 worker 会直接报错)。
console:详情页终端非 merged 任务出继续会话表单(消息 +
模型/强度/权限/计划/goal,模型下拉复用中转列表)。
测试:新增 test_task_continue.py 11 个 + test_host_continue.py 2 个。
This commit is contained in:
host committed 2026-10-27 04:40:11 +08:00
1 parent cf293914d9
commit 3a4997d346
9 files changed
+823 -20

No files matched your search

+50 -9
View File
@@ -247,6 +247,8 @@ def _task_from_row(row):
"forked_from": _col("forked_from"),
# 环境:老行缺列时 None = 不指定环境,走项目仓库路径。
"environment_id": _col("environment_id"),
# 续跑消息:老行缺列时 None = 没有待续跑消息。
"resume_message": _col("resume_message"),
}
@@ -580,7 +582,8 @@ class Worker:
"approval": "TEXT",
"plan": "INTEGER",
"forked_from": "TEXT",
"environment_id": "TEXT"}.items():
"environment_id": "TEXT",
"resume_message": "TEXT"}.items():
if name not in cols:
try:
conn.execute(
@@ -1018,7 +1021,7 @@ class Worker:
"host_status": None}
fake = SimpleNamespace(returncode=proc.returncode,
stdout=out or "", stderr=err or "")
if kind == "goal" and hasattr(
if kind in ("goal", "goal-resume") 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"):
@@ -1634,8 +1637,14 @@ class Worker:
effective_docker_timeout = self.task_timeout
try:
if self._use_popen():
if goal_mode and task.get("_resume"):
popen_kind = "goal-resume"
elif goal_mode:
popen_kind = "goal"
else:
popen_kind = "dispatch"
result = self._execute_via_process(
task, kind="goal" if goal_mode else "dispatch",
task, kind=popen_kind,
deadline_seconds=self.task_timeout,
seed_host_dir=seed_host_dir)
combined = (result.get("stdout", "") or "") + \
@@ -1663,12 +1672,20 @@ class Worker:
"stderr": ""},
prep_err)
if goal_mode:
result = self.executor.goal_task(
task, timeout=effective_docker_timeout,
extra_mounts=tenant_mounts,
network=tenant_network,
extra_env=proxy_env,
seed_host_dir=seed_host_dir)
if task.get("_resume"):
result = self.executor.goal_resume_task(
task, task["prompt"],
timeout=effective_docker_timeout,
extra_mounts=tenant_mounts,
network=tenant_network,
extra_env=proxy_env)
else:
result = self.executor.goal_task(
task, timeout=effective_docker_timeout,
extra_mounts=tenant_mounts,
network=tenant_network,
extra_env=proxy_env,
seed_host_dir=seed_host_dir)
else:
result = self.executor.dispatch_task(
task, timeout=effective_docker_timeout,
@@ -1836,6 +1853,15 @@ class Worker:
return {"dispatched": False, "reason": "already_claimed",
"task_id": task_id}
task = self.get_task(task_id) or task
# 续跑消息:旧任务继续会话时本轮要说的话——本轮 prompt 换
# 成它(只换内存里的副本,原 prompt 不动);_resume 标记
# 让 _run_attempt 在 goal 模式下走 goal-resume 而不是 goal
# (goal 会撞已存在,只有 goal-resume 能在旧线程上开新循环)。
resume_message = task.get("resume_message")
if resume_message:
task = dict(task)
task["prompt"] = resume_message
task["_resume"] = True
self._audit(task["project_id"], "worker_claim",
"%s executor=%s" % (task_id, self._executor_name()))
# log_event 只接受 task_id/project_id/status/message 四个
@@ -2153,6 +2179,21 @@ class Worker:
# 补一条 worker_completed/failed 审计误导排查。
final = "interrupted"
self._audit(task["project_id"], "worker_interrupted", task_id)
if resume_message:
# 本轮已跑完(无论成败):续跑消息已消费,清掉;进程
# 中途被杀时清不到,下次认领会重跑同一条消息——可接受
# 的续跑语义,不会静默丢消息。清失败不挡结果返回。
try:
conn = _db_connect(self.db_path)
try:
conn.execute(
"UPDATE tasks SET resume_message = NULL"
" WHERE id = ?", (task_id,))
conn.commit()
finally:
conn.close()
except Exception:
pass
return {"dispatched": True, "task_id": task_id,
"host_name": self.executor._host_name(task)
if hasattr(self.executor, "_host_name") else task_id,