code-to10 goal live update: POST /tasks/{id}/goal updates a running goal loop at the next round boundary
Server writes goal + monotonic goal_version + task_goal_history in one transaction (CAS on version, approve_merge only, new-semantic goal tasks only, terminal/merged 409, same-goal noop). Worker mirror thread (30s) and breakpoint-resume claim hook write the update as a signal file next to host-state.json; host run_goal_loop applies it at round boundaries (top of round and before the declaration check, so a declaration made for the old goal in a turn during which the goal changed is voided), with an explicit old->new notice, same thread/worktree, rounds/usage/ budget/failed counters untouched. handler method accounting is a contract update: 239 total / 223 mixins / 16 in server.py. (cherry picked from commit 6841f287219d2b36cfbb8803642344b84dae4377) Reconcile-port: handler_lifecycle.py contents redirected — _update_goal_task method + _goal_update_reject/_goal_signal_status pure functions + goal_live_update import into cloud/server.py (target keeps 6-mixin split); _task_to_dict goal fields into cloud/handler_support.py; migrate columns into cloud/server.py migrate_tasks_table; test patch target handler_lifecycle→server; test_handler_split_conformance.py kept at target version, to be re-pinned for the unified line in a final commit.
This commit is contained in:
1 parent
63abb3953b
commit
9e56c1d266
6 files changed
+1110
-2
No files matched your search
+109
@@ -168,6 +168,9 @@ from cloud.sandbox_policy import cleanup_decision as _cleanup_decision
|
||||
from cloud.timeouts import STALE_SWEEP_SECONDS as _STALE_SWEEP
|
||||
from cloud.db import connect as _db_connect
|
||||
from cloud.goal_command import parse_goal_command as _parse_goal_command
|
||||
from cloud.goal_live_update import (
|
||||
write_goal_update_signal as _glu_write_signal,
|
||||
)
|
||||
from cloud.logging_config import get_logger, log_event, setup_logging
|
||||
from cloud.models import ModelRouter, is_fallbackable, record_fallback
|
||||
from cloud.auto_memory import AutoMemoryStore
|
||||
@@ -433,6 +436,10 @@ def _task_from_row(row):
|
||||
# 新 goal 语义:goal_mode=1 时 prompt 即目标,无验收命令、
|
||||
# 不限轮次;老行缺列时 None = 按 verify_command 判定。
|
||||
"goal_mode": _col("goal_mode"),
|
||||
# 运行中改 goal:已提交版本与循环已应用版本,老行缺列
|
||||
# 时按 0 计(= 从未改过 goal,信号版本从 1 起算)。
|
||||
"goal_version": _col("goal_version") or 0,
|
||||
"applied_goal_version": _col("applied_goal_version") or 0,
|
||||
# 任务级选项:老行缺列时 None = 跟随配置(见 server 侧)。
|
||||
"model": _col("model"),
|
||||
"effort": _col("effort"),
|
||||
@@ -1270,6 +1277,68 @@ class Worker:
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
def _write_goal_signal_if_newer(self, task, db_goal, db_version):
|
||||
"""运行中改 goal 的信号写侧(worker 侧唯一写点)。
|
||||
|
||||
state 文件里的 applied_goal_version 是 host 循环上次
|
||||
检查点落盘的已应用版本;库里 goal_version 更大时,
|
||||
把新 goal 写成信号文件(host-state 同目录的
|
||||
goal-updates/<host 名>.json,host 循环在轮次边界读)。
|
||||
任何异常一律返回 False,信号写失败绝不能炸掉任务
|
||||
执行或镜像本身。state 文件不存在(Local/Docker 的
|
||||
work 挂载还没建起来、或 CloudExecutor 根本没有
|
||||
host-state)时同样返回 False,不猜路径。
|
||||
"""
|
||||
try:
|
||||
if not db_goal or not isinstance(db_goal, str):
|
||||
return False
|
||||
version = int(db_version or 0)
|
||||
if version < 1:
|
||||
return False
|
||||
state_path = self._host_state_path_for_task(task)
|
||||
if not state_path or not os.path.exists(state_path):
|
||||
return False
|
||||
with open(state_path, encoding="utf-8") as fh:
|
||||
state = json.load(fh)
|
||||
host_name = _worker_host_name(self.executor, task)
|
||||
entry = (state.get("tasks") or {}).get(host_name)
|
||||
if not isinstance(entry, dict):
|
||||
return False
|
||||
applied = int(entry.get("applied_goal_version") or 0)
|
||||
if version <= applied:
|
||||
return False
|
||||
_glu_write_signal(state_path, host_name, version,
|
||||
db_goal, task.get("id"))
|
||||
return True
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
def _sync_resume_goal_signal(self, task):
|
||||
"""断点续跑认领时钩子:库里新 goal 先写成信号再起循环。
|
||||
|
||||
镜像线程只在循环跑起来之后才工作,已中断任务没有
|
||||
在跑的镜像线程,所以认领断点续跑时必须先读一次库:
|
||||
库里 goal_version 大于 state 文件已应用版本时写信号,
|
||||
host 循环在断点轮次的下一轮(第一个轮次边界)应用
|
||||
它,轮次不断点、不回到第 1 轮。任何异常返回 False,
|
||||
续跑本身照常走断点(用旧 goal),不因信号失败而炸。
|
||||
"""
|
||||
try:
|
||||
conn = _db_connect(self.db_path)
|
||||
try:
|
||||
row = conn.execute(
|
||||
"SELECT goal, goal_version FROM tasks WHERE id = ?",
|
||||
(task["id"],)).fetchone()
|
||||
finally:
|
||||
conn.close()
|
||||
if row is None:
|
||||
return False
|
||||
goal = row[0] if len(row) > 0 else None
|
||||
version = row[1] if len(row) > 1 else None
|
||||
return self._write_goal_signal_if_newer(task, goal, version)
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
def _mirror_checkpoint_once(self, task):
|
||||
"""把磁盘 host-state 里的本任务断点镜像进库(单次)。
|
||||
|
||||
@@ -1302,6 +1371,34 @@ class Worker:
|
||||
conn = _db_connect(self.db_path)
|
||||
try:
|
||||
self._ensure_result_columns(conn)
|
||||
# 运行中改 goal 可见性(只在变化时写):host 循环
|
||||
# 检查点里的 applied_goal_version 回写任务行,
|
||||
# 调用方据此区分“已提交的新版本”与“循环已应用
|
||||
# 的版本”,两者不等时绝不冒充已应用。
|
||||
if entry.get("applied_goal_version") is not None:
|
||||
try:
|
||||
applied_v = int(entry["applied_goal_version"])
|
||||
except (TypeError, ValueError):
|
||||
applied_v = None
|
||||
if applied_v is not None:
|
||||
conn.execute(
|
||||
"UPDATE tasks SET applied_goal_version = ?"
|
||||
" WHERE id = ? AND (applied_goal_version"
|
||||
" IS NULL OR applied_goal_version <> ?)",
|
||||
(applied_v, task["id"], applied_v))
|
||||
# 运行中改 goal 反向信号:库里 goal_version 大于
|
||||
# state 已应用版本时写信号文件,host 循环在下个
|
||||
# 轮次边界应用;读库失败时跳过(下个 30 秒周期
|
||||
# 再试),信号写失败由 helper 内部吞掉。
|
||||
try:
|
||||
db_row = conn.execute(
|
||||
"SELECT goal, goal_version FROM tasks"
|
||||
" WHERE id = ?", (task["id"],)).fetchone()
|
||||
except Exception:
|
||||
db_row = None
|
||||
if db_row is not None:
|
||||
self._write_goal_signal_if_newer(
|
||||
task, db_row[0], db_row[1])
|
||||
existing = conn.execute(
|
||||
"SELECT 1 FROM task_checkpoints"
|
||||
" WHERE task_id = ? AND round_no = ?",
|
||||
@@ -1525,6 +1622,9 @@ class Worker:
|
||||
"verify_command": "TEXT",
|
||||
"goal_max_rounds": "INTEGER",
|
||||
"goal_mode": "INTEGER",
|
||||
# 运行中改 goal:与 server.migrate_tasks_table 同列。
|
||||
"goal_version": "INTEGER",
|
||||
"applied_goal_version": "INTEGER",
|
||||
"worktree_host": "TEXT",
|
||||
"claimed_by_worker": "TEXT",
|
||||
"model": "TEXT",
|
||||
@@ -4688,6 +4788,15 @@ class Worker:
|
||||
except Exception:
|
||||
_mirror_stop = None
|
||||
_mirror_thread = None
|
||||
# 运行中改 goal 认领钩子:断点续跑时镜像线程还没跑,
|
||||
# 必须在起循环之前先把库里新 goal 写成信号,否则断点
|
||||
# 续跑会沿用 host-state 里的旧 goal。钩子只写信号,
|
||||
# 失败时续跑照常用旧 goal,不炸执行(见 helper 文档)。
|
||||
if goal_mode and task.get("_goal_resume_breakpoint"):
|
||||
try:
|
||||
self._sync_resume_goal_signal(task)
|
||||
except Exception:
|
||||
pass
|
||||
if is_docker and hasattr(self.executor, "kill_container"):
|
||||
try:
|
||||
_container_name = self.executor.container_name(task)
|
||||
|
||||
Reference in new issue
Block a user