Audit N-12: goal pushback preamble reaches breakpoint resume via --resume-note
ci / test (push) Failing after 35m34s
ci / test (push) Failing after 35m34s
Goal tasks pushed back by the audit loop lost the packet preamble on the next round: claim-time _apply_audit_loop_packet pasted it into the in-memory prompt/goal, but the next round collided with the existing host task name, fell back to breakpoint goal-resume (no --goal-text), and the host reused the stored goal, discarding the preamble. Fix (minimal, direction (c) trimmed): host goal-resume gains --resume-note, accepted only by the breakpoint branch and prepended to the first turn prompt of this resume only -- never written back to the stored goal, never touching rounds or verification. The worker stashes the same preamble as the memory-only _audit_loop_resume_note at claim time and forwards it on breakpoint resume (Local/Docker goal_resume_task and the popen goal-resume command, each guarded to ignore the note when goal_text is set for a new-goal resume). Accounting untouched: audit 3-round cap, goal_max_rounds/quota, and DB goal columns are unchanged; non-goal pushback is unchanged. Tests: cloud/tests/test_audit_n12.py (9 tests) -- host first-turn-only note + stored-goal/rounds, worker exists-fallback end-to-end with --resume-note, non-goal regression, goal-column accounting, and Local/Docker/popen command shapes. Full cloud suite: 1760 OK (skipped=11), baseline 1751 + 9 new; tests/test_host.py: 62 OK.
This commit is contained in:
1 parent
bc19f09dff
commit
159d7112ae
4 files changed
+421
-11
No files matched your search
+19
-2
@@ -687,7 +687,8 @@ class LocalExecutor(Executor):
|
||||
task["id"], self.runner(cmd))
|
||||
|
||||
def goal_resume_task(self, task, goal_text=None, extra_env=None,
|
||||
model_provider=None, max_rounds=None) -> dict:
|
||||
model_provider=None, max_rounds=None,
|
||||
resume_note=None) -> dict:
|
||||
"""调 host.py goal-resume,分两种调用(P1,与 worker、
|
||||
host 三处一致,绝不能混用):
|
||||
- 断点续跑:goal_text=None(worker 的 _resume_kind=
|
||||
@@ -699,6 +700,10 @@ class LocalExecutor(Executor):
|
||||
- 开新目标:goal_text 为新目标文本(云端 /continue 带
|
||||
goal_mode=1 时走这里)。命令带 --goal-text 与
|
||||
--max-rounds(默认 0,新语义不限轮次,上界是配额)。
|
||||
resume_note(N-12):仅断点续跑时用,作 --resume-note
|
||||
传给 host,host 只贴进本次续跑第一轮的 prompt,不写回
|
||||
存储的 goal、不改轮次与验收;goal_text 非空(开新目标)
|
||||
时忽略它(新目标自带 goal_text,不需要断点备注)。
|
||||
不传 --repo:goal-resume 操作已有 host 任务,不新建
|
||||
worktree。结论解析与 goal_task 同款。
|
||||
"""
|
||||
@@ -715,6 +720,8 @@ class LocalExecutor(Executor):
|
||||
str(max_rounds if max_rounds is not None else 0)]
|
||||
elif max_rounds is not None:
|
||||
cmd += ["--max-rounds", str(max_rounds)]
|
||||
if goal_text is None and resume_note:
|
||||
cmd += ["--resume-note", resume_note]
|
||||
return self._goal_result_from_runner(
|
||||
task["id"], self._run_local(cmd, extra_env))
|
||||
|
||||
@@ -863,10 +870,15 @@ class LocalExecutor(Executor):
|
||||
# 沿用原 goal+verify 断点续跑,绝不能用续跑消息覆盖
|
||||
# 原 verify);其它情况(开新目标)才用 task["prompt"]
|
||||
#(worker 已换成续跑消息)当 --goal-text,--max-rounds 0。
|
||||
# N-12:断点续跑且任务带审计推回备注时,另传
|
||||
# --resume-note(只贴第一轮 prompt,不写回存储的 goal)。
|
||||
cmd += ["goal-resume", "--task", self._host_name(task)]
|
||||
if task.get("_resume_kind") != "breakpoint":
|
||||
cmd += ["--goal-text", task["prompt"],
|
||||
"--max-rounds", "0"]
|
||||
elif task.get("_audit_loop_resume_note"):
|
||||
cmd += ["--resume-note",
|
||||
task["_audit_loop_resume_note"]]
|
||||
else:
|
||||
cmd += ["dispatch", "--name", self._host_name(task),
|
||||
"--task", task["prompt"]]
|
||||
@@ -1634,11 +1646,14 @@ class DockerExecutor(Executor):
|
||||
def goal_resume_task(self, task, goal_text=None, timeout=None,
|
||||
extra_mounts=None, network=None,
|
||||
extra_env=None, model_provider=None,
|
||||
max_rounds=None) -> dict:
|
||||
max_rounds=None, resume_note=None) -> dict:
|
||||
"""容器内调 host.py goal-resume,分两种调用(P1,与 Local
|
||||
goal_resume_task 同款契约):断点续跑传 goal_text=None
|
||||
(不带 --goal-text,host 沿用原 goal+verify+轮次),开新
|
||||
目标才传 goal_text(带 --goal-text 与 --max-rounds,默认 0)。
|
||||
resume_note(N-12):仅断点续跑时作 --resume-note 传给
|
||||
host(Local 同款语义:只贴第一轮 prompt,不写回存储的
|
||||
goal);goal_text 非空时忽略它。
|
||||
解析与 goal_task 同款(完整 stdout 结论、存库截尾),但
|
||||
没有“已存在”分支:goal-resume 本来就是操作已有 host 任务,
|
||||
不存在 dispatch 撞名问题;returncode 非 0 时:有结论仍
|
||||
@@ -1654,6 +1669,8 @@ class DockerExecutor(Executor):
|
||||
if max_rounds is not None else 0)]
|
||||
elif max_rounds is not None:
|
||||
_resume_args += ["--max-rounds", str(max_rounds)]
|
||||
if goal_text is None and resume_note:
|
||||
_resume_args += ["--resume-note", resume_note]
|
||||
inner = self._inner_host_command(
|
||||
*_resume_args,
|
||||
model=task.get("model"), effort=task.get("effort"),
|
||||
|
||||
@@ -0,0 +1,355 @@
|
||||
"""N-12:goal 任务审计推回包前言在断点续跑时丢失的修复测试。
|
||||
|
||||
缺口链路(批 5 定性):认领时 _apply_audit_loop_packet 把包
|
||||
前言贴进内存 prompt 与 goal,但推回后的 goal 任务下一轮无
|
||||
_resume、无断点标记,先走 goal_task 撞宿主任务名已存在,再
|
||||
经已存在回落走断点续跑(goal-resume 不传 --goal-text,宿主
|
||||
沿用存储的原 goal),内存 goal 里的前言被覆盖、前言丢失。
|
||||
|
||||
修复(c 精简版):新增 host 侧 --resume-note 参数,只由断
|
||||
点分支受理,只贴进本次续跑第一轮的 prompt,绝不写回存储
|
||||
的 goal、不改轮次与验收;worker 认领贴前言时另存内存键
|
||||
_audit_loop_resume_note,断点续跑时作 --resume-note 透传
|
||||
(Local/Docker 的 goal_resume_task 与 popen 的 goal-resume
|
||||
命令构造各有配对守卫:goal_text 已设时忽略备注)。
|
||||
|
||||
只用注入的假 runner / 假 server,不跑真实模型回合、不联
|
||||
网、不登录任何服务。断言口径:包前言渲染的是 findings
|
||||
(id/证据/复现/验收)与固定收尾句,criteria 是包里的门映
|
||||
射(由接线会计用,不渲染进前言文本),所以“前言到达”
|
||||
断言为前言文本含 finding id 与证据/验收句;criteria 仍在
|
||||
包 JSON 里可读、接线会计不变。
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import sqlite3
|
||||
import sys
|
||||
import tempfile
|
||||
import time
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
from unittest import mock
|
||||
|
||||
try:
|
||||
from cloud.sandbox import DockerExecutor, LocalExecutor
|
||||
from cloud.server import init_db
|
||||
from cloud.worker import Worker
|
||||
except ImportError: # discover -s cloud/tests 时把项目根目录补进 sys.path
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parents[2]))
|
||||
from cloud.sandbox import DockerExecutor, LocalExecutor
|
||||
from cloud.server import init_db
|
||||
from cloud.worker import Worker
|
||||
|
||||
|
||||
def _host_mod():
|
||||
"""host.py 只在测试执行时才导入,不在模块层导入:host.py
|
||||
有 `import queue`(标准库),模块层导入会先于
|
||||
test_queue_sandbox 的模块导入把标准库 queue 写进
|
||||
sys.modules,导致后者 `from queue import TaskQueue`
|
||||
(本意是 cloud/queue.py)拿到标准库而导入失败、47 项
|
||||
用例缺席。全部模块发现完成后再导入 host 就不受影响
|
||||
(test_queue_sandbox 的名字在发现时已绑定 cloud/queue)。"""
|
||||
try:
|
||||
import host as host_mod
|
||||
except ImportError:
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parents[2]))
|
||||
import host as host_mod
|
||||
return host_mod
|
||||
|
||||
|
||||
def _packet(task_id="TG", round_no=1):
|
||||
return json.dumps({
|
||||
"task_id": task_id, "round": round_no,
|
||||
"findings": [{"id": "execution_failed", "severity": "high",
|
||||
"evidence": "boom detail", "reproduction": "re-run",
|
||||
"acceptance": "task completes",
|
||||
"kind": "deterministic", "gate": "execution"}],
|
||||
"criteria": {"execution_failed": "execution"}})
|
||||
|
||||
|
||||
def _insert_task(db_path, task_id, prompt="do work", status="queued",
|
||||
project_id="p1", audit_loop=0, packet=None,
|
||||
goal_mode=0, goal=None, verify_command=None,
|
||||
goal_max_rounds=None):
|
||||
now = time.time()
|
||||
conn = sqlite3.connect(db_path)
|
||||
try:
|
||||
conn.execute(
|
||||
"INSERT INTO tasks(id, project_id, prompt, status,"
|
||||
" idempotency_key, depends_on, priority, created_by,"
|
||||
" created_at, updated_at, last_heartbeat, audit_loop,"
|
||||
" audit_loop_packet, goal_mode, goal, verify_command,"
|
||||
" goal_max_rounds)"
|
||||
" VALUES(?, ?, ?, ?, NULL, '[]', 0, 'tester', ?, ?, NULL,"
|
||||
" ?, ?, ?, ?, ?, ?)",
|
||||
(task_id, project_id, prompt, status, now, now,
|
||||
audit_loop, packet, goal_mode, goal, verify_command,
|
||||
goal_max_rounds),
|
||||
)
|
||||
conn.commit()
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
def _row(db_path, task_id):
|
||||
conn = sqlite3.connect(db_path)
|
||||
try:
|
||||
return conn.execute(
|
||||
"SELECT status, prompt, goal, verify_command,"
|
||||
" goal_max_rounds, audit_loop_packet"
|
||||
" FROM tasks WHERE id = ?", (task_id,)).fetchone()
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
class _FakeServer:
|
||||
"""只记 prompt、不跑模型:run_turn 返回 completed。"""
|
||||
|
||||
def __init__(self):
|
||||
self.prompts = []
|
||||
|
||||
def run_turn(self, thread_id, prompt, **kwargs):
|
||||
self.prompts.append(prompt)
|
||||
return {"status": "completed", "text": "did work", "usage": {}}
|
||||
|
||||
|
||||
def _host_state(tmp, task):
|
||||
state_path = os.path.join(tmp, "host-state.json")
|
||||
state = {"tasks": {task["name"]: task}, "automations": [],
|
||||
"usage": {}}
|
||||
return state, state_path
|
||||
|
||||
|
||||
class HostResumeNoteTests(unittest.TestCase):
|
||||
"""host 侧:--resume-note 只贴首轮、不写回存储 goal、不改轮次。"""
|
||||
|
||||
def test_breakpoint_note_first_turn_only_and_rounds_continue(self):
|
||||
host_mod = _host_mod()
|
||||
srv = _FakeServer()
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
task = {"name": "g", "thread_id": "x", "cwd": tmp,
|
||||
"sandbox": None, "max_retries": 0, "retry_count": 0,
|
||||
"turns": [], "status": "goal-running",
|
||||
"goal_rounds_done": 1}
|
||||
state, state_path = _host_state(tmp, task)
|
||||
note = ("[审计推回包(第 1 轮)]\n"
|
||||
"- [execution_failed] 证据:boom detail")
|
||||
host_mod.run_goal_loop(
|
||||
srv, state, state_path, task, "orig goal", "false",
|
||||
3, "m", "low", None, start_round=2,
|
||||
last_verify="", last_turn_status=None,
|
||||
last_turn_text="", resume_note=note)
|
||||
self.assertEqual(len(srv.prompts), 2)
|
||||
self.assertTrue(srv.prompts[0].startswith(note))
|
||||
self.assertNotIn("execution_failed", srv.prompts[1])
|
||||
# 存储的 goal 不变(备注只是本次续跑首轮的展示前缀),
|
||||
# 轮次从断点续接:1(已做)+ 2 轮 = 3,不重置、不跳号。
|
||||
self.assertEqual(task["goal"], "orig goal")
|
||||
self.assertEqual(task["goal_rounds_done"], 3)
|
||||
self.assertEqual(task["status"], "goal-unmet")
|
||||
|
||||
def test_met_path_note_first_turn_only_and_goal_unchanged(self):
|
||||
host_mod = _host_mod()
|
||||
srv = _FakeServer()
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
task = {"name": "g", "thread_id": "x", "cwd": tmp,
|
||||
"sandbox": None, "max_retries": 0, "retry_count": 0,
|
||||
"turns": []}
|
||||
state, state_path = _host_state(tmp, task)
|
||||
note = "[审计推回包(第 1 轮)]\n- [execution_failed]"
|
||||
result = host_mod.run_goal_loop(
|
||||
srv, state, state_path, task, "orig goal", "true",
|
||||
3, "m", "low", None, resume_note=note)
|
||||
self.assertTrue(result["ok"])
|
||||
self.assertEqual(len(srv.prompts), 1)
|
||||
self.assertTrue(srv.prompts[0].startswith(note))
|
||||
self.assertIn("orig goal", srv.prompts[0])
|
||||
self.assertEqual(task["goal"], "orig goal")
|
||||
|
||||
def test_no_note_behavior_unchanged(self):
|
||||
host_mod = _host_mod()
|
||||
srv = _FakeServer()
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
task = {"name": "g", "thread_id": "x", "cwd": tmp,
|
||||
"sandbox": None, "max_retries": 0, "retry_count": 0,
|
||||
"turns": []}
|
||||
state, state_path = _host_state(tmp, task)
|
||||
host_mod.run_goal_loop(
|
||||
srv, state, state_path, task, "orig goal", "true",
|
||||
3, "m", "low", None)
|
||||
self.assertEqual(srv.prompts, ["orig goal"])
|
||||
self.assertEqual(task["goal"], "orig goal")
|
||||
|
||||
|
||||
class WorkerResumeNoteTests(unittest.TestCase):
|
||||
"""worker 侧:推回 goal 任务经已存在回落走断点续跑时带备注。"""
|
||||
|
||||
def setUp(self):
|
||||
self.tmp = tempfile.TemporaryDirectory()
|
||||
self.db_path = os.path.join(self.tmp.name, "control.db")
|
||||
init_db(self.db_path)
|
||||
self.env = {"XCM_API_KEY": "test-placeholder"}
|
||||
self.seen = []
|
||||
|
||||
def tearDown(self):
|
||||
self.tmp.cleanup()
|
||||
|
||||
def _worker(self, runner):
|
||||
return Worker(self.db_path, "/fake/host.py", runner=runner,
|
||||
env=self.env, max_retries=0,
|
||||
retry_backoff_seconds=0)
|
||||
|
||||
def _fake_runner(self, cmd):
|
||||
self.seen.append(list(cmd))
|
||||
if "goal-resume" in cmd:
|
||||
return SimpleNamespace(
|
||||
returncode=0,
|
||||
stdout="round 1: turn=completed verify_rc=-\n"
|
||||
"goal met after rounds\n",
|
||||
stderr="")
|
||||
return SimpleNamespace(returncode=0,
|
||||
stdout="task 已存在", stderr="")
|
||||
|
||||
def test_pushback_goal_fallback_resume_carries_note(self):
|
||||
_insert_task(self.db_path, "TG", prompt="do goal work",
|
||||
audit_loop=1, packet=_packet(), goal_mode=1,
|
||||
goal="orig goal", verify_command="false",
|
||||
goal_max_rounds=3)
|
||||
worker = self._worker(self._fake_runner)
|
||||
result = worker.run_one()
|
||||
self.assertEqual(result["status"], "completed", result)
|
||||
# 第一程 goal(撞已存在),第二程断点续跑(带备注)。
|
||||
self.assertEqual(len(self.seen), 2)
|
||||
self.assertIn("goal", self.seen[0])
|
||||
self.assertNotIn("goal-resume", self.seen[0])
|
||||
resume_cmd = self.seen[1]
|
||||
self.assertIn("goal-resume", resume_cmd)
|
||||
self.assertNotIn("--goal-text", resume_cmd)
|
||||
self.assertIn("--resume-note", resume_cmd)
|
||||
note = resume_cmd[resume_cmd.index("--resume-note") + 1]
|
||||
self.assertIn("execution_failed", note)
|
||||
self.assertIn("boom detail", note)
|
||||
self.assertIn("task completes", note)
|
||||
# 库内原 goal / prompt / 验收 / 轮次上限一律不动(备注只
|
||||
# 走本次续跑输入,不写回库)。
|
||||
row = _row(self.db_path, "TG")
|
||||
self.assertEqual(row[0], "completed")
|
||||
self.assertEqual(row[1], "do goal work")
|
||||
self.assertEqual(row[2], "orig goal")
|
||||
self.assertEqual(row[3], "false")
|
||||
self.assertEqual(row[4], 3)
|
||||
|
||||
def test_apply_packet_keeps_goal_columns_and_stashes_note(self):
|
||||
worker = self._worker(self._fake_runner)
|
||||
task = {"id": "TG", "project_id": "p1", "prompt": "do goal work",
|
||||
"goal": "orig goal", "goal_mode": 1,
|
||||
"verify_command": "false", "goal_max_rounds": 3,
|
||||
"audit_loop_packet": _packet()}
|
||||
out = worker._apply_audit_loop_packet(task)
|
||||
self.assertTrue(out["prompt"].startswith("[审计推回包"))
|
||||
self.assertIn("execution_failed", out["goal"])
|
||||
self.assertEqual(out["verify_command"], "false")
|
||||
self.assertEqual(out["goal_max_rounds"], 3)
|
||||
self.assertIn("execution_failed",
|
||||
out["_audit_loop_resume_note"])
|
||||
again = worker._apply_audit_loop_packet(out)
|
||||
self.assertEqual(again["prompt"].count("execution_failed"), 1)
|
||||
|
||||
def test_non_goal_pushback_unchanged(self):
|
||||
_insert_task(self.db_path, "TP", prompt="do work",
|
||||
audit_loop=1, packet=_packet(task_id="TP"))
|
||||
worker = self._worker(self._fake_runner)
|
||||
task = worker.get_task("TP")
|
||||
out = worker._apply_audit_loop_packet(task)
|
||||
self.assertTrue(out["prompt"].startswith("[审计推回包"))
|
||||
self.assertIn("execution_failed", out["prompt"])
|
||||
# 库内原 prompt 不动(只改内存副本)。
|
||||
self.assertEqual(_row(self.db_path, "TP")[1], "do work")
|
||||
|
||||
|
||||
class CommandShapeTests(unittest.TestCase):
|
||||
"""命令形状:有备注时含 --resume-note,无备注时无,
|
||||
goal_text 已设(开新目标)时忽略备注。"""
|
||||
|
||||
def test_local_goal_resume_note_shape(self):
|
||||
calls = []
|
||||
|
||||
def fake(cmd):
|
||||
calls.append(list(cmd))
|
||||
return SimpleNamespace(
|
||||
returncode=0,
|
||||
stdout="goal met after rounds\n",
|
||||
stderr="")
|
||||
|
||||
task = {"id": "T1", "project_id": "p1", "prompt": "orig goal",
|
||||
"verify_command": "true", "goal_max_rounds": 3}
|
||||
local = LocalExecutor("/fake/host.py", runner=fake)
|
||||
local.goal_resume_task(task, None, resume_note="NOTE")
|
||||
self.assertIn("--resume-note", calls[-1])
|
||||
self.assertIn("NOTE", calls[-1])
|
||||
self.assertNotIn("--goal-text", calls[-1])
|
||||
local.goal_resume_task(task, None)
|
||||
self.assertNotIn("--resume-note", calls[-1])
|
||||
local.goal_resume_task(task, "new goal", resume_note="NOTE")
|
||||
self.assertIn("--goal-text", calls[-1])
|
||||
self.assertNotIn("--resume-note", calls[-1])
|
||||
|
||||
def test_docker_goal_resume_note_shape(self):
|
||||
calls = []
|
||||
|
||||
def dfake(cmd, timeout=None):
|
||||
calls.append(list(cmd))
|
||||
return SimpleNamespace(
|
||||
returncode=0,
|
||||
stdout="goal met after rounds\n",
|
||||
stderr="")
|
||||
|
||||
task = {"id": "T1", "project_id": "p1", "prompt": "orig goal",
|
||||
"verify_command": "true", "goal_max_rounds": 3}
|
||||
docker = DockerExecutor(
|
||||
runner=dfake, env={"XCM_API_KEY": "test-placeholder"})
|
||||
with mock.patch("cloud.sandbox.shutil.which",
|
||||
return_value="/usr/bin/docker"):
|
||||
docker.goal_resume_task(task, None, resume_note="NOTE")
|
||||
self.assertIn("--resume-note", calls[-1])
|
||||
self.assertIn("NOTE", calls[-1])
|
||||
with mock.patch("cloud.sandbox.shutil.which",
|
||||
return_value="/usr/bin/docker"):
|
||||
docker.goal_resume_task(task, None)
|
||||
self.assertNotIn("--resume-note", calls[-1])
|
||||
with mock.patch("cloud.sandbox.shutil.which",
|
||||
return_value="/usr/bin/docker"):
|
||||
docker.goal_resume_task(task, "new goal",
|
||||
resume_note="NOTE")
|
||||
self.assertIn("--goal-text", calls[-1])
|
||||
self.assertNotIn("--resume-note", calls[-1])
|
||||
|
||||
def test_popen_goal_resume_breakpoint_note_shape(self):
|
||||
local = LocalExecutor("/fake/host.py", runner=lambda cmd: None)
|
||||
with mock.patch.object(
|
||||
__import__("cloud.sandbox", fromlist=["subprocess"])
|
||||
.subprocess, "Popen") as popen:
|
||||
popen.return_value = SimpleNamespace(pid=9999)
|
||||
local.popen_task(
|
||||
{"id": "T1", "prompt": "resume msg",
|
||||
"_resume_kind": "breakpoint",
|
||||
"_audit_loop_resume_note": "NOTE"},
|
||||
kind="goal-resume")
|
||||
cmd = popen.call_args[0][0]
|
||||
self.assertIn("goal-resume", cmd)
|
||||
self.assertNotIn("--goal-text", cmd)
|
||||
self.assertIn("--resume-note", cmd)
|
||||
self.assertIn("NOTE", cmd)
|
||||
# 无备注的断点续跑:命令形状与旧版一致。
|
||||
local.popen_task(
|
||||
{"id": "T1", "prompt": "resume msg",
|
||||
"_resume_kind": "breakpoint"},
|
||||
kind="goal-resume")
|
||||
cmd = popen.call_args[0][0]
|
||||
self.assertNotIn("--resume-note", cmd)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
+24
-7
@@ -2194,10 +2194,12 @@ class Worker:
|
||||
packet 对 goal 任务同步贴两者),但 goal 任务下一轮
|
||||
实际会撞宿主任务名已存在、经 run_one 的已存在回落走
|
||||
断点续跑(goal-resume 不传 --goal-text,宿主用存储的
|
||||
goal 与 goal_rounds_done+1 继续),所以包前言贴了却
|
||||
不进续跑循环——这是审计推回对 goal 任务的已知效果
|
||||
缺口,不是轮次会计错误,本批不改行为、留待后续批
|
||||
定夺。轮次会计本身有界:审计轮次硬上限 3(MAX_
|
||||
goal 与 goal_rounds_done+1 继续)。N-12 修复:认领贴
|
||||
前言时另存 _audit_loop_resume_note,断点续跑时作
|
||||
--resume-note 传给 host,host 只贴进本次续跑第一轮的
|
||||
prompt,不写回存储的 goal、不改轮次与验收——前言从此
|
||||
到达续跑循环,库内原 goal 与宿主存储的 goal 均不动。
|
||||
轮次会计本身有界:审计轮次硬上限 3(MAX_
|
||||
PUSHBACKS),3 轮后必升级;goal 旧形态总回合数受
|
||||
goal_max_rounds 约束(续跑不重置计数),新形态受配额
|
||||
约束;两组计数器独立、不叠加成无界循环。
|
||||
@@ -2720,7 +2722,10 @@ class Worker:
|
||||
顺序是目录树 → 推回包 → 重跑前言 → 项目上下文 → 原
|
||||
prompt,推回包是最需要 agent 先看到的一段。库里的
|
||||
原 prompt 与包列都不动,包列只在 pass/escalate 时由
|
||||
接线清理、pushback 时由接线覆盖。
|
||||
接线清理、pushback 时由接线覆盖。另存
|
||||
_audit_loop_resume_note(N-12):同一份前言的副本,
|
||||
仅内存键,供断点续跑时作 --resume-note 传给 host;
|
||||
非 goal 任务与开新目标续跑忽略它。
|
||||
"""
|
||||
if task.get("coordinator_parts"):
|
||||
return task
|
||||
@@ -2748,6 +2753,14 @@ class Worker:
|
||||
if task.get("goal_mode") or task.get("verify_command"):
|
||||
new_task["goal"] = preamble + "\n\n" + (
|
||||
task.get("goal") or task.get("prompt") or "")
|
||||
# N-12:前言另存一份作断点续跑备注(仅内存键,不进库)。
|
||||
# goal 任务下一轮撞“已存在”走断点续跑时,host 只用存储
|
||||
# 的原 goal(见 _run_attempt 的 goal_resume 调用),内存
|
||||
# goal 里的前言到不了 agent;_run_attempt 在断点续跑时
|
||||
# 把这份备注作 --resume-note 传给 host,host 只贴进本次
|
||||
# 续跑第一轮的 prompt,不写回存储的 goal。非 goal 任务
|
||||
# 与开新目标续跑忽略此键(sandbox 侧的配对守卫)。
|
||||
new_task["_audit_loop_resume_note"] = preamble
|
||||
new_task["_audit_loop_packet_injected"] = True
|
||||
self._audit(task["project_id"], "worker_audit_loop_packet_injected",
|
||||
task["id"])
|
||||
@@ -5553,7 +5566,9 @@ class Worker:
|
||||
network=tenant_network,
|
||||
extra_env=_merge_env(proxy_env,
|
||||
gateway_env),
|
||||
model_provider=gateway_provider),
|
||||
model_provider=gateway_provider,
|
||||
resume_note=task.get(
|
||||
"_audit_loop_resume_note")),
|
||||
quota_check)
|
||||
else:
|
||||
result = self._docker_call_with_quota_poll(
|
||||
@@ -5620,7 +5635,9 @@ class Worker:
|
||||
result = self.executor.goal_resume_task(
|
||||
task, _resume_text,
|
||||
extra_env=gateway_env,
|
||||
model_provider=gateway_provider)
|
||||
model_provider=gateway_provider,
|
||||
resume_note=task.get(
|
||||
"_audit_loop_resume_note"))
|
||||
elif isinstance(self.executor, LocalExecutor):
|
||||
result = self.executor.goal_task(
|
||||
task, seed_dir=seed_host_dir,
|
||||
|
||||
@@ -1950,7 +1950,8 @@ def _llm_declared_done(text):
|
||||
def run_goal_loop(server, state, state_path, task, goal_text, verify,
|
||||
max_rounds, model, effort, sandbox, cfg=None,
|
||||
timeout=None, verify_timeout=600, start_round=1,
|
||||
last_verify="", last_turn_status=None, last_turn_text=""):
|
||||
last_verify="", last_turn_status=None, last_turn_text="",
|
||||
resume_note=None):
|
||||
"""目标循环(长任务版,对齐 Codex App 的可续跑语义)。
|
||||
|
||||
两种验收形态(2026-10-27 起,console goal 模式走新形态):
|
||||
@@ -1980,6 +1981,12 @@ def run_goal_loop(server, state, state_path, task, goal_text, verify,
|
||||
判 failed,60 分钟工作无续跑口)。
|
||||
goal-unmet 穷尽轮次后不允许 goal-resume(按既有规则须新建
|
||||
任务),goal-resume 只接受 goal-running / interrupted / running。
|
||||
resume_note(N-12 审计推回前言续跑缺口):仅断点续跑分支
|
||||
传,贴进本次续跑第一轮的 prompt 最前面(仅展示,不写回
|
||||
task["goal"]、不进存储的 goal、不改轮次与验收;后续轮次
|
||||
的 prompt 不再重复贴,同一 thread 的历史里已有它)。不传
|
||||
时行为与旧版逐字相同;开新目标分支绝不传它(新目标自带
|
||||
goal_text,不需要也不接受断点备注)。
|
||||
"""
|
||||
task["goal"] = goal_text
|
||||
task["verify_command"] = verify
|
||||
@@ -2019,6 +2026,9 @@ def run_goal_loop(server, state, state_path, task, goal_text, verify,
|
||||
# max_rounds=0 是不限轮次:while 循环只在模型宣告、预算耗尽
|
||||
# 或取消时结束;非 0 时按旧语义穷尽轮次即 unmet。
|
||||
round_no = int(start_round) - 1
|
||||
# N-12:断点备注只贴本次续跑的第一轮 prompt(thread 历史
|
||||
# 会把它带到后续轮次,不必每轮重复贴、避免 token 膨胀)。
|
||||
_first_turn = True
|
||||
# 连续失败轮次护栏:回合瞬间失败(如密钥缺失)时不限轮次
|
||||
# 循环会无限空转(2026-10-29 实弹:断点后 274 轮全失败,
|
||||
# state 文件持续增长)。3 轮连续失败即走中断出口,任务
|
||||
@@ -2072,6 +2082,12 @@ def run_goal_loop(server, state, state_path, task, goal_text, verify,
|
||||
f"目标:{goal_text}\n"
|
||||
f"验收命令:{verify}\n"
|
||||
f"上一轮验收输出:\n{last_verify}")
|
||||
# N-12:断点备注只贴第一轮 prompt(见 _first_turn 注释),
|
||||
# 绝不写回 goal_text——task["goal"] 在循环开始时已按原
|
||||
# goal_text 落盘,备注只是本次续跑的展示前缀。
|
||||
if _first_turn and resume_note:
|
||||
prompt = "%s\n\n%s" % (resume_note, prompt)
|
||||
_first_turn = False
|
||||
try:
|
||||
result = run_task_turn(server, state, task, prompt,
|
||||
model, effort, sandbox, timeout=timeout,
|
||||
@@ -2362,6 +2378,10 @@ def main():
|
||||
"传了会把原验收命令覆盖成 None")
|
||||
p.add_argument("--max-rounds", type=int, default=None,
|
||||
help="不传时沿用原任务的 goal_max_rounds;传了则覆盖总轮次")
|
||||
p.add_argument("--resume-note", default=None,
|
||||
help="断点续跑备注(N-12):仅断点续跑(不传 --goal-text)时用,"
|
||||
"只贴进本次续跑第一轮的 prompt,不写回存储的 goal、不改"
|
||||
"轮次与验收;与 --goal-text 同传时忽略(开新目标分支不用它)")
|
||||
p.add_argument("--verify-timeout", type=int, default=600,
|
||||
help="验测命令上限(秒);0 表示不限时,默认 600")
|
||||
p = sub.add_parser("parallel"); p.add_argument("--jobs", required=True)
|
||||
@@ -3258,7 +3278,8 @@ def main():
|
||||
start_round=int(t.get("goal_rounds_done") or 0) + 1,
|
||||
last_verify=t.get("goal_last_verify") or "",
|
||||
last_turn_status=t.get("goal_last_turn_status"),
|
||||
last_turn_text=last_result.get("text", "") or "")
|
||||
last_turn_text=last_result.get("text", "") or "",
|
||||
resume_note=args.resume_note)
|
||||
elif args.cmd == "parallel":
|
||||
jobs = json.load(open(args.jobs))
|
||||
# 依赖 + 优先级:先拓扑排序,缺依赖/成环直接报错不跑;
|
||||
|
||||
Reference in new issue
Block a user