diff --git a/cloud/tests/test_goal_live_update.py b/cloud/tests/test_goal_live_update.py index dba0af3..322b7a6 100644 --- a/cloud/tests/test_goal_live_update.py +++ b/cloud/tests/test_goal_live_update.py @@ -504,6 +504,33 @@ class HostLoopTestCase(unittest.TestCase): self.assertIn("new goal", prompts[0]) self.assertIn("旧目标", prompts[0]) + def test_first_turn_both_pending_note_before_notice(self): + # FLAG-3 定序(v2 演练决定):首轮断点备注与改 goal 通知 + # 同 pending 时,模型可见顺序为备注在前、通知在后。 + # 注意代码里两次 prepend 的执行顺序与可见顺序相反 + # (先贴通知、再贴备注),本测试钉住的是可见顺序, + # 而非执行顺序。 + task = self._task() + self._write_signal(1, "new goal") + prompts = [] + script = [("completed", "目标已达成", None)] + fake = _fake_turn_script(prompts, script) + note = ("[审计推回包(第 1 轮)]\n" + "- [execution_failed] 证据:boom detail") + with mock.patch.object(host_mod, "run_task_turn", fake): + result = host_mod.run_goal_loop( + SimpleNamespace(), self.state, self.state_path, task, + "old goal", None, 0, None, None, None, cfg=None, + resume_note=note) + self.assertTrue(result["ok"]) + self.assertEqual(result["rounds_done"], 1) + self.assertEqual(len(prompts), 1) + first = prompts[0] + self.assertTrue(first.startswith(note)) + self.assertLess(first.index(note), first.index("目标已更新")) + self.assertIn("new goal", first) + self.assertIn("作废", first) + if __name__ == "__main__": unittest.main() diff --git a/host.py b/host.py index ea74be2..b72a391 100644 --- a/host.py +++ b/host.py @@ -2915,21 +2915,25 @@ def run_goal_loop(server, state, state_path, task, goal_text, verify, f"目标:{goal_text}\n" f"验收命令:{verify_text}\n" f"上一轮验收输出:\n{last_verify}") + # FLAG-3 定序(v2 演练决定):首轮两者同 pending 时, + # 模型可见顺序为断点备注在前、改 goal 通知在后、再是 + # 本轮任务提示。注意两次 prepend 的执行顺序与可见顺序 + # 相反:先贴通知、再贴备注,最终才是备注在前。v1 注释 + # 声称“先见备注”但代码实际产生通知在前,v2 在此修正, + # 并由 test_goal_live_update 的共 pending 测试钉住。 + # 影响面仅首轮共 pending 时;之后轮次备注块是 no-op, + # 通知块行为不变。 + if pending_goal_notice is not None: + # 本轮提示显式写出旧 goal → 新 goal;通知只用一次, + # 下一轮提示是重新构造的,不重复贴。 + prompt = pending_goal_notice + "\n\n" + prompt + pending_goal_notice = None # 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 - # 演练注 FLAG:与上同锚点——运行中改 goal 的旧→新通知 - # 与 N-12 断点备注同属 prompt 前缀;两段独立 flag, - # 并集保留(备注在前、改 goal 通知在后);若首轮两者 - # 同 pending,模型先见备注后见改 goal 通知,顺序待定。 - if pending_goal_notice is not None: - # 本轮提示显式写出旧 goal → 新 goal;通知只用一次, - # 下一轮提示是重新构造的,不重复贴。 - prompt = pending_goal_notice + "\n\n" + prompt - pending_goal_notice = None try: result = run_task_turn(server, state, task, prompt, model, effort, sandbox, timeout=timeout,