From 47247ad6408e088ef36f7695700a4bccbca90b2a Mon Sep 17 00:00:00 2001 From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> Date: Fri, 16 Oct 2026 15:08:25 +0800 Subject: [PATCH 01/22] fix(benchmark): budget complete controller capture preparation Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> --- benchmark/runtime/checkpoint.py | 4 +- benchmark/runtime/checkpoint_handoff.py | 5 ++ benchmark/runtime/checkpoint_wake.py | 4 +- benchmark/tests/test_checkpoint_handoff.py | 77 ++++++++++++++++++++++ 4 files changed, 88 insertions(+), 2 deletions(-) diff --git a/benchmark/runtime/checkpoint.py b/benchmark/runtime/checkpoint.py index 69213ca46b..bbb66de210 100644 --- a/benchmark/runtime/checkpoint.py +++ b/benchmark/runtime/checkpoint.py @@ -20,6 +20,7 @@ from loopx.hosts.research_checkpoint import ( _participant_id, native_binding_participant, research_participants, verify_research_participants, ) +from .checkpoint_handoff import CHECKPOINT_CAPTURE_SECONDS def normalize_checkpoint_request_path(value: str) -> str: @@ -246,7 +247,8 @@ def capture_after_wake(env: dict[str, str], receipt: dict[str, Any], *, started = time.monotonic() try: phase_deadline = env.get("LOOPX_PHASE_DEADLINE_EPOCH") - deadline = time.time() + 30 + window = CHECKPOINT_CAPTURE_SECONDS if supplied_request and supplied_request[0].get("controller_handoff") else 30 + deadline = time.time() + window if phase_deadline is not None: original_deadline = float(phase_deadline) if not math.isfinite(original_deadline): diff --git a/benchmark/runtime/checkpoint_handoff.py b/benchmark/runtime/checkpoint_handoff.py index 1f9458e751..19a77dc6a0 100644 --- a/benchmark/runtime/checkpoint_handoff.py +++ b/benchmark/runtime/checkpoint_handoff.py @@ -13,6 +13,11 @@ import uuid +# One shared budget for explicitly enabled full controller capture, including complete +# native/research preparation and the controller's live-source export. Mature +# histories can exhaust 30 seconds before the writer barrier even starts. The +# original phase still caps this clock; handoff/retries cannot renew it. +CHECKPOINT_CAPTURE_SECONDS = 90 CONTROLLER_SEAL_SECONDS = 30 diff --git a/benchmark/runtime/checkpoint_wake.py b/benchmark/runtime/checkpoint_wake.py index 2a38ae8975..b83e9c54dd 100644 --- a/benchmark/runtime/checkpoint_wake.py +++ b/benchmark/runtime/checkpoint_wake.py @@ -18,6 +18,7 @@ from loopx.control_plane.effect_runtime import effect_runtime_result from .checkpoint import _mark_replan_attempt, _participant_id, held_quota_decision, read_checkpoint_request +from .checkpoint_handoff import CHECKPOINT_CAPTURE_SECONDS class CheckpointWake: @@ -103,7 +104,8 @@ def _capture(self, held, deadline): "decision_sha256": held["sha256"], "capture_order": "after_native_interruption", "writer_barrier": "owned_native_process_group_only"} - window = min(time.time() + 30, time.time() + deadline - time.monotonic()) + capture_seconds = CHECKPOINT_CAPTURE_SECONDS if self.request.get("controller_handoff") else 30 + window = min(time.time() + capture_seconds, time.time() + deadline - time.monotonic()) if window <= time.time(): raise TimeoutError("original wake budget expired before capture") components = list(self.request["components"]) diff --git a/benchmark/tests/test_checkpoint_handoff.py b/benchmark/tests/test_checkpoint_handoff.py index 380036236d..b8214a5a75 100644 --- a/benchmark/tests/test_checkpoint_handoff.py +++ b/benchmark/tests/test_checkpoint_handoff.py @@ -102,6 +102,83 @@ def test_controller_completion_requires_finite_clocks(capture, phase): controller_completion_deadline(capture, phase) +@pytest.mark.parametrize("hook", ["settled", "interrupted"]) +@pytest.mark.parametrize("phase_seconds", [None, 40, 20]) +def test_capture_preparation_uses_one_window_without_renewing_phase(tmp_path, monkeypatch, hook, phase_seconds): + """A mature witness may take >30s; that grants no new phase or retry clock.""" + from contextlib import contextmanager + from types import SimpleNamespace + from benchmark.runtime import checkpoint, checkpoint_wake + + clock = [100.0] + timer = SimpleNamespace(time=lambda: clock[0], monotonic=lambda: clock[0]) + monkeypatch.setattr(checkpoint, "time", timer) + monkeypatch.setattr(checkpoint_wake, "time", timer) + code = tmp_path / "code" + code.write_text("retained source") + env = {"LOOPX_CHECKPOINT_REQUEST": "explicit declaration", "LOOPX_WAKE_LOG_DIR": str(tmp_path / "wakes"), + "LOOPX_CLI": "loopx", "LOOPX_PROJECT": str(tmp_path), + "LOOPX_RUNTIME_ROOT": str(tmp_path / "runtime"), "LOOPX_REGISTRY": str(tmp_path / "registry"), + "LOOPX_GOAL_ID": "source", "LOOPX_AGENT_ID": "agent"} + if phase_seconds is not None: + env["LOOPX_PHASE_DEADLINE_EPOCH"] = str(100 + phase_seconds) + request = {"destination": str(tmp_path / "checkpoints" / "point"), + "components": [{"id": "code", "source_path": str(code)}], "point_metadata": {}, + "controller_handoff": True, "loopx_state": True, "native_session": True, "evaluation_opportunity": True} + receipt = {"ok": True, "stage": "execute", "mode": "heartbeat", "turn_id": "turn", + "session": {"session_id": "native-session"}} + binding = SimpleNamespace(session_id="native-session", goal_ref="source", digest="profile") + monkeypatch.setattr(checkpoint, "research_participants", lambda env, components, *a: (components, {})) + monkeypatch.setattr(checkpoint, "verify_research_participants", lambda *a: None) + monkeypatch.setattr(checkpoint, "native_binding_participant", lambda env, components, *a: (components, binding)) + monkeypatch.setattr("benchmark.runtime.sessions.BenchmarkSessionWake", lambda *a, **k: binding) + monkeypatch.setattr("benchmark.runtime.checkpoint_continuation.restored_capture_history", lambda *a, **k: {}) + @contextmanager + def reader(*a, **k): + yield object() + monkeypatch.setattr("loopx.hosts.codex_checkpoint.native_checkpoint_reader", reader) + monkeypatch.setattr("loopx.hosts.codex_checkpoint.native_session_checkpoint", + lambda *a, **k: {"components": [], "metadata": {"store_members": []}}) + deadlines = [] + def artifact(method, params, **kwargs): + deadline = params["deadline_epoch_ms"] / 1000 + deadlines.append(deadline) + if params["action"] == "probe": + clock[0] += 35 # Complete retained witness, rather than trimming history. + if clock[0] >= deadline: + raise TimeoutError("original capture window expired") + return {"source_sha256": "witness", "source_witness": {}, "sha256": "sealed-original"} + def controller(root, components, metadata, probe, deadline, **kwargs): + deadlines.append(deadline) + if clock[0] >= deadline: + raise TimeoutError("original capture window expired") + return {"sha256": "sealed-original"} + monkeypatch.setattr("benchmark.runtime.checkpoint_handoff.await_controller_capture", controller) + monkeypatch.setattr(checkpoint, "effect_runtime_result", artifact) + monkeypatch.setattr(checkpoint_wake, "effect_runtime_result", artifact) + if hook == "settled": + observed = SimpleNamespace(progress={"state": "settled"}, + identity=SimpleNamespace(value=SimpleNamespace(effect_id="original-effect"))) + monkeypatch.setattr(checkpoint, "read_heartbeat_settlement", lambda *a, **k: observed) + checkpoint.capture_after_wake(env, receipt, supplied_request=(request, b"declared")) + assert receipt["checkpoint"]["captured"] is (phase_seconds != 20) + assert receipt["ok"] is True # An optional miss cannot replay successful work. + else: + wake = checkpoint_wake.CheckpointWake(env, receipt) + wake.request, wake.raw = request, b"declared" + held = {"intent": {"obligation_id": "replan-fixture"}, "sha256": "original-guard", + "path": code, "quota": {"heartbeat_receipt": {"turn_instance_id": "turn"}}} + phase = float("inf") if phase_seconds is None else 100 + phase_seconds + if phase_seconds == 20: + with pytest.raises(TimeoutError, match="original capture window"): + wake._capture(held, phase) + else: + assert wake._capture(held, phase)["sha256"] == "sealed-original" + expected = 190 if phase_seconds is None else 100 + phase_seconds + assert deadlines and all(deadline == expected for deadline in deadlines) + assert code.read_text() == "retained source" + + @pytest.mark.skipif(sys.platform != "linux", reason="Qualifies the Linux writer process group") @pytest.mark.parametrize("use_selector,closeout,settlement_only", [ (False, False, False), (True, False, False), (True, True, False), (True, False, True), From 072178167e8e3389986502922495f2cc9a32e919 Mon Sep 17 00:00:00 2001 From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> Date: Fri, 16 Oct 2026 15:08:39 +0800 Subject: [PATCH 02/22] docs(benchmark): explain bounded full checkpoint capture window Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> --- benchmark/README.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/benchmark/README.md b/benchmark/README.md index 6b725ef4d3..3ed4d4e2d7 100644 --- a/benchmark/README.md +++ b/benchmark/README.md @@ -674,6 +674,16 @@ the source continues from the original packet and deadline. Optional capture failure records a missed point, never a later replacement or a new model/eval budget. A barrier that cannot be released remains an explicit controller recovery problem. +The explicitly enabled full controller capture hooks now share a 90-second window for complete +native/research preparation, stability checks and live-source capture (previously +30 seconds). Both hooks cap it by the original trial phase; the controller keeps +that same absolute deadline. After releasing the source, it may spend up to 30 +more seconds sealing its immutable exports, also within the original phase. This +allowance changes neither the trial budget nor cooldown, completeness or retry +rules. A missed point remains missed; direct controller commands retain their +caller-supplied timeout, and other capture declarations keep their 30-second +window. Capture remains off unless explicitly enabled. + A real native CLI/worker plus isolated Docker/native-HTTP qualification covers the mid-wake path, complete canonical context, conserved evaluator opportunity and original source continuation. It uses a deterministic local model and synthetic @@ -897,6 +907,12 @@ worker 保留原 guard,controller 暂停全部容器写入者,核对仍有 文件 witness,再封存原生评测机会、incumbent 及剩余预算。源变化、过期或失去 交接会拒绝;可恢复的捕获失败不取消原研究,也不得改用稍后的状态。解除暂停后, 源会话继续使用原 packet 和 deadline。 +显式开启的两处完整 controller 捕获 hook 共用 90 秒窗口(原为 30 秒),覆盖完整原生/研究上下文 +准备、一致性复核与 live-source 捕获,并受原试验 phase 截止时间约束。controller +沿用同一个绝对截止时间;解除暂停后,只能在原 phase 内再用至多 30 秒封存静态副本。 +这不增加试验预算、冷却或机会,不改变完整性及首次尝试规则;丢失的点仍不能补记, +直接 controller 命令仍受调用者给定的 timeout 约束,其他捕获声明保持 30 秒窗口。 +未显式开启时仍不捕获。 自动 supervisor 的准备截止时间仍约束暂停、文件导出及原生捕获。成功解除暂停后, controller 只对自己持有的静态导出副本继续校验、封存,使用已捕获的原 Host phase 截止时间;该截止时间同时传给类型化产物 owner,过期不能发布。源可以继续工作, From da01f996c820aad3bae85268963e64c6bbb19d7c Mon Sep 17 00:00:00 2001 From: song Date: Fri, 16 Oct 2026 15:49:23 +0800 Subject: [PATCH 03/22] fix(planning): preserve initial quality in shared delivery guidance Signed-off-by: song --- .../hierarchical-agent-stride-control-v0.md | 10 ++++++- ...rarchical-agent-stride-control-v0.zh-CN.md | 7 ++++- .../protocols/loopx-goal-command-v0.md | 11 ++++++++ loopx/control_plane/goals/start_contract.py | 15 ++++++++++- .../control_plane/work_items/stride_policy.ts | 5 ++++ tests/control_plane/test_task_planning.py | 26 +++++++++++++++++++ tests/control_plane_ts/stride_policy.test.ts | 9 +++++++ 7 files changed, 80 insertions(+), 3 deletions(-) diff --git a/docs/architecture/rfcs/hierarchical-agent-stride-control-v0.md b/docs/architecture/rfcs/hierarchical-agent-stride-control-v0.md index 87a97e688a..5b6cf09285 100644 --- a/docs/architecture/rfcs/hierarchical-agent-stride-control-v0.md +++ b/docs/architecture/rfcs/hierarchical-agent-stride-control-v0.md @@ -687,7 +687,15 @@ repeating unchanged checks is not a default policy. The next probe declares minimum evidence for a coherent accepted deliverable or interpretable experiment, its prediction, adoption criterion and full cost. The enabled review now supplies `decision_value_review` questions for the three -existing axes. Identify the remaining authoritative goal gap, its limiting +existing axes. It separately considers first-deliverable quality and later gain +beyond the retained best: recovery from an avoidably weak initial artifact does +not establish additional capability. Compare attributable artifact versions, +input conditions and original budgets, retaining submission and feedback-arrival +times separately. Check whether later work implemented the proposed mechanism; +a decision record, saved snapshot or new identifier alone does not establish +adoption, restoration or improved results. These are refinements of the existing +Agent questions, not new decision fields, score thresholds or settlement gates; +the strategy remains default-off. Identify the remaining authoritative goal gap, its limiting component and plausible headroom before choosing the next mechanism. Compare continuation with one feasible different approach that could affect that component, including complete cost. A changed auxiliary mechanism can be useful, diff --git a/docs/architecture/rfcs/hierarchical-agent-stride-control-v0.zh-CN.md b/docs/architecture/rfcs/hierarchical-agent-stride-control-v0.zh-CN.md index 557d82e728..b6c95e6ceb 100644 --- a/docs/architecture/rfcs/hierarchical-agent-stride-control-v0.zh-CN.md +++ b/docs/architecture/rfcs/hierarchical-agent-stride-control-v0.zh-CN.md @@ -582,7 +582,12 @@ switch(尝试不同机制)或 verify(解决决定扩大或切换的具体 反复检查作为默认接续。下一实验声明形成可解释结论所需 的最低证据、预测、采用条件和完整成本。 -启用后的复盘为三个既有轴提供 `decision_value_review` 问题:先识别仍未满足的 +启用后的复盘为三个既有轴提供 `decision_value_review` 问题。首个交付的质量和 +超过保留最佳产物之后的新增收益分别评价:补回本可避免的低起点,不证明新增能力。 +比较须绑定产物版本、输入条件和原预算,区分提交时刻与反馈实际到达时刻,并检查 +后续工作是否执行了提议的机制变化。决定记录、快照或新标识本身不证明采用、恢复 +或结果改善。这些是既有Agent问题的细化,不增加决策字段、分数阈值或结算门槛; +策略仍默认关闭。先识别仍未满足的 权威目标、当前瓶颈分量和可能的收益空间,再选择下一机制。比较原路线接续与 一种能影响该瓶颈的可行替代路线,并计入完整成本。辅助机制变化可能有用,但 新颖本身不证明能解决剩余缺口。局部或合成数据的提升须有适用的迁移证据;重复 diff --git a/docs/reference/protocols/loopx-goal-command-v0.md b/docs/reference/protocols/loopx-goal-command-v0.md index 0663a506f3..d7803827ec 100644 --- a/docs/reference/protocols/loopx-goal-command-v0.md +++ b/docs/reference/protocols/loopx-goal-command-v0.md @@ -154,6 +154,17 @@ but the shape depends on how clear the goal already is: concise todos to make the approach explicit, without arbitrary item-count caps or management-only filler. +The shared planner now includes `first_delivery_guidance`, also rendered in the +guided `/loopx` start prompt and the existing `todo plan` packet. This changes +default Agent guidance in both ordinary and fine-grained planning: preserve a +strong feasible first deliverable and its coherent implementation/validation, +rather than deliberately weakening it to produce an earlier feedback signal. +Judge initial quality alongside latency and full cost; diagnostic probes remain +legal under the original acceptance, feedback, budget and authority limits, and +preparation must not grow indefinitely. The guidance adds no quality oracle, +mandatory external evaluation, submission allowance or machine admission gate. +Model adoption and resulting quality require separate real-task evidence. + Each new item includes: - `priority`: `P0`, `P1`, or `P2`; diff --git a/loopx/control_plane/goals/start_contract.py b/loopx/control_plane/goals/start_contract.py index 1a395a6323..c7b6564933 100644 --- a/loopx/control_plane/goals/start_contract.py +++ b/loopx/control_plane/goals/start_contract.py @@ -4,6 +4,18 @@ GOAL_START_SCHEMA_VERSION = "loopx_goal_start_command_v0" +# Shared Agent guidance, not a quality classifier or an admission gate. +_FIRST_DELIVERY_GUIDANCE = ( + "Use the strongest feasible approach supported by current knowledge for the " + "first deliverable, including its necessary implementation and validation as " + "a coherent slice. Do not choose a deliberately weak placeholder merely to " + "obtain earlier feedback or declare progress. Compare first-result quality, " + "latency and full cost; neither immediate feedback nor indefinite preparation " + "is the objective. Useful diagnostic probes remain allowed under the existing " + "acceptance, feedback and authority limits; identify their purpose and do not " + "substitute them for the requested deliverable." +) + def goal_planner_contract(*, fine_grained: bool = False) -> dict[str, Any]: planner = { @@ -39,6 +51,7 @@ def goal_planner_contract(*, fine_grained: bool = False) -> dict[str, Any]: "required_fields": ["priority", "text", "task_class", "action_kind"], "public_safe_only": True, "budget_policy": "minimum sufficient plan; no fixed-count filler", + "first_delivery_guidance": _FIRST_DELIVERY_GUIDANCE, } if fine_grained: planner["fine_grained_plan_horizon"] = ( @@ -208,7 +221,7 @@ def build_goal_start_prompt( Rules: 1. Identity: use verified binding/active contract; a stable unbound host gets a fresh public-safe agent. Existing lanes require explicit takeover plus bind/readback before Todo; never infer peers from Todo/worktree/arguments. Unknown host: `loopx agent-onboard --list-agent-types`. 2. Capability: only `selected_capability_route`; run entry/admission and its later `capability show`; never infer from text/URLs. Capability state owns facts; generic Todos schedule. -3. Todos: {todo_rule}. +3. Todos: {todo_rule}. {_FIRST_DELIVERY_GUIDANCE} 4. Writeback: current Todo evidence + next executable Todo, then `loopx refresh-state --goal-id {goal_id}` and quota readback. Chat/model summaries are not durable state. 5. Host loop: after Todo write, activate missing/unknown/stale/type-changed Codex App heartbeat automation, CLI/TraeX `/goal`, Claude `/loop`, OpenCode bridge, Ark one-shot, or custom gate. Else surface the exact pasteable gate; never claim autonomy. 6. Run the returned typed `quota_guard`; finish one bounded segment with validation + LoopX writeback or an exact blocker. Setup/planning/claim is not delivery. diff --git a/loopx/control_plane/work_items/stride_policy.ts b/loopx/control_plane/work_items/stride_policy.ts index 4f546b1135..861de5fd4e 100644 --- a/loopx/control_plane/work_items/stride_policy.ts +++ b/loopx/control_plane/work_items/stride_policy.ts @@ -19,12 +19,17 @@ export function normalizeStrideStrategy(value: unknown): string { // result classifier, route ban or admission authority. const decisionValueReview = { work_slice: "Compare the complete cost and useful outcome of continuation with one feasible alternative. " + + "Assess the quality of the first deliverable and later gain beyond the retained best separately; " + + "recovering an avoidable initial quality deficit is not evidence of additional capability. " + "Name the observation that would change that choice, and size a complete experiment to obtain it. " + "A larger test count is not greater decision value; preserve unfinished decisive work rather than restart its setup.", route_distance: "Name the remaining authoritative goal gap, its limiting component and the plausible headroom. " + + "Keep the retained best artifact as the comparison anchor and check whether subsequent work actually implements the proposed mechanism change. " + "Compare a different approach that can affect that component. Distinguish an unexecuted intervention, failed transfer and insufficient route potential: they imply different next experiments. " + "A changed auxiliary mechanism alone does not show that it addresses the gap.", validation_scope: "Use evidence applicable to the limiting outcome and changed mechanism. " + + "Bind comparisons to the artifact version, input conditions and original budget, and distinguish when feedback became available from when work was submitted. " + + "Keep decision, execution and measured outcome separate; a saved snapshot is not proof of restoration or continued execution. " + "First establish that the intervention actually changes the relevant behavior; unchanged outcomes may require tracing its activation rather than a wider parameter sweep. " + "Choose a comparison or negative control that separates the surviving explanations, and state what each possible result would change. " + "Before a check vetoes further work, identify its source and protected goal: an original constraint or frozen study criterion, versus an agent-proposed candidate heuristic. " + diff --git a/tests/control_plane/test_task_planning.py b/tests/control_plane/test_task_planning.py index 6f5fba6631..b83a1f67ab 100644 --- a/tests/control_plane/test_task_planning.py +++ b/tests/control_plane/test_task_planning.py @@ -8,6 +8,10 @@ import pytest from loopx.cli import main +from loopx.control_plane.goals.start_contract import ( + build_goal_start_prompt, + goal_planner_contract, +) from loopx.control_plane.goals.task_planning import build_task_planning_packet @@ -138,6 +142,28 @@ def test_public_cli_returns_a_read_only_checkpoint_and_rejects_execution(bound_g packet = json.loads(output.getvalue()) assert packet["read_only"] and packet["dry_run"] assert packet["runnable_todo_ids"] == [] + quality = packet["planner"]["first_delivery_guidance"] + # Agent-consumed obligations must reach both public planning entrypoints; + # their presence is not a measurement of the resulting model quality. + for obligation in ( + "strongest feasible approach", + "deliberately weak placeholder", + "quality, latency and full cost", + "existing acceptance, feedback and authority limits", + ): + assert obligation in quality + for fine_grained in (False, True): + assert goal_planner_contract(fine_grained=fine_grained)[ + "first_delivery_guidance" + ] == quality + assert quality in build_goal_start_prompt( + goal_text=bound_goal["text"], + goal_id=bound_goal["goal_id"], + agent_id=bound_goal["agent_id"], + fine_grained=fine_grained, + ) + assert packet["execution_handoff"]["spends_quota"] is False + assert packet["execution_handoff"]["requires_quota_guard"] is True output = io.StringIO() with contextlib.redirect_stdout(output): assert main(command + ["--execute"]) == 1 diff --git a/tests/control_plane_ts/stride_policy.test.ts b/tests/control_plane_ts/stride_policy.test.ts index 720583fb51..a8b2444e99 100644 --- a/tests/control_plane_ts/stride_policy.test.ts +++ b/tests/control_plane_ts/stride_policy.test.ts @@ -58,6 +58,8 @@ test("goal-value questions are enabled review input, not persisted decisions or assert.deepEqual(Object.keys(contract.decision_value_review as object), ["work_slice", "route_distance", "validation_scope"]); const validation = (contract.decision_value_review as Record).validation_scope; + const work = (contract.decision_value_review as Record).work_slice; + const route = (contract.decision_value_review as Record).route_distance; // These clauses are Agent-consumed behavior, not machine classification of // a failed check. Keep both decision value and the authority boundary visible. assert.match(validation, /identify its source and protected goal/); @@ -69,6 +71,13 @@ test("goal-value questions are enabled review input, not persisted decisions or assert.match(validation, /readiness for an authorized informative evaluation/); assert.match(validation, /evidence sufficient to replace the incumbent/); assert.match(validation, /budget, feedback and permission limits/); + assert.match(work, /first deliverable.*later gain/); + assert.match(work, /recovering an avoidable initial quality deficit/); + assert.match(route, /retained best artifact/); + assert.match(route, /subsequent work actually implements/); + assert.match(validation, /artifact version, input conditions/); + assert.match(validation, /feedback became available/); + assert.match(validation, /not proof of restoration or continued execution/); // All existing legal choices remain valid without answering a new field. for (const action of ["grow", "switch", "verify"]) { const decision = normalizeStrideDecision({...proposal, action})[0]!; From a9dfe8c4c7fb792af6ee7f557be0968e0692a624 Mon Sep 17 00:00:00 2001 From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> Date: Fri, 16 Oct 2026 15:57:56 +0800 Subject: [PATCH 04/22] fix(turn): reuse the canonical frozen-request digest matcher Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> --- .../turn_driver/operation_requests.py | 5 +++-- .../test_content_digest_single_owner.py | 1 + tests/control_plane/test_operation_requests.py | 16 +++++++++++++++- 3 files changed, 19 insertions(+), 3 deletions(-) diff --git a/loopx/control_plane/turn_driver/operation_requests.py b/loopx/control_plane/turn_driver/operation_requests.py index 5cac3aea97..de86b3eb08 100644 --- a/loopx/control_plane/turn_driver/operation_requests.py +++ b/loopx/control_plane/turn_driver/operation_requests.py @@ -12,9 +12,10 @@ import json import math from pathlib import Path -import re from typing import Any, Mapping +from loopx.control_plane.content_digest import BARE_SHA256_PATTERN + def request_reference_instruction(configured: bool) -> str: if not configured: @@ -101,7 +102,7 @@ def operation_request_source( if path is None and sha256 is None: return None if (path is None or not path.is_absolute() or not isinstance(sha256, str) - or re.fullmatch(r"[0-9a-f]{64}", sha256) is None): + or BARE_SHA256_PATTERN.fullmatch(sha256) is None): raise ValueError("frozen operation request requires an absolute file and SHA-256") source = OperationRequestSource(path, sha256) source.read() # Fail before SDK startup; no file path/content enters errors. diff --git a/tests/architecture/test_content_digest_single_owner.py b/tests/architecture/test_content_digest_single_owner.py index b4739e4133..039d36b0b7 100644 --- a/tests/architecture/test_content_digest_single_owner.py +++ b/tests/architecture/test_content_digest_single_owner.py @@ -200,6 +200,7 @@ "loopx.control_plane.todos.completion_validation_store", "loopx.control_plane.todos.machine_section_projection", "loopx.control_plane.turn_driver.codex_sessions", + "loopx.control_plane.turn_driver.operation_requests", "loopx.control_plane.work_items.governed_transition_proposal", "loopx.control_plane.work_items.progress_review_policy", "loopx.domain_packs.issue_fix", diff --git a/tests/control_plane/test_operation_requests.py b/tests/control_plane/test_operation_requests.py index 175da2cd98..2da59d276b 100644 --- a/tests/control_plane/test_operation_requests.py +++ b/tests/control_plane/test_operation_requests.py @@ -10,7 +10,9 @@ from examples import operation_action_fixtures as fixtures from loopx.control_plane.turn_driver.codex_operation_host import operation_tool_handler from loopx.control_plane.turn_driver.codex_sessions import _store_codex_cli_session -from loopx.control_plane.turn_driver.operation_requests import operation_request_source +from loopx.control_plane.turn_driver.operation_requests import ( + OperationRequestSource, operation_request_source, +) from tests.control_plane.test_dsh_operation_connection import ( DshOperationConnection, PROFILE, call, envelope, event, exchange, opened, ) @@ -174,6 +176,18 @@ def test_operator_file_requires_strict_bounded_json(tmp_path, raw): operation_request_source(path, hashlib.sha256(raw).hexdigest()) +@pytest.mark.parametrize("digest", [ + "", "a" * 63, "a" * 65, "A" * 64, "g" * 64, + "sha256:" + "a" * 64, "a" * 64 + "\n", "\n" + "a" * 64, + "a" * 64 + " ", " " + "a" * 64, b"a" * 64, 64, +]) +def test_invalid_digest_shape_is_refused_before_frozen_file_io(tmp_path, monkeypatch, digest): + monkeypatch.setattr(OperationRequestSource, "read", + lambda *_: pytest.fail("invalid digest must not read the frozen file")) + with pytest.raises(ValueError, match="requires an absolute file and SHA-256"): + operation_request_source(tmp_path / "request.json", digest) + + @pytest.mark.parametrize("path,digest", [(None, "a" * 64), ("relative.json", "a" * 64), ("/absent.json", None), ("/absent.json", "bad")]) def test_source_configuration_cannot_be_partial_or_model_selected(path, digest): From 34f1c99618b6f78de7c3fbf67fb33f40f9bc1238 Mon Sep 17 00:00:00 2001 From: song Date: Fri, 16 Oct 2026 16:08:30 +0800 Subject: [PATCH 05/22] fix(benchmark): keep a refused checkpoint handoff from killing the source wake A fresh_replan handoff whose packet is refused (unparseable, outside this wake's capture root, or rejected by the typed selector) raised out of the wake loop and ended the source session; the next resume then reused the ended wake's socket and capture root. The refusal is now recorded as a bounded, unreleased handoff event and the uninterrupted original wake keeps running. A refusal raised after the decision holder was entered, or after the source exited, still re-raises. Wake receipts also keep a bounded error message beside error_kind, so a failed wake is diagnosable without an out-of-band incident file. Co-Authored-By: Claude Fable 5.1 Signed-off-by: song --- benchmark/runtime/checkpoint_wake.py | 52 +++++++++++------ benchmark/runtime/worker.py | 9 ++- benchmark/tests/test_checkpoint_handoff.py | 68 ++++++++++++++++++++++ 3 files changed, 108 insertions(+), 21 deletions(-) diff --git a/benchmark/runtime/checkpoint_wake.py b/benchmark/runtime/checkpoint_wake.py index 2a38ae8975..98a6f7524e 100644 --- a/benchmark/runtime/checkpoint_wake.py +++ b/benchmark/runtime/checkpoint_wake.py @@ -200,25 +200,39 @@ def run(self, execution, body, wake, session_wake, stdout, stderr, timeout): raise subprocess.TimeoutExpired(command, timeout) if not select.select([self.listener], [], [], min(.1, remaining))[0]: continue - with held_quota_decision(self.listener, self.capture_root, - goal_id=self.env["LOOPX_GOAL_ID"], agent_id=self.env["LOOPX_AGENT_ID"], - turn_instance_id=None, native_process=process, - interrupt_timeout=min(10, remaining), interrupt_when=self._first_replan, - deadline=deadline) as held: - if "native_process_return_code" not in held: - continue - stdout.flush() - session_wake.observe(wake / "stdout.jsonl") - event = {"obligation_id": held["intent"]["obligation_id"], - "decision_path": str(held["path"]), "decision_sha256": held["sha256"], - "captured": False, "research_point_qualified": False} - self.receipt.setdefault("checkpoint_handoffs", []).append(event) - try: - event.update(self._capture(held, deadline), captured=True) - except Exception as error: - # A missed optional point cannot discard the original - # committed guard or turn a later retry into its capture. - event.update(error_kind=type(error).__name__, error=str(error)[:500]) + entered = False + try: + with held_quota_decision(self.listener, self.capture_root, + goal_id=self.env["LOOPX_GOAL_ID"], agent_id=self.env["LOOPX_AGENT_ID"], + turn_instance_id=None, native_process=process, + interrupt_timeout=min(10, remaining), interrupt_when=self._first_replan, + deadline=deadline) as held: + entered = True + if "native_process_return_code" not in held: + continue + stdout.flush() + session_wake.observe(wake / "stdout.jsonl") + event = {"obligation_id": held["intent"]["obligation_id"], + "decision_path": str(held["path"]), "decision_sha256": held["sha256"], + "captured": False, "research_point_qualified": False} + self.receipt.setdefault("checkpoint_handoffs", []).append(event) + try: + event.update(self._capture(held, deadline), captured=True) + except Exception as error: + # A missed optional point cannot discard the original + # committed guard or turn a later retry into its capture. + event.update(error_kind=type(error).__name__, error=str(error)[:500]) + except Exception as error: + # A refused handoff (unparseable, outside this wake's capture + # root, or rejected by the typed selector) is neither released + # nor captured; its caller already reports a typed transport + # failure. The uninterrupted original wake keeps running. + if entered or process.poll() is not None: + raise + self.receipt.setdefault("checkpoint_handoffs", []).append({ + "captured": False, "research_point_qualified": False, "released": False, + "error_kind": type(error).__name__, "error": str(error)[:500]}) + continue continuation = held.get("continuation_body") if continuation is not None: break diff --git a/benchmark/runtime/worker.py b/benchmark/runtime/worker.py index ea703f02b2..dfccdbd187 100644 --- a/benchmark/runtime/worker.py +++ b/benchmark/runtime/worker.py @@ -327,6 +327,11 @@ class _InertCheckpointWake: listener = None +def _error_tail(exc: BaseException) -> dict[str, str]: + """Private wake evidence: the class plus a bounded message, never a traceback.""" + return {"error_kind": type(exc).__name__, "error": " ".join(str(exc).split())[:500]} + + def run_once(env: dict[str, str]) -> dict: if env.get("LOOPX_TURN_ENVELOPE", "0") not in {"0", "1"}: raise ValueError("LOOPX_TURN_ENVELOPE must be 0 or 1") @@ -505,7 +510,7 @@ def run_once(env: dict[str, str]) -> dict: receipt["timed_out"] = True except BaseException as exc: receipt["ok"] = False - receipt["error_kind"] = type(exc).__name__ + receipt.update(_error_tail(exc)) raise finally: checkpoint_context.close() @@ -513,7 +518,7 @@ def run_once(env: dict[str, str]) -> dict: if session_wake is not None: session_wake.observe(wake / "stdout.jsonl") except BaseException as exc: - receipt.update(ok=False, error_kind=type(exc).__name__) + receipt.update(ok=False, **_error_tail(exc)) raise finally: if env.get("LOOPX_CHECKPOINT_REQUEST"): diff --git a/benchmark/tests/test_checkpoint_handoff.py b/benchmark/tests/test_checkpoint_handoff.py index 380036236d..4133281d5a 100644 --- a/benchmark/tests/test_checkpoint_handoff.py +++ b/benchmark/tests/test_checkpoint_handoff.py @@ -405,3 +405,71 @@ def gate(*args, full_history): assert checkpoint.capture_before_replan(env, receipt, "current work").endswith("current work") assert observed == [not native_participant] assert receipt["checkpoint"]["reason"] == "no_fresh_replan" + + +@pytest.mark.skipif(os.name != "posix", reason="Requires the Host Unix rendezvous") +def test_refused_handoff_keeps_original_wake_running(tmp_path, monkeypatch): + """A resumed session reusing an earlier wake's capture root is refused, not fatal. + + The refused packet is neither released nor captured; the same source keeps + using the private endpoint and exits on its own. The receipt keeps a bounded + reason instead of only an exception class. + """ + import socket + import tempfile + from pathlib import Path + from types import SimpleNamespace + + import benchmark.runtime.worker as worker + from benchmark.runtime.checkpoint_wake import CheckpointWake + + wakes = tmp_path / "wakes" + current = wakes / "wake-current" / "decisions" + earlier = wakes / "wake-earlier" / "decisions" / "decision-earlier" + current.mkdir(parents=True, mode=0o700) + earlier.mkdir(parents=True) + released = current / "decision-current" + released.mkdir() + packets = {} + for name, directory in (("earlier", earlier), ("current", released)): + raw = json.dumps({"ok": False, "goal_id": "source", "error_code": "fixture"}).encode() + (directory / "decision.json").write_bytes(raw) + packets[name] = {"path": str(directory / "decision.json"), "sha256": hashlib.sha256(raw).hexdigest()} + project = tmp_path / "project" + project.mkdir() + env = {**os.environ, "LOOPX_WAKE_LOG_DIR": str(wakes), "LOOPX_GOAL_ID": "source", + "LOOPX_AGENT_ID": "agent", "LOOPX_PROJECT": str(project)} + with tempfile.TemporaryDirectory(prefix="refused-", dir="/tmp") as short: + address = str(Path(short).resolve() / "host.sock") + code = ( + "import json,socket,sys; from pathlib import Path\n" + "def call(message):\n" + " with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as s:\n" + f" s.connect({address!r}); s.sendall(json.dumps(message).encode()+b'\\n')\n" + " return s.makefile('rb').readline()\n" + f"assert call({packets['earlier']!r}) == b''\n" + f"assert json.loads(call({packets['current']!r}))['released'] is True\n" + "Path('continued').write_text('source continued')\n") + monkeypatch.setattr(worker, "native_command", lambda *args: [sys.executable, "-c", code]) + wake = CheckpointWake(env, {"turn_id": "wake-current", "mode": "heartbeat", "stage": "execute"}) + wake.capture_root = current.resolve() + with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as listener: + listener.bind(address) + listener.listen(1) + wake.listener = listener + with (tmp_path / "stdout").open("w") as stdout, (tmp_path / "stderr").open("w") as stderr: + assert wake.run(SimpleNamespace(), "body", tmp_path, None, stdout, stderr, 30) == 0 + assert (project / "continued").read_text() == "source continued" + [event] = wake.receipt["checkpoint_handoffs"] + assert event["captured"] is False and event["released"] is False + assert event["error_kind"] == "ValueError" and "capture root" in event["error"] + assert (earlier / "decision.json").read_bytes() # Earlier evidence is untouched. + + +def test_wake_receipt_error_tail_is_bounded_message(): + from benchmark.runtime.worker import _error_tail + + tail = _error_tail(ValueError("decision handoff must reference\nthis Host's capture root " + "x" * 900)) + assert tail["error_kind"] == "ValueError" + assert tail["error"].startswith("decision handoff must reference this Host's capture root") + assert len(tail["error"]) == 500 and "\n" not in tail["error"] From 311126b5b9c036fc2708cd53c4b212fc383763fe Mon Sep 17 00:00:00 2001 From: song Date: Fri, 16 Oct 2026 17:02:56 +0800 Subject: [PATCH 06/22] test(planning): qualify bounded first-delivery guidance growth The shared first-delivery guidance adds fixed decision text to four agent-facing CLI surfaces. The frozen 96-row differential fixture measures +2,340 JSON characters, +3,496 for explicit command-pack detail and +578 Markdown; the guidance is one bounded block with no per-unit growth and no line change. Declare that one-time allowance for the four surfaces and teach the authoring-input observation to recognize the exact guidance, so the budget keeps characterizing cost instead of hiding the clause. Co-Authored-By: Claude Fable 5.1 Signed-off-by: song --- docs/development/testing-and-quality.md | 25 +++++++++++++ .../testing/cli_output_differential.py | 14 ++++++++ .../testing/cli_output_semantics.py | 19 +++++++++- .../test_cli_output_differential.py | 36 +++++++++++++++++++ 4 files changed, 93 insertions(+), 1 deletion(-) diff --git a/docs/development/testing-and-quality.md b/docs/development/testing-and-quality.md index ed8d24fd27..884526476d 100644 --- a/docs/development/testing-and-quality.md +++ b/docs/development/testing-and-quality.md @@ -1182,6 +1182,31 @@ it does not grant execution quota, spending, or provider authority. or promotion thresholds stay fixed for that result; revised thresholds belong to a new qualification, never a relabeled historical pass. +The initial-delivery guidance change measured the same 96-row fixture at +`f6cf9583794eb8c4cca41fa832a1c2f150059583` and +`da01f996c820aad3bae85268963e64c6bbb19d7c`. The complete instruction adds +2,340 JSON characters to guided start/bootstrap, 3,496 to explicit command-pack +detail, and 578 to bootstrap Markdown, with no added lines. Existing planner, +plan-prompt, guided-transaction and message consumers retain their contracts; +removing a copy requires a caller migration. The instruction preserves strong +feasible first quality, coherent implementation/validation, full-cost comparison, +bounded diagnostic probes and existing acceptance/feedback/authority limits. +Truncating those obligations to fit the old differential is not lossless. + +Only the observed absence-to-complete-guidance transition on those carriers gets +2,400 / 3,600 / 600 character-and-byte allowances (60 / 104 / 22 headroom). +The probe recognizes the complete frozen structured field or rendered Todo +instruction, never a runtime permission or quality classification. Absolute +ceilings, semantic fields, fixture scale, line limits and every other surface +remain unchanged; an already-upgraded, missing or incomplete instruction gets no +transition allowance. This qualifies the disclosed default guidance change, +not a real model's adoption, benchmark improvement or additional execution quota. + +首次交付指引在同一 96 行负载上增加默认 JSON 2340、详情 JSON 3496、Markdown +578 个字符,行数不变。保留现有消费者与完整质量、诊断和权限义务,仅对上述输出 +载体从缺失到完整指引的首次变化给予 2400/3600/600 的有界额度;绝对上限、语义、 +规模及后续增长检查不变。原失败保留;不据此声称模型采用、实验增益或额外执行权限。 + The latest qualification pins true main `e47e4507a` and merged candidate `3955d1c8b` on the same public fixture, with explicit per-probe source imports. It retains 1/36/18 Todos, 1/12/12 history records, complete command routes, diff --git a/loopx/control_plane/testing/cli_output_differential.py b/loopx/control_plane/testing/cli_output_differential.py index c5b53539f9..aac1d1d6a4 100644 --- a/loopx/control_plane/testing/cli_output_differential.py +++ b/loopx/control_plane/testing/cli_output_differential.py @@ -754,6 +754,20 @@ def _authoring_input_migration( {"chars": 256, "utf8_bytes": 256, "lines": 0, "compact_payload_chars": 256} ) reason = "Vision CLI authoring input added" + elif (surface in {"start_goal_guided", "start_goal_guided_command_pack_detail", + "bootstrap_command_pack", "bootstrap_command_pack_message_only"} + and before.get("first_delivery_guidance") is False + and after.get("first_delivery_guidance") is True): + # Frozen 96-row fixture: +2,340 JSON chars, +3,496 for explicit + # command-pack detail, +578 Markdown. Keep all existing consumers and + # the full guidance; no additional lines or absolute budget increase. + chars = ( + (3_600 if surface == "start_goal_guided_command_pack_detail" else 2_400) + if base.get("format") == "json" else 600 + ) + limits = {"chars": chars, "utf8_bytes": chars, "lines": 0, + "compact_payload_chars": chars if base.get("format") == "json" else 0} + reason = "complete initial-delivery quality guidance added" return limits, [reason + "; bounded one-time growth"] if limits else [] diff --git a/loopx/control_plane/testing/cli_output_semantics.py b/loopx/control_plane/testing/cli_output_semantics.py index db9209990f..ea728826dc 100644 --- a/loopx/control_plane/testing/cli_output_semantics.py +++ b/loopx/control_plane/testing/cli_output_semantics.py @@ -26,10 +26,23 @@ def authoring_input_observations(text: str) -> dict[str, bool]: "checkpoint-context is only for recovery after the original committed Turn writeback; " "follow its returned same-Turn recovery action, not a fresh-turn preflight." ) - observed = {"complete_todo_readback": False, "vision_cli_authoring": False} + first_delivery_guidance = ( + 'Use the strongest feasible approach supported by current knowledge for the first ' + 'deliverable, including its necessary implementation and validation as a coherent ' + 'slice. Do not choose a deliberately weak placeholder merely to obtain earlier ' + 'feedback or declare progress. Compare first-result quality, latency and full cost; ' + 'neither immediate feedback nor indefinite preparation is the objective. Useful ' + 'diagnostic probes remain allowed under the existing acceptance, feedback and ' + 'authority limits; identify their purpose and do not substitute them for the requested' + ' deliverable.' + ) + observed = {"complete_todo_readback": False, "vision_cli_authoring": False, + "first_delivery_guidance": False} def collect(value: Any) -> None: if isinstance(value, dict): + if value.get("first_delivery_guidance") == first_delivery_guidance: + observed["first_delivery_guidance"] = True if (value.get("id") == "read_back_authored_todos" and value.get("kind") == "operator_or_agent_actions" and value.get("purpose") == readback_purpose @@ -47,6 +60,10 @@ def collect(value: Any) -> None: try: collect(json.loads(text)) except json.JSONDecodeError: + observed["first_delivery_guidance"] = bool(re.search( + r"^3\. Todos: [^\n]*" + re.escape(first_delivery_guidance) + r"$", + text, re.MULTILINE, + )) # Existential across every rendered candidate: an earlier line whose purpose is # not exact must not mask a later complete instruction. rendered_candidates = re.finditer( diff --git a/tests/control_plane/test_cli_output_differential.py b/tests/control_plane/test_cli_output_differential.py index 31524113c7..68e55f3d8f 100644 --- a/tests/control_plane/test_cli_output_differential.py +++ b/tests/control_plane/test_cli_output_differential.py @@ -66,6 +66,11 @@ def _receipt(*rows: dict[str, object]) -> dict[str, object]: ("start_goal_guided_command_pack_detail", "markdown", "complete_todo_readback", (436, 2, 0)), ("quota_should_run", "json", "vision_cli_authoring", (3049, 94, 1807)), ("loopx_turn_plan", "json", "vision_cli_authoring", (237, 0, 237)), + ("start_goal_guided", "json", "first_delivery_guidance", (2340, 0, 2340)), + ("bootstrap_command_pack", "json", "first_delivery_guidance", (2340, 0, 2340)), + ("bootstrap_command_pack", "markdown", "first_delivery_guidance", (578, 0, 0)), + ("start_goal_guided_command_pack_detail", "json", "first_delivery_guidance", (3496, 0, 3496)), + ("bootstrap_command_pack_message_only", "markdown", "first_delivery_guidance", (578, 0, 0)), ]) def test_authoring_growth_is_bounded_one_time_and_surface_scoped( surface, output_format, feature, growth, @@ -1416,3 +1421,34 @@ def test_settlement_v5_migration_does_not_waive_output_growth(previous): assert not compare_cli_output_receipts( _receipt(candidate), _receipt({**candidate, "action_signature_sha256": "lost-identity"}), )["ok"] + + +def test_first_delivery_growth_requires_every_reviewed_instruction(): + from loopx.control_plane.testing.cli_output_semantics import authoring_input_observations + + guidance = ( + 'Use the strongest feasible approach supported by current knowledge for the first ' + 'deliverable, including its necessary implementation and validation as a coherent ' + 'slice. Do not choose a deliberately weak placeholder merely to obtain earlier ' + 'feedback or declare progress. Compare first-result quality, latency and full cost; ' + 'neither immediate feedback nor indefinite preparation is the objective. Useful ' + 'diagnostic probes remain allowed under the existing acceptance, feedback and ' + 'authority limits; identify their purpose and do not substitute them for the requested' + ' deliverable.' + ) + def structured(value): + return json.dumps({"planner": {"first_delivery_guidance": value}}) + + def rendered(value): + return "3. Todos: existing planning obligations. " + value + + for render in [structured, rendered]: + assert authoring_input_observations(render(guidance))["first_delivery_guidance"] + for clause in ["strongest feasible approach", "current knowledge", "necessary implementation and validation", + "coherent slice", "deliberately weak placeholder", "earlier feedback or declare progress", + "first-result quality", "latency and full cost", "indefinite preparation", + "Useful diagnostic probes", "acceptance, feedback and authority limits", + "identify their purpose", "do not substitute them for the requested deliverable"]: + assert not authoring_input_observations(render(guidance.replace(clause, "")))["first_delivery_guidance"] + assert not authoring_input_observations(json.dumps({"note": guidance}))["first_delivery_guidance"] + assert not authoring_input_observations("note: " + guidance)["first_delivery_guidance"] From fae4371d7dabb038610f290259cdd39a35fb0783 Mon Sep 17 00:00:00 2001 From: song Date: Fri, 16 Oct 2026 17:18:17 +0800 Subject: [PATCH 07/22] fix(research): bind stride outcomes to complete source evidence Return canonical planned work with the previous decision and replay cited outcome evidence against complete scoped history at writeback. Display limits and optional typed-progress attachments no longer exclude valid work evidence; descriptive legacy Todo deltas remain readable. Preserve unknown and negative outcomes, historical decision identities and off-mode isolation. Keep initial-delivery guidance in the shared planner for structured packets and expand it for standalone messages. Qualify the bounded standalone growth while removing repeated full guidance. Cover late/ordinary evidence, unavailable sources, legacy deltas and the real File/SQLite continuation path. Signed-off-by: song --- loopx/bootstrap_command_pack.py | 11 +- .../goals/goal_frontier/__init__.py | 1 + .../goals/goal_vision_read_model.py | 4 + loopx/control_plane/goals/start_contract.py | 17 ++- .../testing/cli_output_differential.py | 11 +- .../testing/cli_output_semantics.py | 6 +- .../work_items/progress_observation.py | 18 ++- .../work_items/replan_context.ts | 110 +++++++++++++++++- .../work_items/replan_context_codec.py | 34 ++++++ .../work_items/replan_semantics.ts | 2 +- .../control_plane/work_items/stride_policy.ts | 42 ++++++- .../test_effective_turn_replan.py | 40 ++++++- .../test_start_goal_compact_projection.py | 25 ++++ tests/control_plane/test_task_planning.py | 4 + tests/control_plane_ts/replan_context.test.ts | 92 +++++++++++++++ tests/control_plane_ts/stride_policy.test.ts | 35 +++++- 16 files changed, 426 insertions(+), 26 deletions(-) diff --git a/loopx/bootstrap_command_pack.py b/loopx/bootstrap_command_pack.py index 8784824861..fa585fc77c 100644 --- a/loopx/bootstrap_command_pack.py +++ b/loopx/bootstrap_command_pack.py @@ -892,6 +892,7 @@ def build_loopx_bootstrap_command_pack( goal_id=resolved_goal_id, agent_id=str(selected_agent_id) if selected_agent_id else None, fine_grained=fine_grained, + inline_first_delivery_guidance=False, ) slash_command_catalog = build_slash_command_catalog(cli_bin=cli_bin) @@ -2039,6 +2040,14 @@ def render_loopx_bootstrap_command_pack_message(payload: dict[str, Any]) -> str: Rerun `{payload.get("canonical_cli_command")} --agent-id ` with the selected identity before continuing.""" elif goal_text: + planner = goal_start_contract.get("planner") or {} + quality = planner.get("first_delivery_guidance") + # Structured packs carry this once in the shared planner. Message-only + # consumers need its complete text here, without another packet read. + quality_block = ( + f"goal_start_contract.planner.first_delivery_guidance:\n{quality}\n\n" + if isinstance(quality, str) and quality else "" + ) action = f"""This is an explicit goal-start invocation. Connect project-local LoopX state if needed: ```bash @@ -2048,7 +2057,7 @@ def render_loopx_bootstrap_command_pack_message(payload: dict[str, Any]) -> str: Then plan before writing todos. Preserve relative priority by write order: ````text -{commands.get("goal_start_plan_prompt", "")} +{quality_block}{commands.get("goal_start_plan_prompt", "")} ```` Write the planned todos with `loopx todo add` in the exact planned order. Same-priority items use that write order as the tie-breaker. diff --git a/loopx/control_plane/goals/goal_frontier/__init__.py b/loopx/control_plane/goals/goal_frontier/__init__.py index 27e49c5374..7d35f24a1b 100644 --- a/loopx/control_plane/goals/goal_frontier/__init__.py +++ b/loopx/control_plane/goals/goal_frontier/__init__.py @@ -1763,6 +1763,7 @@ def build_goal_frontier_projection_context_from_status( newest_first_runs=(), source_status=status_payload, stride_strategy=strategy, goal_acceptance_contract=(agent_todo_summary or {}).get("goal_acceptance_contract"), + todo_items=agent_todo_source_items, ) goal_frontier_projection = build_goal_frontier_projection_from_summaries( diff --git a/loopx/control_plane/goals/goal_vision_read_model.py b/loopx/control_plane/goals/goal_vision_read_model.py index 87e7db4dba..8281242c52 100644 --- a/loopx/control_plane/goals/goal_vision_read_model.py +++ b/loopx/control_plane/goals/goal_vision_read_model.py @@ -31,6 +31,10 @@ def parse_vision_todo_delta_entries(entries: Any) -> list[tuple[str, str]]: if not isinstance(value, str): continue action, separator, raw_todo_id = value.strip().partition(":") + # Identity text cannot be shortened into a different existing Todo. + # Historical descriptive deltas remain readable but do not create links. + if len(raw_todo_id.strip()) > VISION_TODO_DELTA_ID_LIMIT: + continue todo_id = ( _compact_projection_text(raw_todo_id, limit=VISION_TODO_DELTA_ID_LIMIT) or "" diff --git a/loopx/control_plane/goals/start_contract.py b/loopx/control_plane/goals/start_contract.py index c7b6564933..b35b31cd82 100644 --- a/loopx/control_plane/goals/start_contract.py +++ b/loopx/control_plane/goals/start_contract.py @@ -13,7 +13,15 @@ "latency and full cost; neither immediate feedback nor indefinite preparation " "is the objective. Useful diagnostic probes remain allowed under the existing " "acceptance, feedback and authority limits; identify their purpose and do not " - "substitute them for the requested deliverable." + "substitute them for the requested deliverable. Before an expensive implementation, " + "check the declared interface, units, scale and other decision-critical assumptions " + "against the available task material. Compare a feasible alternative with the " + "strongest available starting point; preserve that reference while testing a replacement. " + "Use a cheap discriminating check when it can resolve the choice, then budget enough " + "work for the selected complete experiment. An informative negative experiment can " + "complete its own Todo without satisfying the broader goal. Keep evaluation readiness " + "separate from evidence for replacing the best artifact; neither creates a new " + "acceptance document or permission requirement." ) @@ -182,6 +190,7 @@ def build_goal_start_prompt( goal_id: str, agent_id: str | None, fine_grained: bool, + inline_first_delivery_guidance: bool = True, ) -> str: goal_clause = ( f"Goal text: {goal_text}" @@ -213,6 +222,10 @@ def build_goal_start_prompt( if fine_grained else "" ) + quality_rule = ( + _FIRST_DELIVERY_GUIDANCE if inline_first_delivery_guidance else + "Apply `goal_start_contract.planner.first_delivery_guidance` from the returned packet." + ) return f"""Plan; returned `ordered_steps` + `goal_start_contract` are authoritative. {goal_clause} @@ -221,7 +234,7 @@ def build_goal_start_prompt( Rules: 1. Identity: use verified binding/active contract; a stable unbound host gets a fresh public-safe agent. Existing lanes require explicit takeover plus bind/readback before Todo; never infer peers from Todo/worktree/arguments. Unknown host: `loopx agent-onboard --list-agent-types`. 2. Capability: only `selected_capability_route`; run entry/admission and its later `capability show`; never infer from text/URLs. Capability state owns facts; generic Todos schedule. -3. Todos: {todo_rule}. {_FIRST_DELIVERY_GUIDANCE} +3. Todos: {todo_rule}. {quality_rule} 4. Writeback: current Todo evidence + next executable Todo, then `loopx refresh-state --goal-id {goal_id}` and quota readback. Chat/model summaries are not durable state. 5. Host loop: after Todo write, activate missing/unknown/stale/type-changed Codex App heartbeat automation, CLI/TraeX `/goal`, Claude `/loop`, OpenCode bridge, Ark one-shot, or custom gate. Else surface the exact pasteable gate; never claim autonomy. 6. Run the returned typed `quota_guard`; finish one bounded segment with validation + LoopX writeback or an exact blocker. Setup/planning/claim is not delivery. diff --git a/loopx/control_plane/testing/cli_output_differential.py b/loopx/control_plane/testing/cli_output_differential.py index aac1d1d6a4..3d5c59a7da 100644 --- a/loopx/control_plane/testing/cli_output_differential.py +++ b/loopx/control_plane/testing/cli_output_differential.py @@ -758,14 +758,15 @@ def _authoring_input_migration( "bootstrap_command_pack", "bootstrap_command_pack_message_only"} and before.get("first_delivery_guidance") is False and after.get("first_delivery_guidance") is True): - # Frozen 96-row fixture: +2,340 JSON chars, +3,496 for explicit - # command-pack detail, +578 Markdown. Keep all existing consumers and - # the full guidance; no additional lines or absolute budget increase. + # Structured prompts now reference the shared planner (one full copy, + # two with the standalone message in detail). The standalone message + # retains the expanded 1,238-byte guidance plus its explicit reference + # label/spacing. Keep growth scoped to the first introduction only. chars = ( (3_600 if surface == "start_goal_guided_command_pack_detail" else 2_400) - if base.get("format") == "json" else 600 + if base.get("format") == "json" else 1_400 ) - limits = {"chars": chars, "utf8_bytes": chars, "lines": 0, + limits = {"chars": chars, "utf8_bytes": chars, "lines": 4, "compact_payload_chars": chars if base.get("format") == "json" else 0} reason = "complete initial-delivery quality guidance added" return limits, [reason + "; bounded one-time growth"] if limits else [] diff --git a/loopx/control_plane/testing/cli_output_semantics.py b/loopx/control_plane/testing/cli_output_semantics.py index ea728826dc..315ebf21fb 100644 --- a/loopx/control_plane/testing/cli_output_semantics.py +++ b/loopx/control_plane/testing/cli_output_semantics.py @@ -10,6 +10,7 @@ def authoring_input_observations(text: str) -> dict[str, bool]: + from ..goals.start_contract import _FIRST_DELIVERY_GUIDANCE """Recognize complete reviewed guidance for one-time output qualification. These observations never classify runtime authority. Match the structured @@ -41,7 +42,7 @@ def authoring_input_observations(text: str) -> dict[str, bool]: def collect(value: Any) -> None: if isinstance(value, dict): - if value.get("first_delivery_guidance") == first_delivery_guidance: + if value.get("first_delivery_guidance") in (first_delivery_guidance, _FIRST_DELIVERY_GUIDANCE): observed["first_delivery_guidance"] = True if (value.get("id") == "read_back_authored_todos" and value.get("kind") == "operator_or_agent_actions" @@ -63,6 +64,9 @@ def collect(value: Any) -> None: observed["first_delivery_guidance"] = bool(re.search( r"^3\. Todos: [^\n]*" + re.escape(first_delivery_guidance) + r"$", text, re.MULTILINE, + )) or bool(re.search( + r"^goal_start_contract\.planner\.first_delivery_guidance:\n" + + re.escape(_FIRST_DELIVERY_GUIDANCE) + r"$", text, re.MULTILINE, )) # Existential across every rendered candidate: an earlier line whose purpose is # not exact must not mask a later complete instruction. diff --git a/loopx/control_plane/work_items/progress_observation.py b/loopx/control_plane/work_items/progress_observation.py index 9a5ea58582..a6239af891 100644 --- a/loopx/control_plane/work_items/progress_observation.py +++ b/loopx/control_plane/work_items/progress_observation.py @@ -430,6 +430,9 @@ def semantic_delta_from_writeback( vision path, can contribute outcomes. """ + # The same source serves novelty and cited-outcome replay; consume an + # iterable once without reopening or modifying its authority store. + runs = list(history_runs) if history_runs is not None else None baseline = obligation.get("progress_baseline") if not isinstance(baseline, Mapping): baseline = next( @@ -462,14 +465,14 @@ def semantic_delta_from_writeback( claimed = list(window) if isinstance(window, list) else [] if isinstance(coverage, list): claimed.extend(coverage) - if history_runs is not None and context is not None: + if runs is not None and context is not None: from .replan_context_codec import replan_evidence_rows # The readable context has a display budget; admission does not forget # an older claim merely because it was omitted from that display. claimed.extend( row["progress_observation"] - for row in replan_evidence_rows(history_runs, goal_id=context["goal_id"], agent_id=context["agent_id"]) + for row in replan_evidence_rows(runs, goal_id=context["goal_id"], agent_id=context["agent_id"]) if row["progress_observation"] is not None ) elif context is not None and context.get("coverage_truncated"): @@ -482,10 +485,19 @@ def semantic_delta_from_writeback( vision = dict(agent_vision) if isinstance(agent_vision, Mapping) else {} if vision: vision["state"] = normalize_goal_vision_state(vision.get("state")) + source_review = None + stride = vision.get("stride_decision") + review = stride.get("previous_review") if isinstance(stride, Mapping) else None + if (runs is not None and context is not None and context.get("stride_strategy") != "off" + and context.get("decision_followthrough") and isinstance(review, Mapping)): + from .replan_context_codec import replay_stride_review_evidence + + source_review = replay_stride_review_evidence(context=context, review=review, runs=runs) try: result = project_replan_semantics({ "operation": "qualify", "obligation": dict(obligation), "observation_delta": observation_delta, "agent_vision": vision, + **({"stride_review_evidence": source_review} if source_review is not None else {}), }) except EffectRuntimeRejected as exc: raise ValueError(str(exc)) from None @@ -504,6 +516,7 @@ def build_replan_context( stride_strategy: str = "off", explore_context: Mapping[str, Any] | None = None, goal_acceptance_contract: Mapping[str, Any] | None = None, + todo_items: Iterable[Mapping[str, Any]] | None = None, ) -> dict[str, Any]: from .replan_context_codec import project_replan_context @@ -512,6 +525,7 @@ def build_replan_context( runs=newest_first_runs, obligation=obligation, source_status=source_status, goal_acceptance_contract=goal_acceptance_contract, stride_strategy=stride_strategy, history_newest_first=True, explore_context=explore_context, + todo_items=todo_items, ) diff --git a/loopx/control_plane/work_items/replan_context.ts b/loopx/control_plane/work_items/replan_context.ts index 17c61ceefe..a9caaebec1 100644 --- a/loopx/control_plane/work_items/replan_context.ts +++ b/loopx/control_plane/work_items/replan_context.ts @@ -170,9 +170,9 @@ function recordedDecision(row: JsonObject): JsonObject | null { * Append order belongs to the captured history, not timestamps or prose. A * progress observation proves neither execution nor scientific improvement. */ -function decisionFollowthrough(rows: JsonObject[], goalFacts: unknown, fromFullIndex: boolean): JsonObject | null { +function orderedDecisionRows(rows: JsonObject[], goalFacts: unknown): JsonObject[] { const currentRef = objectOrNull(objectOrNull(goalFacts)?.goal_ref); - const scoped = rows.filter(row => { + return rows.filter(row => { const source = objectOrNull(row._source_facts); const ref = objectOrNull(source?.goal_ref); return source && Number.isSafeInteger(source.history_position) && @@ -181,6 +181,53 @@ function decisionFollowthrough(rows: JsonObject[], goalFacts: unknown, fromFullI ref?.goal_instance_id === currentRef.goal_instance_id)); }).sort((a, b) => Number(objectOrNull(b._source_facts)!.history_position) - Number(objectOrNull(a._source_facts)!.history_position)); +} + +function plannedTodoIds(source: JsonObject): string[] { + // todo_delta predates typed work links and permits descriptive text. A + // legacy description remains unresolved; it must not poison later reads. + const declared = Array.isArray(source.planned_todo_ids) ? source.planned_todo_ids : []; + return [...new Set(declared.filter((id): id is string => typeof id === "string" && ID.test(id)))]; +} + +function observationTodo(row: JsonObject): unknown { + const work = objectOrNull(row.progress_observation)?.work_item_id; + return work == null ? row.todo_id : row.todo_id == null || row.todo_id === work ? work : null; +} + +/** Resolve only the cited refs against full scoped source facts at writeback. + * Display limits never decide evidence eligibility. Ordinary work rows do not + * need an optional typed progress attachment to remain attributable. */ +function reviewEvidence(rows: JsonObject[], request: JsonObject): JsonObject { + const scoped = orderedDecisionRows(rows, request.goal_facts); + const previous = scoped.find(acceptedDecisionSource); + const decision = previous ? recordedDecision(previous) : null; + if (!decision || decision.evidence_ref !== request.previous_decision_ref) { + throw new EffectRuntimeRequestError("previous decision changed; refresh replan context before reviewing its outcome"); + } + const refs = request.evidence_refs; + if (!Array.isArray(refs) || refs.length > 3 || new Set(refs).size !== refs.length) { + throw new EffectRuntimeRequestError("previous review accepts at most three distinct evidence references"); + } + for (const ref of refs) identifier(ref, "review evidence reference"); + const source = objectOrNull(previous!._source_facts)!; + const targets = new Set(plannedTodoIds(source)); + const citations = refs.map(ref => { + const matches = scoped.filter(row => "replan-evidence-" + digest(publicEvidenceRow(row)) === ref + || (objectOrNull(row.progress_observation)?.evidence_ids as unknown[] | undefined)?.includes(ref)); + const subsequent = matches.filter(row => { + const facts = objectOrNull(row._source_facts)!; + return Number(facts.history_position) > Number(source.history_position) && !facts.quota_spend_commit; + }); + return {evidence_ref: ref, available: matches.length > 0, subsequent: subsequent.length > 0, + declared_work: subsequent.some(row => targets.has(String(observationTodo(row))))}; + }); + return {previous_decision_ref: decision.evidence_ref, has_declared_work: targets.size > 0, citations}; +} + +function decisionFollowthrough(rows: JsonObject[], goalFacts: unknown, fromFullIndex: boolean, + todoItems: unknown): JsonObject | null { + const scoped = orderedDecisionRows(rows, goalFacts); const previous = scoped.find(acceptedDecisionSource); if (!previous) return null; // Do not fall back to an older decision when the latest basis is corrupt. @@ -196,21 +243,49 @@ function decisionFollowthrough(rows: JsonObject[], goalFacts: unknown, fromFullI } // The complete denominator is retained even when the review details are capped. const observations = [...subsequent.values()]; + const declared = Array.isArray(source.planned_todo_ids) ? source.planned_todo_ids : []; + const todoIds = plannedTodoIds(source); + const todos = todoItems == null ? [] : objects(todoItems, "Todo source"); + const plannedWork = todoIds.slice(0, 6).map(todoId => { + const matched = todos.filter(todo => todo.todo_id === todoId && todo.role === "agent" + && (todo.goal_id == null || todo.goal_id === previous.goal_id)); + if (matched.length > 1) throw new EffectRuntimeRequestError("planned Todo source identity is ambiguous"); + const current = matched[0]; + return {todo_id: todoId, + current: current ? Object.fromEntries(["status", "claimed_by", "task_class", "action_kind"] + .filter(key => Object.hasOwn(current, key)).map(key => [key, current[key]])) : null, + observation_refs: observations.filter(row => observationTodo(row) === todoId) + .slice(0, 6).map(row => "replan-evidence-" + digest(publicEvidenceRow(row)))}; + }); + // Keep relevant experiment observations in the small visible window even + // when newer unrelated work would otherwise crowd them out. + const linkedRefs = new Set(plannedWork.flatMap(work => work.observation_refs)); + const visible = [...observations.filter(row => linkedRefs.has("replan-evidence-" + digest(publicEvidenceRow(row)))), + ...observations.filter(row => !linkedRefs.has("replan-evidence-" + digest(publicEvidenceRow(row))))].slice(0, 6); + const visibleRefs = new Set(visible.map(row => "replan-evidence-" + digest(publicEvidenceRow(row)))); + for (const work of plannedWork) work.observation_refs = work.observation_refs.filter(ref => visibleRefs.has(ref)); return { previous_decision: decision, source_history_complete: fromFullIndex, unsequenced_progress_count: rows.filter(row => row.progress_observation && !Number.isSafeInteger(objectOrNull(row._source_facts)?.history_position)).length, recorded_progress_count: observations.length, - observations: observations.slice(0, 6).map(row => ({ + observations: visible.map(row => ({ evidence_ref: "replan-evidence-" + digest(publicEvidenceRow(row)), generated_at: row.generated_at, todo_id: row.todo_id, coverage_ref: (row.progress_observation as JsonObject).fingerprint, })), omitted_observation_count: Math.max(0, observations.length - 6), + todo_source_available: todoItems != null, + planned_work: plannedWork, + omitted_planned_work_count: Math.max(0, todoIds.length - plannedWork.length), + unresolved_planned_work_count: declared.filter(id => typeof id !== "string" || !ID.test(id)).length, execution_and_goal_gain: "unknown", guidance: "Compare the previous path decision with subsequent work and authoritative outcomes. " + "Recorded progress and changed identifiers do not prove execution, a new mechanism, or goal gain. " + + "Use planned_work to compare declared Todo targets with their current state and subsequent observations. " + + "A missing row or observation is unknown, not proof of non-execution; a done Todo does not certify objective gain. " + + "Cited evidence is replayed against full scoped history at writeback; this display is not an eligibility boundary. " + "Resolve evidence before deciding what to retain, change, or test next.", }; } @@ -261,6 +336,27 @@ export function validateReplanContext(value: unknown): JsonObject { } normalizeGoalPathDelta(previous.path_delta); normalizeRecordedStrideDecision(previous.stride_decision); + if (follow.planned_work !== undefined) { + const work = objects(follow.planned_work, "planned work"); + const ids = work.map(row => identifier(row.todo_id, "planned Todo identity")); + const refs = new Set(observations.map(row => row.evidence_ref)); + if (work.length > 6 || new Set(ids).size !== ids.length || typeof follow.todo_source_available !== "boolean" + || !Number.isSafeInteger(follow.omitted_planned_work_count) || Number(follow.omitted_planned_work_count) < 0) { + throw new EffectRuntimeRequestError("invalid planned work followthrough"); + } + for (const row of work) { + if (!Array.isArray(row.observation_refs) || row.observation_refs.some(ref => !refs.has(ref))) { + throw new EffectRuntimeRequestError("planned work requires delivered subsequent observations"); + } + if (row.current !== null) { + const current = requireJsonObject(row.current, "planned work current state"); + if (!follow.todo_source_available || Object.entries(current).some(([key, value]) => + !["status", "claimed_by", "task_class", "action_kind"].includes(key) || value !== null && typeof value !== "string")) { + throw new EffectRuntimeRequestError("planned work current state requires available canonical facts"); + } + } + } + } } return context; } @@ -272,7 +368,7 @@ export async function projectReplanContextSnapshot(value: unknown): Promise Number(b.observed_at) - Number(a.observed_at) || JSON.stringify(publicEvidenceRow(a)).localeCompare(JSON.stringify(publicEvidenceRow(b)))); const unique = new Map(); @@ -392,7 +492,7 @@ export function projectReplanContext(value: unknown): JsonObject { const strideStrategy = normalizeStrideStrategy(request.stride_strategy); const exploreBasis = strideStrategy !== "off" ? projectStrideExploreBasis(request.explore_context, goal, agent) : null; const followthrough = agent && strideStrategy !== "off" - ? decisionFollowthrough(rows, request.goal_facts, request.from_full_index === true) : null; + ? decisionFollowthrough(rows, request.goal_facts, request.from_full_index === true, request.todo_items) : null; const contextId = "replan-context-" + digest({goal, agent, obligationId, core, ledger, shown, frontier, ...(strideStrategy !== "off" ? {stride_strategy: strideStrategy} : {}), ...(followthrough ? {decision_followthrough: followthrough} : {}), diff --git a/loopx/control_plane/work_items/replan_context_codec.py b/loopx/control_plane/work_items/replan_context_codec.py index 38839fb3e2..8c9a798423 100644 --- a/loopx/control_plane/work_items/replan_context_codec.py +++ b/loopx/control_plane/work_items/replan_context_codec.py @@ -86,10 +86,18 @@ def replan_evidence_rows(runs: Iterable[Mapping[str, Any]], *, goal_id: str, age # and orders subsequent observations. Do not classify research # progress or infer adoption from prose in this transport codec. if include_decisions: + from ..goals.goal_vision_read_model import parse_vision_todo_delta_entries + if history_newest_first is not None: source["history_position"] = newest_position - position if newest_position is not None else position source["goal_ref"] = run.get("goal_ref") source["agent_vision"] = run.get("agent_vision") + vision = run.get("agent_vision") + source["planned_todo_ids"] = list(dict.fromkeys( + todo_id for _, todo_id in parse_vision_todo_delta_entries( + vision.get("todo_delta") if isinstance(vision, Mapping) else None + ) + )) source["autonomous_replan_ack"] = run.get("autonomous_replan_ack") source["replan_obligation_id"] = run.get("replan_obligation_id") for key, fields in (("settlement_identity", ( @@ -148,6 +156,7 @@ def project_replan_context( stride_strategy: str = "off", explore_context: Mapping[str, Any] | None = None, history_newest_first: bool | None = None, + todo_items: Iterable[Mapping[str, Any]] | None = None, ) -> dict[str, Any]: from .replan_history_codec import project_replan_request from .stride_policy_codec import normalize_stride_strategy @@ -170,6 +179,11 @@ def project_replan_context( include_decisions=strategy != "off" or bool(evidence_ref and evidence_ref.startswith("replan-decision-")), history_newest_first=history_newest_first), "stride_strategy": strategy, + # Canonical caller-owned facts only. TypeScript joins the declared + # work with the accepted decision and causally later observations. + **({"todo_items": [{key: item[key] for key in ( + "todo_id", "goal_id", "role", "status", "claimed_by", "task_class", "action_kind", + ) if key in item} for item in todo_items]} if strategy != "off" and todo_items is not None else {}), **({"explore_context": explore_context} if explore_context is not None else {}), "evidence_ref": evidence_ref, "read_prefix": shlex.join(prefix), @@ -187,3 +201,23 @@ def validate_replan_context(context: Any) -> dict[str, Any]: }) except EffectRuntimeRejected as exc: raise ValueError(str(exc)) from None + + +def replay_stride_review_evidence( + *, context: Mapping[str, Any], review: Mapping[str, Any], runs: Iterable[Mapping[str, Any]], +) -> dict[str, Any]: + """Transport complete existing history; return only the cited source facts.""" + from .replan_history_codec import project_replan_request + + previous = context["decision_followthrough"]["previous_decision"] + try: + return project_replan_request({ + "operation": "review_evidence", "obligation": None, "goal_id": context["goal_id"], + "agent_id": context["agent_id"], "previous_decision_ref": previous["evidence_ref"], + "evidence_refs": review.get("evidence_refs", []), + "goal_facts": {"goal_ref": previous.get("goal_ref")}, + "rows": replan_evidence_rows(runs, goal_id=context["goal_id"], agent_id=context["agent_id"], + include_decisions=True, history_newest_first=True), + }, method="work_item.replan_context") + except EffectRuntimeRejected as exc: + raise ValueError(str(exc)) from None diff --git a/loopx/control_plane/work_items/replan_semantics.ts b/loopx/control_plane/work_items/replan_semantics.ts index 26ff4c4d67..b457d9cc5c 100644 --- a/loopx/control_plane/work_items/replan_semantics.ts +++ b/loopx/control_plane/work_items/replan_semantics.ts @@ -395,7 +395,7 @@ export function projectReplanSemantics(value: unknown): JsonObject { const reviewMismatch = strideDecision && structuredReview ? periodicStrideReview && strideDecision.reference_context_id !== context.context_id ? "Stride decision must reference the delivered due-review context." - : strideReviewMismatch(strideDecision, context) + : strideReviewMismatch(strideDecision, context, request.stride_review_evidence) : null; let outcomes = strings(observation.delta_kinds); if (outcomes.some(value => (CAPABILITY_OUTCOMES as readonly string[]).includes(value))) { diff --git a/loopx/control_plane/work_items/stride_policy.ts b/loopx/control_plane/work_items/stride_policy.ts index 861de5fd4e..7d5a424a66 100644 --- a/loopx/control_plane/work_items/stride_policy.ts +++ b/loopx/control_plane/work_items/stride_policy.ts @@ -54,6 +54,10 @@ export function stridePlanningGuidance(value: unknown): string[] { "Do not force a pivot from elapsed time, a progress count or one noisy result. " + "Missing or refuted evidence is not a universal route ban. Preserve the best qualified artifact and original priorities. " + "Declare minimum evidence, a testable prediction, adoption criterion and full marginal cost including control and validation; review actual adoption, quality and cost next. Verify must resolve a named grow-or-switch question, not repeat an unchanged check. " + + "Carry the next_probe into existing executable Todo work, reusing an unfinished experiment when it still answers the question. " + + "Use the existing agent_vision.todo_delta action:todo_id entries to identify that work after its normal authoring/readback; do not invent IDs or add a planning-only Todo. " + + "Read planned_work and subsequent observations before crediting the previous decision. A negative experiment may finish its Todo while the broader goal remains open. " + + "Keep a candidate experiment separate from the best retained artifact: authorized evaluation may be informative before replacement is justified. " + "Use the live stride_control contract for agent_vision.stride_decision. This controls proposal completeness, not execution, " + "spawn, acceptance or a promise of utility; existing Todo/path and authority owners still govern work."]; } @@ -254,7 +258,7 @@ export function projectStrideExploreBasis(value: unknown, goal: string, agent: s } /** Context binding checks shape and declared provenance, never truth of Agent claims. */ -export function strideReviewMismatch(decision: JsonObject, context: JsonObject): string | null { +export function strideReviewMismatch(decision: JsonObject, context: JsonObject, sourceReview?: unknown): string | null { const follow = context.decision_followthrough as JsonObject | undefined; const previous = follow?.previous_decision as JsonObject | undefined; const review = decision.previous_review as JsonObject | undefined; @@ -265,7 +269,40 @@ export function strideReviewMismatch(decision: JsonObject, context: JsonObject): const allowed = new Set([...rows(context.evidence), ...rows(follow?.observations)] .map(row => row.evidence_ref)); for (const row of rows(context.coverage_ledger)) for (const ref of Array.isArray(row.evidence_ids) ? row.evidence_ids : []) allowed.add(ref); - if ((review.evidence_refs as string[]).some(ref => !allowed.has(ref))) return "Previous-decision claims cite an undelivered evidence reference."; + const cited = review.evidence_refs as string[]; + const subsequent = new Set(rows(follow?.observations).map(row => row.evidence_ref)); + const planned = rows(follow?.planned_work); + const linked = new Set(planned.flatMap(work => Array.isArray(work.observation_refs) ? work.observation_refs : [])); + let hasDeclaredWork = planned.length > 0; + if (sourceReview !== undefined) { + const source = requireJsonObject(sourceReview, "stride review evidence"); + if (source.previous_decision_ref !== previous.evidence_ref || typeof source.has_declared_work !== "boolean" + || !Array.isArray(source.citations) || source.citations.length !== cited.length) { + return "Previous-decision evidence must be replayed against the current scoped source."; + } + hasDeclaredWork = source.has_declared_work; + allowed.clear(); subsequent.clear(); linked.clear(); + for (const [index, value] of source.citations.entries()) { + const row = requireJsonObject(value, "stride review citation"); + if (row.evidence_ref !== cited[index] || typeof row.available !== "boolean" + || typeof row.subsequent !== "boolean" || typeof row.declared_work !== "boolean" + || row.declared_work && !row.subsequent || row.subsequent && !row.available) { + return "Previous-decision evidence replay is inconsistent."; + } + if (row.available) allowed.add(row.evidence_ref); + if (row.subsequent) subsequent.add(row.evidence_ref); + if (row.declared_work) linked.add(row.evidence_ref); + } + } + if (cited.some(ref => !allowed.has(ref))) return "Previous-decision claims cite an undelivered or unavailable evidence reference."; + const claimsOutcome = [review.adoption, review.information_gain, review.goal_gain].some(value => value !== "unknown"); + if (claimsOutcome && !cited.some(ref => subsequent.has(ref))) { + return "Previous-decision claims require subsequent evidence; earlier comparisons alone cannot establish execution or outcome."; + } + if (hasDeclaredWork && (review.adoption === "executed" || review.adoption === "partially_executed" || review.goal_gain !== "unknown") + && !cited.some(ref => linked.has(ref))) { + return "Execution and goal-gain claims require subsequent evidence for the declared work; unrelated activity is insufficient."; + } } const basis = context.explore_basis as JsonObject | undefined; const explore = decision.explore_review as JsonObject | undefined; @@ -315,6 +352,7 @@ export function strideControlContract(context: JsonObject, obligationId: unknown decision_value_review: {...decisionValueReview}, review_text_limit: decisionSchema.review_text_limit, previous_decision_ref: previous, + ...(follow ? {decision_followthrough: follow} : {}), previous_review_options: {adoption: decisionSchema.adoption_options, information_gain: decisionSchema.information_options, goal_gain: decisionSchema.gain_options}, explore_basis: context.explore_basis ?? null, explore_dispositions: decisionSchema.explore_dispositions, minimal_example: example, diff --git a/tests/control_plane/test_effective_turn_replan.py b/tests/control_plane/test_effective_turn_replan.py index 2bd174c0ae..4054843d2e 100644 --- a/tests/control_plane/test_effective_turn_replan.py +++ b/tests/control_plane/test_effective_turn_replan.py @@ -419,6 +419,7 @@ def call(*args: str, expected_code: int = 0) -> dict: "changed": ["Validate the independent source before claiming completion"], "evidence_refs": ["evidence:independent-source-review"]}, "stride_decision": decision, + "todo_delta": [f"create:{successor['todo_id']}", "create:compare two methods"], } decision_path.write_text(json.dumps({**vision_packet, "stride_decision": {**decision, "reference_context_id": "replan-context-" + "b" * 24}})) @@ -450,15 +451,23 @@ def call(*args: str, expected_code: int = 0) -> dict: for index in range(2): turn = f"stride-followthrough-{index}" call("quota", "should-run", "--codex-app", "--goal-id", GOAL_ID, - "--agent-id", AGENT_ID, "--todo-id", TODO_ID, "--turn-instance-id", turn, + "--agent-id", AGENT_ID, "--todo-id", successor["todo_id"], "--turn-instance-id", turn, "--scan-path", str(project)) - call("refresh-state", "--goal-id", GOAL_ID, "--agent-id", AGENT_ID, - "--todo-id", TODO_ID, "--turn-instance-id", turn, + # Synthetic candidate work under the real selected Todo. A worse + # result is still evidence; it must not be rewritten into goal gain. + (project / f"candidate-result-{index}.json").write_text(json.dumps({ + "reference_error": sum(value * value for value in (1, 1)), + "candidate_error": sum(value * value for value in (1, 2)), + })) + work = call("refresh-state", "--goal-id", GOAL_ID, "--agent-id", AGENT_ID, + "--todo-id", successor["todo_id"], "--turn-instance-id", turn, "--classification", "validated_progress", "--delivery-batch-scale", "implementation", "--delivery-outcome", "outcome_progress", "--delivery-boundary", "in_flight_continuation", + *(["--progress-result-class", "unchanged", "--progress-evidence-id", f"fixture:negative-result-{index}"] + if index == 0 else []), "--no-global-sync", "--suppress-external-sinks") call("quota", "spend-slot", "--goal-id", GOAL_ID, "--agent-id", AGENT_ID, - "--todo-id", TODO_ID, "--turn-instance-id", turn, + "--todo-id", successor["todo_id"], "--turn-instance-id", turn, "--slots", "1", "--source", "heartbeat", "--execute") next_guard = call("quota", "should-run", "--codex-app", "--goal-id", GOAL_ID, "--agent-id", AGENT_ID, @@ -480,6 +489,20 @@ def call(*args: str, expected_code: int = 0) -> dict: assert previous["stride_decision"] == decision assert follow["execution_and_goal_gain"] == "unknown" assert next_control["previous_decision_ref"] == previous["evidence_ref"] + assert next_control["decision_followthrough"] == follow + assert follow["todo_source_available"] is True + planned = follow["planned_work"] + assert len(planned) == 1 and planned[0]["todo_id"] == successor["todo_id"] + assert planned[0]["current"]["claimed_by"] == AGENT_ID + assert len(planned[0]["observation_refs"]) == 1 + assert follow["unresolved_planned_work_count"] == 1 + last_work = json.loads(Path(work["json_path"]).read_text()) + assert "progress_observation" not in last_work + ordinary_ref = next(row["evidence_ref"] for row in next_obligation["replan_context"]["evidence"] + if row["generated_at"] == last_work["generated_at"]) + from loopx.control_plane.quota.turn_envelope import build_turn_envelope + envelope = build_turn_envelope(next_guard) + assert envelope["replan_action_packet"]["stride_control"]["decision_followthrough"] == follow resolved = call("history", "--goal-id", GOAL_ID, "--agent-id", AGENT_ID, "--evidence-ref", previous["evidence_ref"]) assert resolved["evidence"]["stride_decision"] == decision @@ -495,9 +518,14 @@ def call(*args: str, expected_code: int = 0) -> dict: assert missing_review.get("appended") is not True assert "previous" in missing_review["error"].lower() next_decision["previous_review"] = {"reference_decision_ref": previous["evidence_ref"], - "adoption": "unknown", "information_gain": "unknown", "goal_gain": "unknown", + "adoption": "executed", "information_gain": "new_evidence", "goal_gain": "regressed", "evidence_refs": [previous["history_evidence_ref"]], - "lesson": "The decision is recorded; subsequent work does not yet prove target gain."} + "lesson": "The completed candidate regressed; retain the reference artifact and scoped negative result."} + decision_path.write_text(json.dumps({**vision_packet, "stride_decision": next_decision})) + old_evidence = call(*refresh_args, expected_code=1) + assert old_evidence.get("appended") is not True + assert "subsequent" in old_evidence["error"].lower() + next_decision["previous_review"]["evidence_refs"] = [ordinary_ref] decision_path.write_text(json.dumps({**vision_packet, "stride_decision": next_decision})) next_accepted = call(*refresh_args) assert next_accepted["autonomous_replan_ack"]["semantic_delta"]["accepted"] is True diff --git a/tests/control_plane/test_start_goal_compact_projection.py b/tests/control_plane/test_start_goal_compact_projection.py index 481c9a24c1..8afadfebed 100644 --- a/tests/control_plane/test_start_goal_compact_projection.py +++ b/tests/control_plane/test_start_goal_compact_projection.py @@ -85,6 +85,31 @@ def _build(project: Path, *, include_detail: bool) -> dict[str, Any]: ) +def test_planning_guidance_is_shared_but_standalone_message_remains_complete(tmp_path: Path) -> None: + project = _write_connected_project(tmp_path) + compact = _build(project, include_detail=False) + detailed = _build(project, include_detail=True) + for payload, copies in ((compact, 1), (detailed, 2)): + pack = payload["command_pack"] + quality = pack["goal_start_contract"]["planner"]["first_delivery_guidance"] + assert json.dumps(payload).count(quality) == copies + assert "goal_start_contract.planner.first_delivery_guidance" in pack["commands"]["goal_start_plan_prompt"] + for step in payload["guided_transaction"]["ordered_steps"]: + if step.get("kind") == "agent_prompt": + assert "goal_start_contract.planner.first_delivery_guidance" in step["prompt"] + pack = detailed["command_pack"] + quality = pack["goal_start_contract"]["planner"]["first_delivery_guidance"] + assert pack["message"].count(quality) == 1 + assert "existing acceptance, feedback and authority limits" in pack["message"] + assert "negative experiment can complete its own Todo" in pack["message"] + output = io.StringIO() + with contextlib.redirect_stdout(output): + code = cli_main(["bootstrap-command-pack", "--project", str(project), "--goal-id", GOAL_ID, + "--agent-id", AGENT_ID, "--host-surface", "codex-app", "--goal-text", GOAL_TEXT, "--message-only"]) + assert code == 0 + assert output.getvalue().count(quality) == 1 + + def _host_shadow_document(payload: dict[str, Any]) -> dict[str, Any]: command_pack = payload["command_pack"] transaction = payload["guided_transaction"] diff --git a/tests/control_plane/test_task_planning.py b/tests/control_plane/test_task_planning.py index b83a1f67ab..ac0938fd51 100644 --- a/tests/control_plane/test_task_planning.py +++ b/tests/control_plane/test_task_planning.py @@ -150,6 +150,10 @@ def test_public_cli_returns_a_read_only_checkpoint_and_rejects_execution(bound_g "deliberately weak placeholder", "quality, latency and full cost", "existing acceptance, feedback and authority limits", + "declared interface, units, scale", + "preserve that reference while testing a replacement", + "negative experiment can complete its own Todo", + "neither creates a new acceptance document", ): assert obligation in quality for fine_grained in (False, True): diff --git a/tests/control_plane_ts/replan_context.test.ts b/tests/control_plane_ts/replan_context.test.ts index 574f6bc37c..ae2369ae9a 100644 --- a/tests/control_plane_ts/replan_context.test.ts +++ b/tests/control_plane_ts/replan_context.test.ts @@ -3,6 +3,7 @@ import test from "node:test"; import {createHash} from "node:crypto"; import {projectReplanContext, validateReplanContext} from "../../loopx/control_plane/work_items/replan_context.ts"; import {settlementIdentityPayload, type JsonObject} from "../../loopx/control_plane/effect_program.ts"; +import {strideReviewMismatch} from "../../loopx/control_plane/work_items/stride_policy.ts"; const progress = {schema_version: "typed_progress_observation_v0", result_class: "unchanged", surface_id: "surface-a", work_item_id: "todo-a", fingerprint: "same-work"}; @@ -130,6 +131,97 @@ function subsequent(position: number): JsonObject { const strideRequest = {...request, stride_strategy: "evidence_conditioned_v1"}; +test("cited outcome evidence replays full history beyond display caps and optional progress records", () => { + const decision = reviewed(1); + decision._source_facts.planned_todo_ids = ["todo-a"]; + for (const typed of [true, false]) { + const actual = {...subsequent(2), health_check: "Executed the comparison with a retained negative result.", + ...(!typed ? {progress_observation: null} : {})}; + const earlier = {...subsequent(0), health_check: "Only the old baseline."}; + const reference = projectReplanContext({...strideRequest, rows: [actual]}).evidence[0].evidence_ref; + const rows = [earlier, decision, actual, ...Array.from({length: 32}, (_, i) => subsequent(i + 3))]; + const context = projectReplanContext({...strideRequest, rows}); + const previous = context.decision_followthrough.previous_decision.evidence_ref; + assert.ok(!context.decision_followthrough.observations.some((row: JsonObject) => row.evidence_ref === reference)); + const review = {previous_review: {reference_decision_ref: previous, adoption: "executed", + information_gain: "new_evidence", goal_gain: "regressed", evidence_refs: [reference], lesson: "Retain the negative outcome."}}; + const replay = projectReplanContext({...strideRequest, operation: "review_evidence", rows, + previous_decision_ref: previous, evidence_refs: [reference]}); + assert.deepEqual(replay.citations, [{evidence_ref: reference, available: true, subsequent: true, declared_work: true}]); + assert.equal(strideReviewMismatch(review, context, replay), null); + assert.equal(projectReplanContext({...request, operation: "resolve", rows, evidence_ref: reference}).evidence.health_check, + actual.health_check); + + const old = projectReplanContext({...strideRequest, rows: [earlier]}).evidence[0].evidence_ref; + const oldReplay = projectReplanContext({...strideRequest, operation: "review_evidence", rows, + previous_decision_ref: previous, evidence_refs: [old]}); + assert.match(String(strideReviewMismatch({previous_review: {...review.previous_review, evidence_refs: [old]}}, context, oldReplay)), /subsequent/); + const missing = projectReplanContext({...strideRequest, operation: "review_evidence", rows, + previous_decision_ref: previous, evidence_refs: ["evidence:missing"]}); + assert.match(String(strideReviewMismatch({previous_review: {...review.previous_review, evidence_refs: ["evidence:missing"]}}, context, missing)), /unavailable/); + assert.throws(() => projectReplanContext({...strideRequest, operation: "review_evidence", rows: [], + previous_decision_ref: previous, evidence_refs: [reference]}), /previous decision changed/); + } +}); + +test("legacy descriptive Todo deltas stay readable and unresolved", () => { + const decision = reviewed(); + decision._source_facts.planned_todo_ids = ["compare two methods", "todo-a"]; + const context = projectReplanContext({...strideRequest, rows: [decision, subsequent(1)]}); + assert.equal(context.decision_followthrough.unresolved_planned_work_count, 1); + assert.deepEqual(context.decision_followthrough.planned_work.map((row: JsonObject) => row.todo_id), ["todo-a"]); + assert.equal(validateReplanContext(context), context); +}); + +test("declared work returns canonical state and only its subsequent observations", () => { + const decision = reviewed(2); + decision._source_facts.planned_todo_ids = ["todo-experiment", "todo-unavailable"]; + const before = {...subsequent(1), todo_id: "todo-experiment", + progress_observation: {...progress, work_item_id: "todo-experiment", fingerprint: "before"}}; + const experiment = {...subsequent(3), todo_id: "todo-experiment", + progress_observation: {...progress, work_item_id: "todo-experiment", fingerprint: "negative-result"}}; + const other = {...subsequent(4), todo_id: "todo-other", + progress_observation: {...progress, work_item_id: "todo-other", fingerprint: "other"}}; + const context = projectReplanContext({...strideRequest, rows: [before, decision, experiment, other], + todo_items: [{todo_id: "todo-experiment", role: "agent", status: "done", claimed_by: null, + task_class: "advancement_task", action_kind: "bounded_probe", text: "Private text stays outside this read model."}, + {todo_id: "todo-other", role: "agent", status: "open", claimed_by: "agent-a"}]}); + const follow = context.decision_followthrough as JsonObject; + assert.equal(follow.todo_source_available, true); + assert.equal(follow.execution_and_goal_gain, "unknown"); + assert.equal(follow.planned_work.length, 2); + const work = follow.planned_work[0]; + assert.equal(work.todo_id, "todo-experiment"); + assert.deepEqual(work.current, {status: "done", claimed_by: null, + task_class: "advancement_task", action_kind: "bounded_probe"}); + assert.equal(work.observation_refs.length, 1); + assert.equal(follow.observations.find((row: JsonObject) => row.evidence_ref === work.observation_refs[0]).coverage_ref, + "negative-result"); + assert.equal(follow.planned_work[1].current, null); + assert.deepEqual(follow.planned_work[1].observation_refs, []); + const withoutSource = projectReplanContext({...strideRequest, rows: [decision, experiment]}); + assert.equal(withoutSource.decision_followthrough.todo_source_available, false); + assert.equal(withoutSource.decision_followthrough.planned_work[0].current, null); + assert.deepEqual(validateReplanContext(context), context); + // Live work state changes the review basis, never the historical decision identity. + assert.equal(withoutSource.decision_followthrough.previous_decision.evidence_ref, + follow.previous_decision.evidence_ref); + assert.deepEqual(projectReplanContext({...request, rows: [decision], todo_items: [{invalid: true}]}), + projectReplanContext({...request, rows: [decision]})); + for (const patch of [{todo_source_available: false}, + {planned_work: [{...work, observation_refs: ["evidence:invented"]}]}, + {planned_work: [work, work]}, {omitted_planned_work_count: -1}]) { + assert.throws(() => validateReplanContext({...context, + decision_followthrough: {...follow, ...patch}}), /planned work/); + } + const unrelated = Array.from({length: 12}, (_, i) => ({...subsequent(i + 5), todo_id: "todo-other", + progress_observation: {...progress, work_item_id: "todo-other", fingerprint: "unrelated-" + i}})); + const crowded = projectReplanContext({...strideRequest, rows: [decision, experiment, ...unrelated]}); + assert.equal(crowded.decision_followthrough.observations.length, 6); + assert.equal(crowded.decision_followthrough.omitted_observation_count, 7); + assert.equal(crowded.decision_followthrough.planned_work[0].observation_refs.length, 1); +}); + function autonomousReviewed(position = 0, obligation = "replan-first"): JsonObject { const legacy = reviewed(position); const source = legacy._source_facts as JsonObject; diff --git a/tests/control_plane_ts/stride_policy.test.ts b/tests/control_plane_ts/stride_policy.test.ts index a8b2444e99..e14f378b88 100644 --- a/tests/control_plane_ts/stride_policy.test.ts +++ b/tests/control_plane_ts/stride_policy.test.ts @@ -1,6 +1,6 @@ import assert from "node:assert/strict"; import test from "node:test"; -import {normalizeStrideDecision, strideControlContract, stridePlanningGuidance} from "../../loopx/control_plane/work_items/stride_policy.ts"; +import {normalizeStrideDecision, strideControlContract, stridePlanningGuidance, strideReviewMismatch} from "../../loopx/control_plane/work_items/stride_policy.ts"; import {buildVisionCheckpoint, VISION_REFRESH_REQUEST_SCHEMA} from "../../loopx/control_plane/goals/vision_checkpoint.ts"; import {projectReplanSemantics} from "../../loopx/control_plane/work_items/replan_semantics.ts"; @@ -21,6 +21,39 @@ const request = {schema_version: VISION_REFRESH_REQUEST_SCHEMA, phase: "prepare" agent_id: "agent-a", merge_patch: false, existing_agent_vision: null, require_path_delta_for_durable_change: false, agent_vision_packet: packet}; +test("previous-outcome claims need subsequent evidence; old comparison evidence remains usable", () => { + const previous = "replan-decision-" + "b".repeat(24); + const context = {evidence: [{evidence_ref: "evidence:before"}, {evidence_ref: "evidence:after"}], + decision_followthrough: {previous_decision: {evidence_ref: previous}, + observations: [{evidence_ref: "evidence:after"}]}}; + const review = {reference_decision_ref: previous, adoption: "executed", information_gain: "new_evidence", + goal_gain: "flat", evidence_refs: ["evidence:before"], lesson: "The complete comparison did not improve the objective."}; + const decision = {...proposal, previous_review: review}; + assert.match(String(strideReviewMismatch(decision, context)), /subsequent/); + assert.equal(strideReviewMismatch({...decision, previous_review: {...review, + evidence_refs: ["evidence:before", "evidence:after"]}}, context), null); + assert.equal(strideReviewMismatch({...decision, previous_review: {...review, + adoption: "unknown", information_gain: "unknown", goal_gain: "unknown", evidence_refs: []}}, context), null); +}); + +test("another Todo's activity cannot establish execution or benefit of the declared experiment", () => { + const previous = "replan-decision-" + "b".repeat(24); + const context = {decision_followthrough: {previous_decision: {evidence_ref: previous}, + observations: [{evidence_ref: "evidence:other"}, {evidence_ref: "evidence:experiment"}], + planned_work: [{todo_id: "todo-experiment", current: {status: "done"}, + observation_refs: ["evidence:experiment"]}]}}; + const review = {reference_decision_ref: previous, adoption: "executed", information_gain: "new_evidence", + goal_gain: "improved", evidence_refs: ["evidence:other"], lesson: "An unrelated task completed."}; + assert.match(String(strideReviewMismatch({...proposal, previous_review: review}, context)), /declared work/); + assert.equal(strideReviewMismatch({...proposal, previous_review: {...review, + adoption: "not_executed", goal_gain: "unknown"}}, context), null); + // Negative experiments may complete; neither a score threshold nor success is required. + assert.equal(strideReviewMismatch({...proposal, previous_review: {...review, + goal_gain: "regressed", evidence_refs: ["evidence:experiment"]}}, context), null); + assert.equal(strideReviewMismatch({...proposal, previous_review: {...review, + adoption: "unknown", information_gain: "unknown", goal_gain: "unknown", evidence_refs: []}}, context), null); +}); + test("Explore review keeps bare lowercase digest revisions and unavailable null distinct", () => { const explore = {disposition: "deferred", considered_refs: [], reason: "Scoped later."}; for (const revision of ["0123456789abcdef".repeat(4), null]) { From cae3257e34bb40436805f768e4883bb6c2b45078 Mon Sep 17 00:00:00 2001 From: song Date: Fri, 16 Oct 2026 17:19:28 +0800 Subject: [PATCH 08/22] docs(research): clarify outcome evidence replay and planning guidance Document complete-source citation replay, readable legacy Todo declarations, unknown/negative outcomes and compact versus standalone guidance delivery. Record the bounded output tradeoff without claiming model adoption or score improvement. Signed-off-by: song --- .../hierarchical-agent-stride-control-v0.md | 32 ++++++++++++- ...rarchical-agent-stride-control-v0.zh-CN.md | 23 ++++++++- docs/development/testing-and-quality.md | 48 +++++++++---------- .../protocols/loopx-goal-command-v0.md | 16 +++++-- 4 files changed, 89 insertions(+), 30 deletions(-) diff --git a/docs/architecture/rfcs/hierarchical-agent-stride-control-v0.md b/docs/architecture/rfcs/hierarchical-agent-stride-control-v0.md index 5b6cf09285..3b843afe0f 100644 --- a/docs/architecture/rfcs/hierarchical-agent-stride-control-v0.md +++ b/docs/architecture/rfcs/hierarchical-agent-stride-control-v0.md @@ -759,8 +759,36 @@ these facts do not change the admitted context/basis identity. Changed source requires rereading within the original Turn. Page coordinates persist with the decision into subsequent review; node-filtered/planner detail routes remain outside this bounded citation path. No separate receipt store is introduced. -These are provenance and proposal-shape obligations, not automatic proof that -research was executed or useful. Ordinary Todo/Next Action carries admitted work. +The enabled review also carries the prior decision's existing `todo_delta` +targets in `decision_followthrough.planned_work`: current canonical status and +claim, plus references to causally later observations of that exact work item. +Legacy descriptive Todo deltas remain readable with unresolved linkage; an +overlong reference is never truncated into a different Todo identity. +Missing Todo sources and omitted work remain explicit; a missing observation +does not prove non-execution, and Todo completion does not prove objective gain. +Related observations survive unrelated recent activity in the bounded window. +The same bounded followthrough is included in `stride_control` so compact Host +envelopes expose it without requiring another full-context read. Historical +decision identities and off-mode output remain unchanged. + +Outcome claims require at least one subsequent observation; earlier evidence +can still be cited for comparison. Writeback replays up to three cited references +against the complete scoped history through the existing typed replan owner. +Neither the six-row followthrough display nor the main evidence display limits +eligibility. Ordinary work rows remain usable without optional typed-progress +attachments, while accounting-only rows cannot establish execution. Execution +or gain claims for declared work must cite that work's subsequent observation, +not another Todo's activity. This +is a provenance check, not a quality threshold or an automatic truth verdict. +An executed negative experiment remains a legal outcome. Planning guidance uses +existing `todo_delta` action/id entries to connect `next_probe` with real Todo +work, preserves unfinished useful experiments and separates informative +evaluation from adopting a replacement artifact. No mandatory new proposal +field, acceptance document, spawn permission or score gate is introduced. +Ordinary Todo/Next Action still carries admitted work. Real File/SQLite CLI +qualification covers a decision, selected successor execution, negative evidence, +next-review readback and rejection/recovery of a claim citing only old evidence; +model adoption and score or cost benefits remain unqualified. The activation vocabulary and existing Goal/device editor stay unchanged; no second selectable strategy or policy database is added. Off retains ordinary diff --git a/docs/architecture/rfcs/hierarchical-agent-stride-control-v0.zh-CN.md b/docs/architecture/rfcs/hierarchical-agent-stride-control-v0.zh-CN.md index b6c95e6ceb..21cfd771d6 100644 --- a/docs/architecture/rfcs/hierarchical-agent-stride-control-v0.zh-CN.md +++ b/docs/architecture/rfcs/hierarchical-agent-stride-control-v0.zh-CN.md @@ -628,7 +628,28 @@ switch(尝试不同机制)或 verify(解决决定扩大或切换的具体 开启时,复用既有 scoped finding、planner 分支和省略结果的读取路径;提案绑定 revision、考虑的 finding 与适用/暂缓等理由,不建新图或第二个 planner。机器 拒绝缺少复盘、错误引用以及过期或不可用依据的错误声明,校验提案结构和来源, -不能证明研究已执行或有收益。正常 Todo/Next Action 接续已准入的工作。 +不能证明研究已执行或有收益。 + +启用策略后,`decision_followthrough.planned_work` 将上次决定中既有 `todo_delta` +指向的 Todo 与当前规范状态、认领人和该工作项在决定之后的观测引用一起返回。 +Todo 来源缺失和省略数量明确保留;缺少观测不等于没有执行,Todo 完成也不等于 +目标收益。历史描述性 Todo delta 保持可读并注明关联未解析;过长引用不会截断成 +另一个 Todo 的身份。相关观测不会被大量较新的无关活动挤出有限窗口。 +同一份有界反馈进入 +`stride_control`,紧凑 Host envelope 可直接消费,避免为这些信息再读完整上下文。 +历史决定的引用身份及 off 模式输出不变。 + +结果声明至少需要一条决定之后的观测,较早证据仍可作为对照。写回经既有类型化 +replan owner 从完整、同范围的历史重放最多三条引用;六条反馈展示及主证据展示 +上限均不改变证据资格。普通工作回写不必附带可选 typed-progress 才能作为证据, +单纯记账行不能证明执行。声明已执行或有 +收益时,若决定明确指向 Todo,必须引用该 Todo 的后续观测,不能借用其他任务的 +活动。这是来源校验,不是质量门槛或自动真实性判定;已执行的负结果仍是合法结果。 +规划指引通过已有 `todo_delta` 动作/id 将 `next_probe` 关联到真实 Todo,保留尚未 +完成但仍有价值的实验,并区分信息性评测与替换最佳产物。不增加必填提案字段、 +验收文档、spawn 权限或分数门槛。正常 Todo/Next Action 继续承载已准入工作。 +真实 File/SQLite CLI 验证覆盖决定、选中后继执行、负证据、下一轮复盘回读,以及 +仅引用旧证据时的拒绝和修正;模型采用、得分与成本收益尚未验证。 配置词表和既有 Goal/设备编辑器不变,不加第二个可选版本或策略数据库。off 保持 普通投影/复盘;两种历史 v1 决策保留原义读取兼容,不能完成新的启用策略复盘; diff --git a/docs/development/testing-and-quality.md b/docs/development/testing-and-quality.md index 884526476d..600f436d8c 100644 --- a/docs/development/testing-and-quality.md +++ b/docs/development/testing-and-quality.md @@ -1182,30 +1182,30 @@ it does not grant execution quota, spending, or provider authority. or promotion thresholds stay fixed for that result; revised thresholds belong to a new qualification, never a relabeled historical pass. -The initial-delivery guidance change measured the same 96-row fixture at -`f6cf9583794eb8c4cca41fa832a1c2f150059583` and -`da01f996c820aad3bae85268963e64c6bbb19d7c`. The complete instruction adds -2,340 JSON characters to guided start/bootstrap, 3,496 to explicit command-pack -detail, and 578 to bootstrap Markdown, with no added lines. Existing planner, -plan-prompt, guided-transaction and message consumers retain their contracts; -removing a copy requires a caller migration. The instruction preserves strong -feasible first quality, coherent implementation/validation, full-cost comparison, -bounded diagnostic probes and existing acceptance/feedback/authority limits. -Truncating those obligations to fit the old differential is not lossless. - -Only the observed absence-to-complete-guidance transition on those carriers gets -2,400 / 3,600 / 600 character-and-byte allowances (60 / 104 / 22 headroom). -The probe recognizes the complete frozen structured field or rendered Todo -instruction, never a runtime permission or quality classification. Absolute -ceilings, semantic fields, fixture scale, line limits and every other surface -remain unchanged; an already-upgraded, missing or incomplete instruction gets no -transition allowance. This qualifies the disclosed default guidance change, -not a real model's adoption, benchmark improvement or additional execution quota. - -首次交付指引在同一 96 行负载上增加默认 JSON 2340、详情 JSON 3496、Markdown -578 个字符,行数不变。保留现有消费者与完整质量、诊断和权限义务,仅对上述输出 -载体从缺失到完整指引的首次变化给予 2400/3600/600 的有界额度;绝对上限、语义、 -规模及后续增长检查不变。原失败保留;不据此声称模型采用、实验增益或额外执行权限。 +Initial-delivery guidance originally appeared four times in guided JSON and six +times with command-pack detail. The earlier 96-row comparison measured growth +of 2,340 / 3,496 JSON characters and 578 Markdown characters. The revised +consumer uses the complete shared planner field from repeated structured plan +prompts. Standalone messages expand the full guidance locally: guided JSON now +has one complete copy, or two with the independently usable detailed message. +No instruction clause is removed to meet a byte ceiling. + +The expanded shared guidance is 1,238 bytes. Standalone rendering additionally +needs its explicit field label, spacing and reference instruction. The bounded +absence-to-complete-guidance allowance remains 2,400 / 3,600 JSON characters and +bytes, and is 1,400 for Markdown with at most four additional lines. This +one-time allowance applies only to the existing four carriers; absolute limits, +fixture scale and later growth checks remain in force. Recognizing the complete +frozen or current owner-provided instruction is an observation, not proof that +arbitrary prose changes preserve semantics or that a model will adopt them. + +首次交付指引原先在普通/详情 JSON 中重复四/六次;既有 96 行对照保留原始 +2340/3496 JSON、578 Markdown 字符增长记录。结构化计划现引用同一 planner +完整字段,独立消息就地展开全文,普通/详情 JSON 分别保留一/两份完整指引。 +扩展后的指引为 1238 字节;独立消息另需字段标签、引用指令与间隔。因此仅对既有 +四个载体的首次引入保留 JSON 2400/3600、Markdown 1400 字符及字节、最多四行 +的有界额度。绝对上限、负载规模、后续增长检查不变,不删除义务,也不据此声称 +模型采用、实验增益、语义等价或额外执行权限。 The latest qualification pins true main `e47e4507a` and merged candidate `3955d1c8b` on the same public fixture, with explicit per-probe source imports. diff --git a/docs/reference/protocols/loopx-goal-command-v0.md b/docs/reference/protocols/loopx-goal-command-v0.md index d7803827ec..ead6015c62 100644 --- a/docs/reference/protocols/loopx-goal-command-v0.md +++ b/docs/reference/protocols/loopx-goal-command-v0.md @@ -154,14 +154,24 @@ but the shape depends on how clear the goal already is: concise todos to make the approach explicit, without arbitrary item-count caps or management-only filler. -The shared planner now includes `first_delivery_guidance`, also rendered in the -guided `/loopx` start prompt and the existing `todo plan` packet. This changes +The shared planner includes `first_delivery_guidance`, consumed by the guided +`/loopx` start prompt and the existing `todo plan` packet. Structured command +packs keep the full guidance in the planner and refer to it from repeated plan +prompts. Standalone bootstrap messages retain the full text without requiring +another packet read. This changes default Agent guidance in both ordinary and fine-grained planning: preserve a strong feasible first deliverable and its coherent implementation/validation, rather than deliberately weakening it to produce an earlier feedback signal. Judge initial quality alongside latency and full cost; diagnostic probes remain legal under the original acceptance, feedback, budget and authority limits, and -preparation must not grow indefinitely. The guidance adds no quality oracle, +preparation must not grow indefinitely. Check decision-critical interface, unit +and scale assumptions against available task material before expensive work. +Compare a feasible alternative with the strongest available starting point, +retain that reference and use a discriminating check when it resolves the +choice. Budget enough work to complete the selected experiment. An informative +negative experiment can finish its own Todo while the broader objective stays +open; evaluation readiness and replacement of the best artifact remain separate. +No new acceptance document is required. The guidance adds no quality oracle, mandatory external evaluation, submission allowance or machine admission gate. Model adoption and resulting quality require separate real-task evidence. From f7756ff29448f491159172d3ebf2647e7113a271 Mon Sep 17 00:00:00 2001 From: song Date: Fri, 16 Oct 2026 17:36:40 +0800 Subject: [PATCH 09/22] fix(testing): recognize planning guidance without dynamic regex Signed-off-by: song --- .../testing/cli_output_semantics.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/loopx/control_plane/testing/cli_output_semantics.py b/loopx/control_plane/testing/cli_output_semantics.py index 315ebf21fb..f037953d7f 100644 --- a/loopx/control_plane/testing/cli_output_semantics.py +++ b/loopx/control_plane/testing/cli_output_semantics.py @@ -61,13 +61,15 @@ def collect(value: Any) -> None: try: collect(json.loads(text)) except json.JSONDecodeError: - observed["first_delivery_guidance"] = bool(re.search( - r"^3\. Todos: [^\n]*" + re.escape(first_delivery_guidance) + r"$", - text, re.MULTILINE, - )) or bool(re.search( - r"^goal_start_contract\.planner\.first_delivery_guidance:\n" - + re.escape(_FIRST_DELIVERY_GUIDANCE) + r"$", text, re.MULTILINE, - )) + lines = text.split("\n") + observed["first_delivery_guidance"] = any( + line.startswith("3. Todos: ") and line.endswith(first_delivery_guidance) + for line in lines + ) or any( + line == "goal_start_contract.planner.first_delivery_guidance:" + and following == _FIRST_DELIVERY_GUIDANCE + for line, following in zip(lines, lines[1:]) + ) # Existential across every rendered candidate: an earlier line whose purpose is # not exact must not mask a later complete instruction. rendered_candidates = re.finditer( From fb6f060e7acebda7946e57c8c3fc80d0e1a8ff72 Mon Sep 17 00:00:00 2001 From: song Date: Fri, 16 Oct 2026 18:13:24 +0800 Subject: [PATCH 10/22] fix(testing): keep the CLI output probe importable against a base checkout The base/head differential copies cli_output_semantics.py out of the package and measures a base checkout that predates the planner guidance constant. A relative import there has no parent, and an absolute import fails on that base; both broke kernel-static-checks on this branch. Treat a missing owner as "guidance absent" and never let a None match. Bootstrap JSON packs carry their rendered message, so the expanded guidance lands twice (planner field plus the labelled block message-only consumers need). Measured growth is 2,821 chars; raise that one-time allowance to 3,000 and record the measured value in the parity fixture. Co-Authored-By: Claude Fable 5.1 Signed-off-by: song --- .../testing/cli_output_differential.py | 12 +++++++----- .../testing/cli_output_semantics.py | 17 +++++++++++++---- .../test_cli_output_differential.py | 2 +- 3 files changed, 21 insertions(+), 10 deletions(-) diff --git a/loopx/control_plane/testing/cli_output_differential.py b/loopx/control_plane/testing/cli_output_differential.py index 3d5c59a7da..cb3a0862e9 100644 --- a/loopx/control_plane/testing/cli_output_differential.py +++ b/loopx/control_plane/testing/cli_output_differential.py @@ -758,12 +758,14 @@ def _authoring_input_migration( "bootstrap_command_pack", "bootstrap_command_pack_message_only"} and before.get("first_delivery_guidance") is False and after.get("first_delivery_guidance") is True): - # Structured prompts now reference the shared planner (one full copy, - # two with the standalone message in detail). The standalone message - # retains the expanded 1,238-byte guidance plus its explicit reference - # label/spacing. Keep growth scoped to the first introduction only. + # Structured prompts reference the shared planner once. JSON packs that + # also carry their rendered message hold the expanded 1,238-byte + # guidance twice: the planner field plus the labelled block the + # message-only consumer needs (bootstrap measured 2,821; detail more). + # Keep growth scoped to the first introduction only. chars = ( - (3_600 if surface == "start_goal_guided_command_pack_detail" else 2_400) + (3_600 if surface == "start_goal_guided_command_pack_detail" + else 3_000 if surface == "bootstrap_command_pack" else 2_400) if base.get("format") == "json" else 1_400 ) limits = {"chars": chars, "utf8_bytes": chars, "lines": 4, diff --git a/loopx/control_plane/testing/cli_output_semantics.py b/loopx/control_plane/testing/cli_output_semantics.py index f037953d7f..a59d8fcc45 100644 --- a/loopx/control_plane/testing/cli_output_semantics.py +++ b/loopx/control_plane/testing/cli_output_semantics.py @@ -10,12 +10,18 @@ def authoring_input_observations(text: str) -> dict[str, bool]: - from ..goals.start_contract import _FIRST_DELIVERY_GUIDANCE """Recognize complete reviewed guidance for one-time output qualification. These observations never classify runtime authority. Match the structured owner and full instruction, not a keyword in arbitrary operator prose. """ + # The base/head differential probe copies this module out of the package + # and measures a base checkout that may predate the planner constant. A + # missing owner means the guidance is absent there, never an import error. + try: + from loopx.control_plane.goals.start_contract import _FIRST_DELIVERY_GUIDANCE + except ImportError: + _FIRST_DELIVERY_GUIDANCE = None readback_purpose = ( "Read each authored/reused id: one match, todo_detail_projection.source_complete=true; " "compare .todo.text, status/claim. Excerpts cannot verify writes. " @@ -37,12 +43,15 @@ def authoring_input_observations(text: str) -> dict[str, bool]: 'authority limits; identify their purpose and do not substitute them for the requested' ' deliverable.' ) + complete_guidance = tuple( + value for value in (first_delivery_guidance, _FIRST_DELIVERY_GUIDANCE) if value + ) observed = {"complete_todo_readback": False, "vision_cli_authoring": False, "first_delivery_guidance": False} def collect(value: Any) -> None: if isinstance(value, dict): - if value.get("first_delivery_guidance") in (first_delivery_guidance, _FIRST_DELIVERY_GUIDANCE): + if value.get("first_delivery_guidance") in complete_guidance: observed["first_delivery_guidance"] = True if (value.get("id") == "read_back_authored_todos" and value.get("kind") == "operator_or_agent_actions" @@ -63,11 +72,11 @@ def collect(value: Any) -> None: except json.JSONDecodeError: lines = text.split("\n") observed["first_delivery_guidance"] = any( - line.startswith("3. Todos: ") and line.endswith(first_delivery_guidance) + line.startswith("3. Todos: ") and line.endswith(complete_guidance) for line in lines ) or any( line == "goal_start_contract.planner.first_delivery_guidance:" - and following == _FIRST_DELIVERY_GUIDANCE + and following in complete_guidance for line, following in zip(lines, lines[1:]) ) # Existential across every rendered candidate: an earlier line whose purpose is diff --git a/tests/control_plane/test_cli_output_differential.py b/tests/control_plane/test_cli_output_differential.py index 68e55f3d8f..279bebfc0d 100644 --- a/tests/control_plane/test_cli_output_differential.py +++ b/tests/control_plane/test_cli_output_differential.py @@ -67,7 +67,7 @@ def _receipt(*rows: dict[str, object]) -> dict[str, object]: ("quota_should_run", "json", "vision_cli_authoring", (3049, 94, 1807)), ("loopx_turn_plan", "json", "vision_cli_authoring", (237, 0, 237)), ("start_goal_guided", "json", "first_delivery_guidance", (2340, 0, 2340)), - ("bootstrap_command_pack", "json", "first_delivery_guidance", (2340, 0, 2340)), + ("bootstrap_command_pack", "json", "first_delivery_guidance", (2821, 0, 2821)), ("bootstrap_command_pack", "markdown", "first_delivery_guidance", (578, 0, 0)), ("start_goal_guided_command_pack_detail", "json", "first_delivery_guidance", (3496, 0, 3496)), ("bootstrap_command_pack_message_only", "markdown", "first_delivery_guidance", (578, 0, 0)), From a0fa2798134259c59c7ae746077ecdc72ecdc020 Mon Sep 17 00:00:00 2001 From: song Date: Fri, 16 Oct 2026 18:16:59 +0800 Subject: [PATCH 11/22] fix(benchmark): keep the uploaded native prompt readable under any launcher umask The worker re-uploads the feedback prompt from a host temp file and the native copy preserves that file's mode while landing it root-owned. A launcher running under umask 077 therefore delivered a 0600 prompt that the container's agent user could not read, and every heartbeat arm failed at format_run_cmd with "Native task prompt is unavailable" before its first turn. Set the prompt mode explicitly before upload and cover the umask case. Co-Authored-By: Claude Fable 5.1 Signed-off-by: song --- benchmark/runtime/sforge.py | 4 ++ benchmark/tests/test_edgebench_prompts.py | 49 +++++++++++++++++++++++ 2 files changed, 53 insertions(+) diff --git a/benchmark/runtime/sforge.py b/benchmark/runtime/sforge.py index 459a43148b..b0aca3ebad 100644 --- a/benchmark/runtime/sforge.py +++ b/benchmark/runtime/sforge.py @@ -346,6 +346,10 @@ def format_run_cmd(self, prompt_path, *, model=None, cwd="", internet=True, resu with tempfile.TemporaryDirectory(prefix="benchmark-prompt-") as directory: prompt = Path(directory) / "task.md" prompt.write_text(self.feedback_prompt) + # The native copy preserves the host mode and lands root-owned. + # The container's agent user must read the prompt whatever + # umask the launcher runs under. + prompt.chmod(0o644) asyncio.run(self.environment.upload_file(prompt, prompt_path)) # Native run_agent saved the pre-override wrapper. Keep its visible # prompt artifact aligned with what all five workers actually read. diff --git a/benchmark/tests/test_edgebench_prompts.py b/benchmark/tests/test_edgebench_prompts.py index 10b9f9985c..e5221fbd56 100644 --- a/benchmark/tests/test_edgebench_prompts.py +++ b/benchmark/tests/test_edgebench_prompts.py @@ -369,3 +369,52 @@ def test_workspace_policy_cannot_drift_from_feedback_mode_or_startup( SForgeWorker(SForgeConfig(agent_model="fixture", agent_effort="xhigh"), profile="official", cwd="/task", feedback=feedback, feedback_prompt=prompt, workspace_instructions=instructions) + + +def test_native_prompt_upload_is_readable_regardless_of_launcher_umask(tmp_path, monkeypatch): + pytest.importorskip("sforge") + pytest.importorskip("harbor") + from sforge.harness.config import SForgeConfig + from benchmark.runtime.sforge import SForgeWorker + + monkeypatch.setenv("CODEX_AUTH_JSON_PATH", "/private-credential") + workspace = tmp_path / "workspace" + workspace.mkdir() + prompt = native_task_prompt("Complete the supplied task.", ["solver.py"], + submission_cooldown=3600, eval_interval=0, internet=False, + selection="pass_rate_first", score_direction="maximize") + worker = SForgeWorker(SForgeConfig(agent_model="fixture", agent_effort="xhigh"), + profile="heartbeat-resume", cwd=str(workspace), feedback_prompt=prompt, + feedback="native", workspace_instructions=None, task_objective=None) + worker.backend = SimpleNamespace(start_feedback=lambda handle: None) + worker.handle = "worker-handle" + worker.log_dir = tmp_path + remote = tmp_path / "remote-prompt.md" + modes = [] + + class Environment: + async def upload_file(self, source, target): + # The native copy keeps the host mode; record what the agent would see. + modes.append(Path(source).stat().st_mode & 0o777) + Path(target).write_text(Path(source).read_text()) + + async def exec(self, command): + result = subprocess.run(["/bin/bash", "-c", command], capture_output=True, text=True) + return SimpleNamespace(return_code=result.returncode, stdout=result.stdout) + + async def prepare(environment, text, **kwargs): + return None + + worker.environment = Environment() + worker.runtime = SimpleNamespace( + goal_id="fixture-goal", execution=SimpleNamespace(uses_loopx=True), + _prepare_phase=prepare, _write_task_document=prepare, _worker_env=lambda **kwargs: {}, + ) + monkeypatch.setattr("benchmark.runtime.sforge.worker_command", lambda *a, **kw: ["worker"]) + previous = os.umask(0o077) + try: + worker.format_run_cmd(str(remote), internet=False) + finally: + os.umask(previous) + assert modes == [0o644] + assert remote.read_text() == prompt From b7c47c97dfece4785aee83bf5b2266b1dc33b2b9 Mon Sep 17 00:00:00 2001 From: song Date: Fri, 16 Oct 2026 18:31:01 +0800 Subject: [PATCH 12/22] fix(benchmark): retry a dropped planning wake within the trial budget One planning wake whose native process exited non-zero without a plan or a harness error stranded whole paired launches: the model stream disconnected during planning, the entry raised "Planning failed" and the trial ended before execution with no submission. Retry such a wake up to three times inside the same phase budget and record every attempt beside the planning receipt. Timeouts, harness failures and exhausted budgets still fail once, and a validated or blocked plan is never re-run. Co-Authored-By: Claude Fable 5.1 Signed-off-by: song --- benchmark/runtime/sforge_entry.py | 23 ++++++++++++- benchmark/tests/test_task_entry.py | 52 ++++++++++++++++++++++++++++++ 2 files changed, 74 insertions(+), 1 deletion(-) diff --git a/benchmark/runtime/sforge_entry.py b/benchmark/runtime/sforge_entry.py index e3cf9886bb..5e6e8c953d 100644 --- a/benchmark/runtime/sforge_entry.py +++ b/benchmark/runtime/sforge_entry.py @@ -13,6 +13,17 @@ from .planning import task_plan_packet, validate_plan_readback from .worker import PHASE_WORK_RESERVE_SECONDS, loopx_command, run_once +# One planning wake whose native process exits non-zero without a harness +# error or a validated plan (a dropped model stream, a failed turn) must not +# strand the whole trial before execution. Retry within the same phase budget. +PLANNING_WAKE_ATTEMPTS = 3 + + +def _retryable_planning_wake(receipt: dict) -> bool: + return (receipt.get("ok") is not True and receipt.get("timed_out") is not True + and "error_kind" not in receipt and "planning" not in receipt + and receipt.get("return_code") not in (None, 0)) + def prepare_entry(env: dict[str, str]) -> bool: """Reuse a qualified initial plan; execution still requires ordinary quota.""" @@ -48,7 +59,17 @@ def prepare_entry(env: dict[str, str]) -> bool: # Native SForge owns one absolute trial deadline for planning and execution. # Do not inherit a separate planning cap from the shared worker environment. planning_env.pop("LOOPX_PLANNING_TIMEOUT_SEC", None) - receipt = run_once(planning_env) + attempts = [] + for attempt in range(1, PLANNING_WAKE_ATTEMPTS + 1): + receipt = run_once(planning_env) + attempts.append({"attempt": attempt, "ok": receipt.get("ok") is True, + "timed_out": receipt.get("timed_out") is True, + "return_code": receipt.get("return_code"), + **({"error_kind": receipt["error_kind"]} if "error_kind" in receipt else {})}) + remaining = float(env["LOOPX_PHASE_DEADLINE_EPOCH"]) - time.time() + if not _retryable_planning_wake(receipt) or remaining <= PHASE_WORK_RESERVE_SECONDS: + break + path.with_name("planning-attempts.json").write_text(json.dumps(attempts)) if receipt.get("ok") is not True: raise RuntimeError("Planning failed; execution was not started") entry = receipt.get("planning") or {} diff --git a/benchmark/tests/test_task_entry.py b/benchmark/tests/test_task_entry.py index 8a72e5887d..96ed5c0385 100644 --- a/benchmark/tests/test_task_entry.py +++ b/benchmark/tests/test_task_entry.py @@ -747,3 +747,55 @@ async def no_pending(**kwargs): assert profile.get("stride_strategy", "off") == (strategy or "off") assert agent._replan_receipt() == {field: value, **({"stride_strategy": strategy} if strategy is not None else {})} + + +def test_sforge_planning_entry_retries_a_dropped_planning_wake_within_budget(planning_env, monkeypatch): + import benchmark.runtime.sforge_entry as entry + + real = entry.run_once + seen = [] + + def flaky(env): + seen.append(env["LOOPX_TASK_STAGE"]) + if len(seen) == 1: + # The native process died without a plan or a harness error. + return {"ok": False, "timed_out": False, "return_code": 1} + return real(env) + + monkeypatch.setattr(entry, "run_once", flaky) + env = planning_env | {"LOOPX_PHASE_DEADLINE_EPOCH": str(time.time() + 600)} + assert entry.prepare_entry(env) + assert seen == ["plan", "plan"] + attempts = json.loads(Path(env["LOOPX_PLANNING_RESULT"]).with_name("planning-attempts.json").read_text()) + assert [a["ok"] for a in attempts] == [False, True] + assert attempts[0]["return_code"] == 1 + assert len(list(Path(env["LOOPX_WAKE_LOG_DIR"]).glob("*/receipt.json"))) == 1 + + +@pytest.mark.parametrize("receipt", [ + {"ok": False, "timed_out": True, "return_code": 124}, + {"ok": False, "timed_out": False, "return_code": 1, "error_kind": "ValueError"}, + {"ok": False, "timed_out": False, "return_code": 0}, +]) +def test_sforge_planning_entry_does_not_retry_timeouts_or_harness_failures(planning_env, monkeypatch, receipt): + import benchmark.runtime.sforge_entry as entry + + calls = [] + monkeypatch.setattr(entry, "run_once", lambda env: calls.append(env["LOOPX_TASK_STAGE"]) or dict(receipt)) + env = planning_env | {"LOOPX_PHASE_DEADLINE_EPOCH": str(time.time() + 600)} + with pytest.raises(RuntimeError, match="Planning failed"): + entry.prepare_entry(env) + assert calls == ["plan"] + attempts = json.loads(Path(env["LOOPX_PLANNING_RESULT"]).with_name("planning-attempts.json").read_text()) + assert len(attempts) == 1 and attempts[0]["ok"] is False + + +def test_sforge_planning_entry_stops_retrying_after_the_attempt_limit(planning_env, monkeypatch): + import benchmark.runtime.sforge_entry as entry + + calls = [] + monkeypatch.setattr(entry, "run_once", lambda env: calls.append(1) or {"ok": False, "timed_out": False, "return_code": 1}) + env = planning_env | {"LOOPX_PHASE_DEADLINE_EPOCH": str(time.time() + 600)} + with pytest.raises(RuntimeError, match="Planning failed"): + entry.prepare_entry(env) + assert len(calls) == entry.PLANNING_WAKE_ATTEMPTS From 495ceae691c5a1a881f0b9f8cd227ddeebb5b961 Mon Sep 17 00:00:00 2001 From: song Date: Fri, 16 Oct 2026 21:55:49 +0800 Subject: [PATCH 13/22] refactor(planning): keep quality message rendering with its owner Signed-off-by: song --- loopx/bootstrap_command_pack.py | 10 ++-------- loopx/control_plane/goals/start_contract.py | 13 +++++++++++++ 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/loopx/bootstrap_command_pack.py b/loopx/bootstrap_command_pack.py index fa585fc77c..c5ad8c48b6 100644 --- a/loopx/bootstrap_command_pack.py +++ b/loopx/bootstrap_command_pack.py @@ -26,6 +26,7 @@ from .control_plane.goals.start_contract import ( build_goal_start_contract, build_goal_start_prompt, + render_goal_start_quality_message, ) from .control_plane.goals.start_goal_todo_delta import ( append_todo_delta_render_line, @@ -2040,14 +2041,7 @@ def render_loopx_bootstrap_command_pack_message(payload: dict[str, Any]) -> str: Rerun `{payload.get("canonical_cli_command")} --agent-id ` with the selected identity before continuing.""" elif goal_text: - planner = goal_start_contract.get("planner") or {} - quality = planner.get("first_delivery_guidance") - # Structured packs carry this once in the shared planner. Message-only - # consumers need its complete text here, without another packet read. - quality_block = ( - f"goal_start_contract.planner.first_delivery_guidance:\n{quality}\n\n" - if isinstance(quality, str) and quality else "" - ) + quality_block = render_goal_start_quality_message(goal_start_contract) action = f"""This is an explicit goal-start invocation. Connect project-local LoopX state if needed: ```bash diff --git a/loopx/control_plane/goals/start_contract.py b/loopx/control_plane/goals/start_contract.py index b35b31cd82..f15cc1da31 100644 --- a/loopx/control_plane/goals/start_contract.py +++ b/loopx/control_plane/goals/start_contract.py @@ -184,6 +184,19 @@ def build_goal_start_contract( return contract + +def render_goal_start_quality_message(contract: dict[str, Any]) -> str: + """Render the shared planner's complete instruction for standalone messages.""" + planner = contract.get("planner") or {} + quality = planner.get("first_delivery_guidance") + # Structured packs carry this once in the shared planner. Message-only + # consumers need its complete text here, without another packet read. + return ( + f"goal_start_contract.planner.first_delivery_guidance:\n{quality}\n\n" + if isinstance(quality, str) and quality else "" + ) + + def build_goal_start_prompt( *, goal_text: str | None, From aa124595e25a45eb840c35aad0678673f2383a2e Mon Sep 17 00:00:00 2001 From: song Date: Fri, 16 Oct 2026 21:04:36 +0800 Subject: [PATCH 14/22] test(architecture): reconcile the retired governance module ratchet Signed-off-by: song (cherry picked from commit 11966b2a08243688d30fca307a1ecb0b0add7c9a) Signed-off-by: song --- tests/architecture/top_level_module_budget.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/architecture/top_level_module_budget.json b/tests/architecture/top_level_module_budget.json index ba36f981a5..d6b72cf0c2 100644 --- a/tests/architecture/top_level_module_budget.json +++ b/tests/architecture/top_level_module_budget.json @@ -1,6 +1,6 @@ { "schema": "loopx_top_level_module_budget_v0", - "baseline_commit": "14112f36cfa56400851494e4236c84dd74538069", - "max_top_level_modules": 146, + "baseline_commit": "a4483aaece5bc74ec3f969fd067229487022401b", + "max_top_level_modules": 145, "allowlist": ["__init__.py", "entrypoint.py", "cli.py"] } From b22bbea05bf9d17bd5eefa0e282705744cdba0a3 Mon Sep 17 00:00:00 2001 From: song Date: Sat, 17 Oct 2026 09:15:56 +0800 Subject: [PATCH 15/22] fix(benchmark): consume refused handoffs before checking capture roots Signed-off-by: song --- benchmark/runtime/checkpoint.py | 7 ++++-- benchmark/tests/test_checkpoint_handoff.py | 29 ++++++++++++++++------ 2 files changed, 26 insertions(+), 10 deletions(-) diff --git a/benchmark/runtime/checkpoint.py b/benchmark/runtime/checkpoint.py index bbb66de210..51da81e200 100644 --- a/benchmark/runtime/checkpoint.py +++ b/benchmark/runtime/checkpoint.py @@ -436,8 +436,6 @@ def held_quota_decision(listener, capture_root: Path, *, goal_id: str, agent_id: Without native_process this is output ordering only; a slow capture can outlive the native tool's yield interval and does not stop inference. """ - if not capture_root.is_absolute() or capture_root.resolve(strict=True) != capture_root: - raise ValueError("decision capture root must be a physical absolute directory") def remaining(): limit = 120 if deadline is None else min(120, deadline - time.monotonic()) if limit <= 0: @@ -449,6 +447,11 @@ def remaining(): channel.settimeout(remaining()) with channel.makefile("rb") as stream: line = stream.readline(4097) + # Consume the bounded request before checking Host prerequisites. A + # refused root must close this channel so the same source can recover; + # leaving it queued would keep the caller's listener always readable. + if not capture_root.is_absolute() or capture_root.resolve(strict=True) != capture_root: + raise ValueError("decision capture root must be a physical absolute directory") message = json.loads(line) if (not isinstance(message, dict) or set(message) != {"path", "sha256"} or not isinstance(message["path"], str) or not isinstance(message["sha256"], str)): diff --git a/benchmark/tests/test_checkpoint_handoff.py b/benchmark/tests/test_checkpoint_handoff.py index b460fbe066..42451d2cf6 100644 --- a/benchmark/tests/test_checkpoint_handoff.py +++ b/benchmark/tests/test_checkpoint_handoff.py @@ -485,7 +485,8 @@ def gate(*args, full_history): @pytest.mark.skipif(os.name != "posix", reason="Requires the Host Unix rendezvous") -def test_refused_handoff_keeps_original_wake_running(tmp_path, monkeypatch): +@pytest.mark.parametrize("capture_root_state", ["present", "missing", "symlink"]) +def test_refused_handoff_keeps_original_wake_running(tmp_path, monkeypatch, capture_root_state): """A resumed session reusing an earlier wake's capture root is refused, not fatal. The refused packet is neither released nor captured; the same source keeps @@ -503,14 +504,20 @@ def test_refused_handoff_keeps_original_wake_running(tmp_path, monkeypatch): wakes = tmp_path / "wakes" current = wakes / "wake-current" / "decisions" earlier = wakes / "wake-earlier" / "decisions" / "decision-earlier" - current.mkdir(parents=True, mode=0o700) + current.parent.mkdir(parents=True) + if capture_root_state == "present": + current.mkdir(mode=0o700) + foreign = tmp_path / "foreign" + foreign.mkdir() + if capture_root_state == "symlink": + current.symlink_to(foreign, target_is_directory=True) earlier.mkdir(parents=True) released = current / "decision-current" - released.mkdir() packets = {} for name, directory in (("earlier", earlier), ("current", released)): raw = json.dumps({"ok": False, "goal_id": "source", "error_code": "fixture"}).encode() - (directory / "decision.json").write_bytes(raw) + if name == "earlier": + (directory / "decision.json").write_bytes(raw) packets[name] = {"path": str(directory / "decision.json"), "sha256": hashlib.sha256(raw).hexdigest()} project = tmp_path / "project" project.mkdir() @@ -525,22 +532,28 @@ def test_refused_handoff_keeps_original_wake_running(tmp_path, monkeypatch): f" s.connect({address!r}); s.sendall(json.dumps(message).encode()+b'\\n')\n" " return s.makefile('rb').readline()\n" f"assert call({packets['earlier']!r}) == b''\n" + f"current = Path({str(current)!r})\n" + "if current.is_symlink(): current.unlink()\n" + f"Path({str(released)!r}).mkdir(parents=True)\n" + f"Path({packets['current']['path']!r}).write_bytes({raw!r})\n" f"assert json.loads(call({packets['current']!r}))['released'] is True\n" "Path('continued').write_text('source continued')\n") monkeypatch.setattr(worker, "native_command", lambda *args: [sys.executable, "-c", code]) wake = CheckpointWake(env, {"turn_id": "wake-current", "mode": "heartbeat", "stage": "execute"}) - wake.capture_root = current.resolve() + wake.capture_root = current with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as listener: listener.bind(address) listener.listen(1) wake.listener = listener with (tmp_path / "stdout").open("w") as stdout, (tmp_path / "stderr").open("w") as stderr: - assert wake.run(SimpleNamespace(), "body", tmp_path, None, stdout, stderr, 30) == 0 + assert wake.run(SimpleNamespace(), "body", tmp_path, None, stdout, stderr, 2) == 0 assert (project / "continued").read_text() == "source continued" [event] = wake.receipt["checkpoint_handoffs"] assert event["captured"] is False and event["released"] is False - assert event["error_kind"] == "ValueError" and "capture root" in event["error"] - assert (earlier / "decision.json").read_bytes() # Earlier evidence is untouched. + expected = "FileNotFoundError" if capture_root_state == "missing" else "ValueError" + assert event["error_kind"] == expected + assert (earlier / "decision.json").read_bytes() == raw + assert not list(foreign.iterdir()) # Refusal cannot write through a foreign root. def test_wake_receipt_error_tail_is_bounded_message(): From d0da647fbb9865952183a84aef9eafaabe594373 Mon Sep 17 00:00:00 2001 From: song Date: Sat, 17 Oct 2026 09:15:56 +0800 Subject: [PATCH 16/22] docs(recovery): align continuation guidance with clock-free planning Signed-off-by: song --- benchmark/README.md | 9 +++++++++ .../rfcs/hierarchical-agent-stride-control-v0.md | 5 +++++ .../rfcs/hierarchical-agent-stride-control-v0.zh-CN.md | 4 ++++ skills/loopx-self-repair/references/repair-patterns.md | 2 +- 4 files changed, 19 insertions(+), 1 deletion(-) diff --git a/benchmark/README.md b/benchmark/README.md index 28396d14e8..50f11c2c68 100644 --- a/benchmark/README.md +++ b/benchmark/README.md @@ -708,6 +708,12 @@ Omit the controller file and `controller_handoff` on a new run to disable automa controller capture; removing the checkpoint request disables capture entirely. Existing artifacts remain inert, and the source trial is not restarted by disabling it. +The native handoff accepts and reads one bounded request before validating its +physical capture root. A missing or symlinked root closes that connection; +the original source can correct its path and continue through the same listener. +The refusal neither captures nor releases the old packet and does not renew the +original wake deadline. The transport does not create or follow a foreign root. + To reproduce **joint source capture and both fresh-guest receivers**, use [`qualify_checkpoint_joint.py`](tests/qualify_checkpoint_joint.py) from the current clean dedicated LoopX checkout; keep it frozen for the whole command. Run the @@ -924,6 +930,9 @@ worker 保留原 guard,controller 暂停全部容器写入者,核对仍有 文件 witness,再封存原生评测机会、incumbent 及剩余预算。源变化、过期或失去 交接会拒绝;可恢复的捕获失败不取消原研究,也不得改用稍后的状态。解除暂停后, 源会话继续使用原 packet 和 deadline。 +原生交接先接收并读取一条有界请求,再校验捕获根目录的物理路径。目录缺失或为 +符号链接时关闭该连接,原来源可纠正路径后经同一监听器继续;拒绝不捕获、不释放 +旧 packet,不延长原 wake 截止时间,也不创建或跟随外来根目录。 显式开启的两处完整 controller 捕获 hook 共用 90 秒窗口(原为 30 秒),覆盖完整原生/研究上下文 准备、一致性复核与 live-source 捕获,并受原试验 phase 截止时间约束。controller 沿用同一个绝对截止时间;解除暂停后,只能在原 phase 内再用至多 30 秒封存静态副本。 diff --git a/docs/architecture/rfcs/hierarchical-agent-stride-control-v0.md b/docs/architecture/rfcs/hierarchical-agent-stride-control-v0.md index 5d026bc9a8..151fae0b91 100644 --- a/docs/architecture/rfcs/hierarchical-agent-stride-control-v0.md +++ b/docs/architecture/rfcs/hierarchical-agent-stride-control-v0.md @@ -852,6 +852,11 @@ after-wake hook alone cannot implement this ordering. The replan and Host owners must provide that handoff without an extra model call or work settlement. If the boundary cannot be qualified, record the missed point and continue under the existing execution contract; do not substitute a later state for that node. +The benchmark handoff consumes the bounded request before validating the physical +capture root. A missing or aliased root closes that connection, allowing the same +source to correct its path and continue without replaying or capturing the refused +packet. Real socket/process cases cover present, missing and symlinked roots; +they do not qualify a complete research checkpoint or extend the original deadline. Measure capture time and storage per node before choosing compression, shared immutable members or retention limits. Retention must preserve declared study points and their branches rather than silently selecting favorable outcomes. diff --git a/docs/architecture/rfcs/hierarchical-agent-stride-control-v0.zh-CN.md b/docs/architecture/rfcs/hierarchical-agent-stride-control-v0.zh-CN.md index 6b920f6bf0..84dba93c07 100644 --- a/docs/architecture/rfcs/hierarchical-agent-stride-control-v0.zh-CN.md +++ b/docs/architecture/rfcs/hierarchical-agent-stride-control-v0.zh-CN.md @@ -794,6 +794,10 @@ native 读取进程,执行两次完整读取,并保持到产物封存或 con 使自身校验基准失效。这只调整读取进程的生命周期,保持分页历史、cursor/profile 新鲜度、字节 witness、显式开启及原捕获/phase deadline。预检成本下降尚不证明 增长历史下的持续捕获可靠性,也不等于后继点 receiver 验收。 +Benchmark 交接先读取有界请求,再校验捕获根目录的物理路径。目录缺失或存在 +别名时关闭该连接,使同一来源能纠正路径并继续,不重放或捕获被拒 packet。 +真实 socket/进程用例覆盖目录存在、缺失和符号链接;这不认证完整研究检查点, +也不延长原截止时间。 显式 source-checkout controller 现可通过既有 registry/ session owner 准备已验证 fork,要求不同且已注册的 Goal/Agent 身份与匹配的 diff --git a/skills/loopx-self-repair/references/repair-patterns.md b/skills/loopx-self-repair/references/repair-patterns.md index 2110567b77..3efbf635aa 100644 --- a/skills/loopx-self-repair/references/repair-patterns.md +++ b/skills/loopx-self-repair/references/repair-patterns.md @@ -5,7 +5,7 @@ teaches a reusable control-plane lesson. | Pattern | Symptoms | Evidence To Read | Likely Root | Durable Repair | | --- | --- | --- | --- | --- | -| `benchmark_planning_clock_projection_gap` | A bounded runtime enforces its deadline, but initial planning receives no phase window or mistakes a planning-stage cap for the whole task budget. | The caller-owned clock, saved model input, planning readback and terminal wake dispositions; keep private trajectories outside public artifacts. | Runtime timeout enforcement and the provider's planning-input projection are disconnected. | Project the existing phase deadline, observed remaining time and any separately labeled planning limit at the provider input boundary. Preserve the product task packet, authority, acceptance and enforcement. Cover absent-clock parity, stage/phase distinctions, aging, invalid clocks and real planning readback. Treat model adoption and outcome improvement as separate experimental questions. | +| `benchmark_planning_clock_projection_gap` | A resumed task closes work using an earlier Host deadline even though a continuation was explicitly authorized; readable history and an old ready plan are mistaken for current executable work. | Original task constraints, Host phase receipts, saved model input, current authorized continuation, actual quota/frontier readback and terminal dispositions; keep private trajectories outside public artifacts. | Historical Host timing and closure context survived recovery without the new continuation intent; ordinary planning should not inject Host countdowns. | Keep Host deadlines, remaining time and execution reserves out of ordinary model input; retain external enforcement and task-owned time constraints. At the explicitly authorized archive-recovery boundary, preserve old history and closure records, identify earlier Host clocks as historical in the new continuation request, and reuse Goal/Todo/replan with a stable operation identity. Read back actionable work or review before calling continuation ready; an old plan or exit code zero is insufficient. Do not renew budgets on retry, reopen without authorization, clear a user stop, filter task deadlines or bypass evaluation cooldowns. Cover clock-free fresh/resumed input, idempotent recovery and legitimate stopping; qualify real model adoption and score gains separately. | | `cli_json_file_argument_contract_gap` | A `--*-json` option repeatedly rejects inline JSON as an unreadable object; reshaping it does not help, or Markdown shows only an invalid-request status. | The command's current help, file-input loader, structured error, Markdown renderer, and a synthetic valid-file recovery. | The option accepts a file path but help does not say so; file-read, JSON-decode and root-shape failures are conflated, or the renderer omits the error reason. | Clarify the existing file contract without silently adding inline input. Separate read, parse and shape diagnostics without echoing supplied contents or paths; preserve the reason in each output format. Validate missing/inline input, malformed/non-object files, same-path recovery and unchanged typed admission. Repair the owning CLI adapter, not provider readiness or Goal retry authority. | | `public_source_receipt_clock_precedes_reads` | Multiple sources share a batch-start observation time, or a receipt reports completion before its final source read returns. | Installed provider source pin, actual execution process bounds, source/receipt timestamps, a controlled advancing-clock case and canonical readback. | The provider captures one UTC timestamp before its HTTP loop and reuses it for source observations and completion. | Record each successful source observation after its bounded read and record completion after all attempts, including partial, empty and failed results. Keep publication/first-availability unknown, retain original receipts and reuse the existing typed identity/admission/projection owner. Verify late failures cannot move the completion boundary before an earlier successful observation; do not claim authenticated execution or downstream coverage from timestamp correctness. | | `public_source_failure_step_erasure` | A public-source plan is ready, but execution reports only a generic unavailable read, hiding whether visibility, download or text validation failed. | Original exact-plan receipt, source index, read-only process bounds, provider source pin and shared readback. | One catch path erased the reached provider step; readiness was mistaken for successful retrieval or the generic error was guessed to mean lost permission. | Preserve the actual reached step in existing redacted limitations, without exposing exception reasons or changing admission, retry or source status. Cover visibility, download, malformed text and partial HTTP failure through the canonical CLI and shared sinks. Keep original failed receipts; diagnose a changed producer under a bounded trial rather than repeatedly fetching the same object or freezing independent work. | From a8225b02e0d691a10d246b42653d51f313cf973d Mon Sep 17 00:00:00 2001 From: song Date: Sat, 17 Oct 2026 09:53:42 +0800 Subject: [PATCH 17/22] fix(benchmark): keep accept failures outside packet refusal recovery Signed-off-by: song --- benchmark/README.md | 4 ++ benchmark/runtime/checkpoint.py | 6 +- benchmark/runtime/checkpoint_wake.py | 69 +++++++++++----------- benchmark/tests/test_checkpoint_handoff.py | 51 ++++++++++++++++ 4 files changed, 95 insertions(+), 35 deletions(-) diff --git a/benchmark/README.md b/benchmark/README.md index 50f11c2c68..cc835b55c5 100644 --- a/benchmark/README.md +++ b/benchmark/README.md @@ -713,6 +713,8 @@ physical capture root. A missing or symlinked root closes that connection; the original source can correct its path and continue through the same listener. The refusal neither captures nor releases the old packet and does not renew the original wake deadline. The transport does not create or follow a foreign root. +Host failures before a connection is accepted remain execution failures. They +are not packet refusals and cannot enter the source-preserving retry loop. To reproduce **joint source capture and both fresh-guest receivers**, use [`qualify_checkpoint_joint.py`](tests/qualify_checkpoint_joint.py) from the current @@ -933,6 +935,8 @@ worker 保留原 guard,controller 暂停全部容器写入者,核对仍有 原生交接先接收并读取一条有界请求,再校验捕获根目录的物理路径。目录缺失或为 符号链接时关闭该连接,原来源可纠正路径后经同一监听器继续;拒绝不捕获、不释放 旧 packet,不延长原 wake 截止时间,也不创建或跟随外来根目录。 +连接尚未接收时的 Host 故障仍是执行失败,不是一次 packet 拒绝,不进入保留 +来源的请求重试循环。 显式开启的两处完整 controller 捕获 hook 共用 90 秒窗口(原为 30 秒),覆盖完整原生/研究上下文 准备、一致性复核与 live-source 捕获,并受原试验 phase 截止时间约束。controller 沿用同一个绝对截止时间;解除暂停后,只能在原 phase 内再用至多 30 秒封存静态副本。 diff --git a/benchmark/runtime/checkpoint.py b/benchmark/runtime/checkpoint.py index 51da81e200..c3aebfcaee 100644 --- a/benchmark/runtime/checkpoint.py +++ b/benchmark/runtime/checkpoint.py @@ -415,7 +415,7 @@ def capture_after_wake(env: dict[str, str], receipt: dict[str, Any], *, @contextmanager def held_quota_decision(listener, capture_root: Path, *, goal_id: str, agent_id: str, turn_instance_id: str | None, native_process=None, interrupt_timeout: float = 10, - interrupt_when=None, deadline: float | None = None): + interrupt_when=None, deadline: float | None = None, accepted_channel=None): """Host-side counterpart of the explicit quota pre-output transport. The existing typed owner decides whether this exact packet admits a fresh @@ -442,7 +442,9 @@ def remaining(): raise TimeoutError("original wake budget expired during decision handoff") return limit - channel, _ = listener.accept() + channel = accepted_channel + if channel is None: + channel, _ = listener.accept() with channel: channel.settimeout(remaining()) with channel.makefile("rb") as stream: diff --git a/benchmark/runtime/checkpoint_wake.py b/benchmark/runtime/checkpoint_wake.py index 81cc406340..c5c00ef9c4 100644 --- a/benchmark/runtime/checkpoint_wake.py +++ b/benchmark/runtime/checkpoint_wake.py @@ -203,39 +203,42 @@ def run(self, execution, body, wake, session_wake, stdout, stderr, timeout): raise subprocess.TimeoutExpired(command, timeout) if not select.select([self.listener], [], [], min(.1, remaining))[0]: continue - entered = False - try: - with held_quota_decision(self.listener, self.capture_root, - goal_id=self.env["LOOPX_GOAL_ID"], agent_id=self.env["LOOPX_AGENT_ID"], - turn_instance_id=None, native_process=process, - interrupt_timeout=min(10, remaining), interrupt_when=self._first_replan, - deadline=deadline) as held: - entered = True - if "native_process_return_code" not in held: - continue - stdout.flush() - session_wake.observe(wake / "stdout.jsonl") - event = {"obligation_id": held["intent"]["obligation_id"], - "decision_path": str(held["path"]), "decision_sha256": held["sha256"], - "captured": False, "research_point_qualified": False} - self.receipt.setdefault("checkpoint_handoffs", []).append(event) - try: - event.update(self._capture(held, deadline), captured=True) - except Exception as error: - # A missed optional point cannot discard the original - # committed guard or turn a later retry into its capture. - event.update(error_kind=type(error).__name__, error=str(error)[:500]) - except Exception as error: - # A refused handoff (unparseable, outside this wake's capture - # root, or rejected by the typed selector) is neither released - # nor captured; its caller already reports a typed transport - # failure. The uninterrupted original wake keeps running. - if entered or process.poll() is not None: - raise - self.receipt.setdefault("checkpoint_handoffs", []).append({ - "captured": False, "research_point_qualified": False, "released": False, - "error_kind": type(error).__name__, "error": str(error)[:500]}) - continue + # Only a consumed request can be refused and retried. + # Accept failures belong to the Host, not the packet. + with self.listener.accept()[0] as channel: + entered = False + try: + with held_quota_decision(self.listener, self.capture_root, + goal_id=self.env["LOOPX_GOAL_ID"], agent_id=self.env["LOOPX_AGENT_ID"], + turn_instance_id=None, native_process=process, + interrupt_timeout=min(10, remaining), interrupt_when=self._first_replan, + deadline=deadline, accepted_channel=channel) as held: + entered = True + if "native_process_return_code" not in held: + continue + stdout.flush() + session_wake.observe(wake / "stdout.jsonl") + event = {"obligation_id": held["intent"]["obligation_id"], + "decision_path": str(held["path"]), "decision_sha256": held["sha256"], + "captured": False, "research_point_qualified": False} + self.receipt.setdefault("checkpoint_handoffs", []).append(event) + try: + event.update(self._capture(held, deadline), captured=True) + except Exception as error: + # A missed optional point cannot discard the original + # committed guard or turn a later retry into its capture. + event.update(error_kind=type(error).__name__, error=str(error)[:500]) + except Exception as error: + # A refused handoff (unparseable, outside this wake's capture + # root, or rejected by the typed selector) is neither released + # nor captured; its caller already reports a typed transport + # failure. The uninterrupted original wake keeps running. + if entered or process.poll() is not None: + raise + self.receipt.setdefault("checkpoint_handoffs", []).append({ + "captured": False, "research_point_qualified": False, "released": False, + "error_kind": type(error).__name__, "error": str(error)[:500]}) + continue continuation = held.get("continuation_body") if continuation is not None: break diff --git a/benchmark/tests/test_checkpoint_handoff.py b/benchmark/tests/test_checkpoint_handoff.py index 42451d2cf6..8c5e343201 100644 --- a/benchmark/tests/test_checkpoint_handoff.py +++ b/benchmark/tests/test_checkpoint_handoff.py @@ -563,3 +563,54 @@ def test_wake_receipt_error_tail_is_bounded_message(): assert tail["error_kind"] == "ValueError" assert tail["error"].startswith("decision handoff must reference this Host's capture root") assert len(tail["error"]) == 500 and "\n" not in tail["error"] + + +@pytest.mark.skipif(os.name != "posix", reason="Requires the Host Unix rendezvous") +def test_accept_failure_is_not_retried_as_a_refused_packet(tmp_path, monkeypatch): + import errno + import socket + import tempfile + from contextlib import contextmanager + from pathlib import Path + from types import SimpleNamespace + + import benchmark.runtime.worker as worker + from benchmark.runtime.checkpoint_wake import CheckpointWake + + processes, accepts = [], [] + owned_child = worker.child_process + + @contextmanager + def record_child(*args, **kwargs): + with owned_child(*args, **kwargs) as process: + processes.append(process) + yield process + + monkeypatch.setattr(worker, "child_process", record_child) + env = {**os.environ, "LOOPX_PROJECT": str(tmp_path), "LOOPX_WAKE_LOG_DIR": str(tmp_path), + "LOOPX_GOAL_ID": "source", "LOOPX_AGENT_ID": "agent"} + wake = CheckpointWake(env, {"turn_id": "turn", "mode": "heartbeat", "stage": "execute"}) + with tempfile.TemporaryDirectory(prefix="accept-failure-", dir="/tmp") as short: + address = str(Path(short).resolve() / "host.sock") + with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as listener: + listener.bind(address) + listener.listen(1) + + class UnavailableListener: + def fileno(self): + return listener.fileno() + + def accept(self): + accepts.append(True) + raise OSError(errno.EMFILE, "file descriptor capacity exhausted") + + wake.listener = UnavailableListener() + code = ("import socket; s=socket.socket(socket.AF_UNIX,socket.SOCK_STREAM); " + f"s.connect({address!r}); s.sendall(b'{{}}\\n'); s.recv(1)") + monkeypatch.setattr(worker, "native_command", lambda *args: [sys.executable, "-c", code]) + with (tmp_path / "out").open("w") as stdout, (tmp_path / "err").open("w") as stderr: + with pytest.raises(OSError, match="capacity exhausted"): + wake.run(SimpleNamespace(), "body", tmp_path, None, stdout, stderr, 1) + assert len(accepts) == 1 + assert "checkpoint_handoffs" not in wake.receipt + assert len(processes) == 1 and processes[0].poll() is not None From 3c8ba61b5a039348c7d9e7536861fdb9be011799 Mon Sep 17 00:00:00 2001 From: song Date: Sat, 17 Oct 2026 09:53:42 +0800 Subject: [PATCH 18/22] test(planning): qualify the observed complete-guidance upgrade Signed-off-by: song --- docs/development/testing-and-quality.md | 14 +++++++ .../testing/cli_output_differential.py | 14 +++++++ .../testing/cli_output_semantics.py | 25 +++++++----- .../test_cli_output_differential.py | 40 +++++++++++++++++++ 4 files changed, 82 insertions(+), 11 deletions(-) diff --git a/docs/development/testing-and-quality.md b/docs/development/testing-and-quality.md index 32abcf6b04..4dbbd582e2 100644 --- a/docs/development/testing-and-quality.md +++ b/docs/development/testing-and-quality.md @@ -1203,6 +1203,15 @@ fixture scale and later growth checks remain in force. Recognizing the complete frozen or current owner-provided instruction is an observation, not proof that arbitrary prose changes preserve semantics or that a model will adopt them. +When the base already includes the original complete guidance, the expanded +research-followthrough instruction is a separate observed upgrade, not another +absence. The same 96-row workload measures standalone bootstrap growth of 481 +JSON characters and 801 Markdown characters plus three Markdown lines. Only +those bootstrap carriers receive a one-time 512/832 character-and-byte allowance +for the observed old-to-expanded transition. Absolute ceilings, other carriers, +missing observations and later growth keep their existing checks. The extra +probe flag is diagnostic evidence, not a runtime quality or permission rule. + 首次交付指引原先在普通/详情 JSON 中重复四/六次;既有 96 行对照保留原始 2340/3496 JSON、578 Markdown 字符增长记录。结构化计划现引用同一 planner 完整字段,独立消息就地展开全文,普通/详情 JSON 分别保留一/两份完整指引。 @@ -1211,6 +1220,11 @@ arbitrary prose changes preserve semantics or that a model will adopt them. 的有界额度。绝对上限、负载规模、后续增长检查不变,不删除义务,也不据此声称 模型采用、实验增益、语义等价或额外执行权限。 +基线已含原完整指引时,扩展研究复盘指引属于另一次明确升级,不再视为首次引入。 +同一 96 行负载的独立 bootstrap 消息实测增加 JSON 481、Markdown 801 字符及三行, +仅对该明确升级给予 512/832 字符及字节额度。绝对上限、其他载体、缺失观测和后续 +增长仍受原检查约束;新增探针标记不是运行时质量或权限规则。 + The latest qualification pins true main `e47e4507a` and merged candidate `3955d1c8b` on the same public fixture, with explicit per-probe source imports. It retains 1/36/18 Todos, 1/12/12 history records, complete command routes, diff --git a/loopx/control_plane/testing/cli_output_differential.py b/loopx/control_plane/testing/cli_output_differential.py index cb3a0862e9..748f75498c 100644 --- a/loopx/control_plane/testing/cli_output_differential.py +++ b/loopx/control_plane/testing/cli_output_differential.py @@ -771,6 +771,20 @@ def _authoring_input_migration( limits = {"chars": chars, "utf8_bytes": chars, "lines": 4, "compact_payload_chars": chars if base.get("format") == "json" else 0} reason = "complete initial-delivery quality guidance added" + elif (surface in {"bootstrap_command_pack", "bootstrap_command_pack_message_only"} + and before.get("first_delivery_guidance") is True + and after.get("first_delivery_guidance") is True + and before.get("expanded_first_delivery_guidance") is False + and after.get("expanded_first_delivery_guidance") is True): + # Main already carries the original complete instruction. The same + # 96-row workload measures +481 JSON chars, +801 Markdown chars and + # three Markdown lines for the expanded guidance and standalone label. + # Qualify that upgrade separately; absence and future growth are not it. + chars = 512 if base.get("format") == "json" else 832 + limits = {"chars": chars, "utf8_bytes": chars, + "lines": 0 if base.get("format") == "json" else 3, + "compact_payload_chars": chars if base.get("format") == "json" else 0} + reason = "complete initial-delivery guidance upgraded with research followthrough" return limits, [reason + "; bounded one-time growth"] if limits else [] diff --git a/loopx/control_plane/testing/cli_output_semantics.py b/loopx/control_plane/testing/cli_output_semantics.py index a59d8fcc45..033d60d041 100644 --- a/loopx/control_plane/testing/cli_output_semantics.py +++ b/loopx/control_plane/testing/cli_output_semantics.py @@ -47,12 +47,17 @@ def authoring_input_observations(text: str) -> dict[str, bool]: value for value in (first_delivery_guidance, _FIRST_DELIVERY_GUIDANCE) if value ) observed = {"complete_todo_readback": False, "vision_cli_authoring": False, - "first_delivery_guidance": False} + "first_delivery_guidance": False, "expanded_first_delivery_guidance": False} + + def guidance(value: Any) -> None: + if value in complete_guidance: + observed["first_delivery_guidance"] = True + if value != first_delivery_guidance: + observed["expanded_first_delivery_guidance"] = True def collect(value: Any) -> None: if isinstance(value, dict): - if value.get("first_delivery_guidance") in complete_guidance: - observed["first_delivery_guidance"] = True + guidance(value.get("first_delivery_guidance")) if (value.get("id") == "read_back_authored_todos" and value.get("kind") == "operator_or_agent_actions" and value.get("purpose") == readback_purpose @@ -71,14 +76,12 @@ def collect(value: Any) -> None: collect(json.loads(text)) except json.JSONDecodeError: lines = text.split("\n") - observed["first_delivery_guidance"] = any( - line.startswith("3. Todos: ") and line.endswith(complete_guidance) - for line in lines - ) or any( - line == "goal_start_contract.planner.first_delivery_guidance:" - and following in complete_guidance - for line, following in zip(lines, lines[1:]) - ) + for instruction in complete_guidance: + if any(line.startswith("3. Todos: ") and line.endswith(instruction) for line in lines) or any( + line == "goal_start_contract.planner.first_delivery_guidance:" + and following == instruction for line, following in zip(lines, lines[1:]) + ): + guidance(instruction) # Existential across every rendered candidate: an earlier line whose purpose is # not exact must not mask a later complete instruction. rendered_candidates = re.finditer( diff --git a/tests/control_plane/test_cli_output_differential.py b/tests/control_plane/test_cli_output_differential.py index 279bebfc0d..fc93427f44 100644 --- a/tests/control_plane/test_cli_output_differential.py +++ b/tests/control_plane/test_cli_output_differential.py @@ -104,6 +104,46 @@ def test_authoring_growth_is_bounded_one_time_and_surface_scoped( assert _compare_row(base, inflated)["failures"] +@pytest.mark.parametrize("fmt,growth,lines", [("json", 481, 0), ("markdown", 801, 3)]) +def test_expanded_quality_guidance_upgrade_is_distinct_from_first_introduction(fmt, growth, lines): + from loopx.control_plane.testing.cli_output_differential import _compare_row + + before = {"first_delivery_guidance": True, "expanded_first_delivery_guidance": False} + after = {"first_delivery_guidance": True, "expanded_first_delivery_guidance": True} + base = _row(row_id=f"surface/bootstrap_command_pack/small/{fmt}", format=fmt, + chars=1000, utf8_bytes=1000, lines=20, compact_payload_chars=800, + authoring_inputs=before) + candidate = {**base, "chars": 1000 + growth, "utf8_bytes": 1000 + growth, + "lines": 20 + lines, "compact_payload_chars": 800 + (growth if fmt == "json" else 0), + "authoring_inputs": after} + result = _compare_row(base, candidate) + assert not result["failures"] and result["review_signals"] + assert _compare_row({**base, "authoring_inputs": after}, candidate)["failures"] + assert _compare_row({**base, "authoring_inputs": {"first_delivery_guidance": True}}, candidate)["failures"] + introduction = _compare_row( + {**base, "authoring_inputs": {**before, "first_delivery_guidance": False}}, candidate) + assert introduction["review_signals"] + assert not any("upgraded" in signal for signal in introduction["review_signals"]) + assert _compare_row( + {**base, "row_id": f"surface/status/small/{fmt}"}, + {**candidate, "row_id": f"surface/status/small/{fmt}"}, + )["failures"] + for metric in ("chars", "utf8_bytes", "lines", "compact_payload_chars"): + assert _compare_row(base, {**candidate, metric: base[metric] + result["allowances"][metric] + 1})["failures"] + + +def test_expanded_quality_observation_requires_complete_owner_instruction(): + from loopx.control_plane.goals.start_contract import _FIRST_DELIVERY_GUIDANCE + from loopx.control_plane.testing.cli_output_semantics import authoring_input_observations + + text = json.dumps({"planner": {"first_delivery_guidance": _FIRST_DELIVERY_GUIDANCE}}) + assert authoring_input_observations(text)["expanded_first_delivery_guidance"] + assert not authoring_input_observations(text.replace("strongest available starting point", "starting point"))[ + "expanded_first_delivery_guidance"] + assert not authoring_input_observations(json.dumps({"note": _FIRST_DELIVERY_GUIDANCE}))[ + "expanded_first_delivery_guidance"] + + @pytest.mark.parametrize("surface,fmt,clauses,growth", [ ("quota_should_run", "json", ["context"], 208), ("loopx_turn_plan", "json", ["context"], 209), From 4cf2660c0d4b8e7c567b16948d7addff75bce5e7 Mon Sep 17 00:00:00 2001 From: song Date: Sat, 17 Oct 2026 10:49:10 +0800 Subject: [PATCH 19/22] fix(replan): deliver ordinary declared-work evidence to followthrough Expose bounded ordinary writeback references separately from typed progress and link them to the declared Todo work. Preserve full-source citation checks and unknown execution or utility semantics. Cover scoped corrections, display bounds, replay rejection, and the real File/SQLite result-readback-to-next-comparison journey. Signed-off-by: song --- .../work_items/replan_context.ts | 52 +++++++++++- .../control_plane/work_items/stride_policy.ts | 10 ++- .../test_effective_turn_replan.py | 23 ++++- tests/control_plane_ts/replan_context.test.ts | 83 +++++++++++++++++++ 4 files changed, 157 insertions(+), 11 deletions(-) diff --git a/loopx/control_plane/work_items/replan_context.ts b/loopx/control_plane/work_items/replan_context.ts index 61b0177478..2e8400afc3 100644 --- a/loopx/control_plane/work_items/replan_context.ts +++ b/loopx/control_plane/work_items/replan_context.ts @@ -238,16 +238,28 @@ function decisionFollowthrough(rows: JsonObject[], goalFacts: unknown, fromFullI if (!decision) return null; const source = objectOrNull(previous._source_facts)!; const subsequent = new Map(); + const ordinary = new Map(); + const declared = Array.isArray(source.planned_todo_ids) ? source.planned_todo_ids : []; + const todoIds = plannedTodoIds(source); + const targets = new Set(todoIds); for (const row of scoped) { const facts = objectOrNull(row._source_facts)!; - if (Number(facts.history_position) <= Number(source.history_position) || !row.progress_observation) continue; + if (Number(facts.history_position) <= Number(source.history_position) || facts.quota_spend_commit) continue; + if (!row.progress_observation) { + if (targets.has(String(observationTodo(row)))) { + const ref = "replan-evidence-" + digest(readableEvidenceRow(row)); + if (!ordinary.has(ref)) ordinary.set(ref, {evidence_ref: ref, + generated_at: row.generated_at, todo_id: row.todo_id}); + } + continue; + } const progress = row.progress_observation as JsonObject; if (!subsequent.has(String(progress.fingerprint))) subsequent.set(String(progress.fingerprint), row); } // The complete denominator is retained even when the review details are capped. const observations = [...subsequent.values()]; - const declared = Array.isArray(source.planned_todo_ids) ? source.planned_todo_ids : []; - const todoIds = plannedTodoIds(source); + const ordinaryEvidence = [...ordinary.values()]; + const visibleOrdinary = ordinaryEvidence.slice(0, 6); const todos = todoItems == null ? [] : objects(todoItems, "Todo source"); const plannedWork = todoIds.slice(0, 6).map(todoId => { const matched = todos.filter(todo => todo.todo_id === todoId && todo.role === "agent" @@ -258,7 +270,9 @@ function decisionFollowthrough(rows: JsonObject[], goalFacts: unknown, fromFullI current: current ? Object.fromEntries(["status", "claimed_by", "task_class", "action_kind"] .filter(key => Object.hasOwn(current, key)).map(key => [key, current[key]])) : null, observation_refs: observations.filter(row => observationTodo(row) === todoId) - .slice(0, 6).map(row => "replan-evidence-" + digest(readableEvidenceRow(row)))}; + .slice(0, 6).map(row => "replan-evidence-" + digest(readableEvidenceRow(row))), + ...(ordinaryEvidence.length ? {ordinary_evidence_refs: visibleOrdinary + .filter(row => row.todo_id === todoId).map(row => row.evidence_ref)} : {})}; }); // Keep relevant experiment observations in the small visible window even // when newer unrelated work would otherwise crowd them out. @@ -279,6 +293,10 @@ function decisionFollowthrough(rows: JsonObject[], goalFacts: unknown, fromFullI coverage_ref: (row.progress_observation as JsonObject).fingerprint, })), omitted_observation_count: Math.max(0, observations.length - 6), + // Ordinary writebacks are source evidence, never fabricated typed progress. + // Keep existing progress counts and historical reference identities intact. + ...(ordinaryEvidence.length ? {ordinary_work: {recorded_count: ordinaryEvidence.length, + omitted_count: ordinaryEvidence.length - visibleOrdinary.length, evidence: visibleOrdinary}} : {}), todo_source_available: todoItems != null, planned_work: plannedWork, omitted_planned_work_count: Math.max(0, todoIds.length - plannedWork.length), @@ -287,12 +305,32 @@ function decisionFollowthrough(rows: JsonObject[], goalFacts: unknown, fromFullI guidance: "Compare the previous path decision with subsequent work and authoritative outcomes. " + "Recorded progress and changed identifiers do not prove execution, a new mechanism, or goal gain. " + "Use planned_work to compare declared Todo targets with their current state and subsequent observations. " + + (ordinaryEvidence.length ? "Ordinary declared-work evidence is returned separately in ordinary_work; resolve its references before reviewing outcomes. " : "") + "A missing row or observation is unknown, not proof of non-execution; a done Todo does not certify objective gain. " + "Cited evidence is replayed against full scoped history at writeback; this display is not an eligibility boundary. " + "Resolve evidence before deciding what to retain, change, or test next.", }; } +function ordinaryWorkEvidence(follow: JsonObject): JsonObject[] { + if (follow.ordinary_work === undefined) return []; + const ordinary = requireJsonObject(follow.ordinary_work, "ordinary work"); + const evidence = objects(ordinary.evidence, "ordinary work evidence"); + if (evidence.length > 6 || ["recorded_count", "omitted_count"].some(key => + !Number.isSafeInteger(ordinary[key]) || Number(ordinary[key]) < 0) || + Number(ordinary.recorded_count) !== evidence.length + Number(ordinary.omitted_count)) { + throw new EffectRuntimeRequestError("ordinary work requires bounded evidence and complete counts"); + } + const refs = evidence.map(row => { + identifier(row.todo_id, "ordinary work Todo"); + requireNonEmptyString(row.generated_at, "ordinary work generated_at"); + if (Object.hasOwn(row, "coverage_ref")) throw new EffectRuntimeRequestError("ordinary work cannot claim typed progress coverage"); + return identifier(row.evidence_ref, "ordinary work evidence reference"); + }); + if (new Set(refs).size !== refs.length) throw new EffectRuntimeRequestError("ordinary work evidence must be distinct"); + return evidence; +} + export function validateReplanContext(value: unknown): JsonObject { const context = requireJsonObject(value, "replan_context"); if (context.schema_version !== CONTEXT_SCHEMA) { @@ -323,6 +361,7 @@ export function validateReplanContext(value: unknown): JsonObject { throw new EffectRuntimeRequestError("decision followthrough requires the enabled strategy and unknown utility"); } const observations = objects(follow.observations, "decision_followthrough.observations"); + const ordinary = ordinaryWorkEvidence(follow); if (observations.length > 6 || typeof follow.source_history_complete !== "boolean" || ["recorded_progress_count", "omitted_observation_count", "unsequenced_progress_count"].some( key => !Number.isSafeInteger(follow[key]) || Number(follow[key]) < 0) || @@ -351,6 +390,11 @@ export function validateReplanContext(value: unknown): JsonObject { if (!Array.isArray(row.observation_refs) || row.observation_refs.some(ref => !refs.has(ref))) { throw new EffectRuntimeRequestError("planned work requires delivered subsequent observations"); } + if (follow.ordinary_work !== undefined && (!Array.isArray(row.ordinary_evidence_refs) || + row.ordinary_evidence_refs.some(ref => !ordinary.some(evidence => + evidence.evidence_ref === ref && evidence.todo_id === row.todo_id)))) { + throw new EffectRuntimeRequestError("planned ordinary work requires its delivered source evidence"); + } if (row.current !== null) { const current = requireJsonObject(row.current, "planned work current state"); if (!follow.todo_source_available || Object.entries(current).some(([key, value]) => diff --git a/loopx/control_plane/work_items/stride_policy.ts b/loopx/control_plane/work_items/stride_policy.ts index 7d5a424a66..1483ed2c30 100644 --- a/loopx/control_plane/work_items/stride_policy.ts +++ b/loopx/control_plane/work_items/stride_policy.ts @@ -266,13 +266,17 @@ export function strideReviewMismatch(decision: JsonObject, context: JsonObject, if (!previous && review) return "A previous decision review cannot claim an undelivered decision."; if (previous && review) { const rows = (value: unknown): JsonObject[] => Array.isArray(value) ? value as JsonObject[] : []; - const allowed = new Set([...rows(context.evidence), ...rows(follow?.observations)] + const ordinary = rows((follow?.ordinary_work as JsonObject | undefined)?.evidence); + const allowed = new Set([...rows(context.evidence), ...rows(follow?.observations), ...ordinary] .map(row => row.evidence_ref)); for (const row of rows(context.coverage_ledger)) for (const ref of Array.isArray(row.evidence_ids) ? row.evidence_ids : []) allowed.add(ref); const cited = review.evidence_refs as string[]; - const subsequent = new Set(rows(follow?.observations).map(row => row.evidence_ref)); + const subsequent = new Set([...rows(follow?.observations), ...ordinary].map(row => row.evidence_ref)); const planned = rows(follow?.planned_work); - const linked = new Set(planned.flatMap(work => Array.isArray(work.observation_refs) ? work.observation_refs : [])); + const linked = new Set(planned.flatMap(work => [ + ...(Array.isArray(work.observation_refs) ? work.observation_refs : []), + ...(Array.isArray(work.ordinary_evidence_refs) ? work.ordinary_evidence_refs : []), + ])); let hasDeclaredWork = planned.length > 0; if (sourceReview !== undefined) { const source = requireJsonObject(sourceReview, "stride review evidence"); diff --git a/tests/control_plane/test_effective_turn_replan.py b/tests/control_plane/test_effective_turn_replan.py index 4054843d2e..348c5cdde7 100644 --- a/tests/control_plane/test_effective_turn_replan.py +++ b/tests/control_plane/test_effective_turn_replan.py @@ -463,6 +463,7 @@ def call(*args: str, expected_code: int = 0) -> dict: "--todo-id", successor["todo_id"], "--turn-instance-id", turn, "--classification", "validated_progress", "--delivery-batch-scale", "implementation", "--delivery-outcome", "outcome_progress", "--delivery-boundary", "in_flight_continuation", + "--recommended-action", "The candidate regressed (error 5 versus reference 2); retain the reference and compare another source.", *(["--progress-result-class", "unchanged", "--progress-evidence-id", f"fixture:negative-result-{index}"] if index == 0 else []), "--no-global-sync", "--suppress-external-sinks") @@ -473,8 +474,15 @@ def call(*args: str, expected_code: int = 0) -> dict: "--agent-id", AGENT_ID, "--turn-instance-id", "stride-next-review", "--scan-path", str(project)) next_obligation = next_guard["autonomous_replan_obligation"] + delivered_result = next_obligation["replan_context"]["decision_followthrough"]["ordinary_work"]["evidence"][0] + observed_result = call("history", "--goal-id", GOAL_ID, "--agent-id", AGENT_ID, + "--evidence-ref", delivered_result["evidence_ref"])["evidence"] + assert "regressed (error 5 versus reference 2)" in observed_result["recommended_action"] + # The actor can now discover the actual ordinary result from the review, + # retain the reference, and choose a successor through the existing owner. + next_task = "Compare another source while retaining the reference" if "regressed" in observed_result["recommended_action"] else "Adopt the candidate" next_successor = call("todo", "add", "--goal-id", GOAL_ID, "--role", "agent", - "--claimed-by", AGENT_ID, "--text", "Resolve the remaining source uncertainty", + "--claimed-by", AGENT_ID, "--text", next_task, "--task-class", "advancement_task", "--action-kind", "validate", "--target-key", "source-uncertainty", "--replan-obligation-id", next_obligation["obligation_id"]) next_guard = call("quota", "should-run", "--codex-app", "--goal-id", GOAL_ID, @@ -498,8 +506,15 @@ def call(*args: str, expected_code: int = 0) -> dict: assert follow["unresolved_planned_work_count"] == 1 last_work = json.loads(Path(work["json_path"]).read_text()) assert "progress_observation" not in last_work - ordinary_ref = next(row["evidence_ref"] for row in next_obligation["replan_context"]["evidence"] - if row["generated_at"] == last_work["generated_at"]) + ordinary_ref = delivered_result["evidence_ref"] + # The optional typed-progress attachment is not required for the Agent to + # discover a declared experiment's ordinary result in its next review. + assert follow["ordinary_work"]["recorded_count"] == 1 + assert follow["ordinary_work"]["evidence"] == [{ + "evidence_ref": ordinary_ref, "generated_at": last_work["generated_at"], + "todo_id": successor["todo_id"], + }] + assert planned[0]["ordinary_evidence_refs"] == [ordinary_ref] from loopx.control_plane.quota.turn_envelope import build_turn_envelope envelope = build_turn_envelope(next_guard) assert envelope["replan_action_packet"]["stride_control"]["decision_followthrough"] == follow @@ -507,7 +522,7 @@ def call(*args: str, expected_code: int = 0) -> dict: "--evidence-ref", previous["evidence_ref"]) assert resolved["evidence"]["stride_decision"] == decision next_decision = {**decision, "reference_context_id": next_control["reference_context_id"], - "reference_obligation_id": next_obligation["obligation_id"]} + "reference_obligation_id": next_obligation["obligation_id"], "action": "switch"} refresh_args = ("refresh-state", "--goal-id", GOAL_ID, "--agent-id", AGENT_ID, "--replan-obligation-id", next_obligation["obligation_id"], "--turn-instance-id", "stride-next-review", "--agent-vision-json", str(decision_path), "--classification", "bounded_replan_progress", diff --git a/tests/control_plane_ts/replan_context.test.ts b/tests/control_plane_ts/replan_context.test.ts index ae2369ae9a..38bf52307e 100644 --- a/tests/control_plane_ts/replan_context.test.ts +++ b/tests/control_plane_ts/replan_context.test.ts @@ -131,6 +131,89 @@ function subsequent(position: number): JsonObject { const strideRequest = {...request, stride_strategy: "evidence_conditioned_v1"}; +type OrdinaryFollowthrough = { + recorded_progress_count: number; execution_and_goal_gain: string; observations: JsonObject[]; + previous_decision: JsonObject; + ordinary_work: {recorded_count: number; omitted_count: number; evidence: JsonObject[]}; + planned_work: {ordinary_evidence_refs: string[]; observation_refs: string[]}[]; +}; + +test("ordinary declared-work evidence reaches followthrough without becoming typed progress", () => { + const decision = reviewed(1); + (decision._source_facts as JsonObject).planned_todo_ids = ["todo-a"]; + const actual: JsonObject = {...subsequent(2), progress_observation: null, + health_check: "The alternate implementation regressed; retain the reference."}; + const ref = (projectReplanContext({...request, rows: [actual]}).evidence as JsonObject[])[0].evidence_ref; + const before = {...subsequent(0), progress_observation: null}; + const other = {...subsequent(3), todo_id: "todo-other", progress_observation: null}; + const accounting = {...actual, generated_at: row(10).generated_at, + _source_facts: {...actual._source_facts as JsonObject, history_position: 10, + quota_spend_commit: {schema_version: "quota_spend_commit_receipt_v0", effect_id: "spent"}}}; + const context = projectReplanContext({...strideRequest, rows: [before, decision, actual, actual, other, accounting]}); + const follow = context.decision_followthrough as OrdinaryFollowthrough; + assert.equal(follow.recorded_progress_count, 0); + assert.deepEqual(follow.observations, []); + assert.equal(follow.execution_and_goal_gain, "unknown"); + assert.equal(follow.previous_decision.evidence_ref, + (projectReplanContext({...strideRequest, rows: [decision]}).decision_followthrough as OrdinaryFollowthrough).previous_decision.evidence_ref); + assert.deepEqual(follow.ordinary_work, {recorded_count: 1, omitted_count: 0, + evidence: [{evidence_ref: ref, generated_at: actual.generated_at, todo_id: "todo-a"}]}); + assert.deepEqual(follow.planned_work[0].ordinary_evidence_refs, [ref]); + assert.deepEqual(follow.planned_work[0].observation_refs, []); + assert.equal(validateReplanContext(context), context); + const review = {previous_review: {reference_decision_ref: follow.previous_decision.evidence_ref, + adoption: "executed", information_gain: "new_evidence", goal_gain: "regressed", + evidence_refs: [ref], lesson: "Reject the alternate implementation, not the experiment evidence."}}; + assert.equal(strideReviewMismatch(review, context), null); + assert.equal((projectReplanContext({...request, operation: "resolve", rows: [actual], evidence_ref: ref}) + .evidence as JsonObject).health_check, actual.health_check); + for (const patch of [ + {ordinary_work: {...follow.ordinary_work, recorded_count: 0}}, + {ordinary_work: {...follow.ordinary_work, omitted_count: -1}}, + {ordinary_work: {...follow.ordinary_work, evidence: [...follow.ordinary_work.evidence, ...follow.ordinary_work.evidence]}}, + {ordinary_work: {...follow.ordinary_work, evidence: [{...follow.ordinary_work.evidence[0], coverage_ref: "invented-progress"}]}}, + {planned_work: [{...follow.planned_work[0], ordinary_evidence_refs: ["evidence:invented"]}]}, + ]) { + assert.throws(() => validateReplanContext({...context, + decision_followthrough: {...follow, ...patch}}), /ordinary work/); + } + assert.equal(projectReplanContext({...request, rows: [decision, actual]}).decision_followthrough, undefined); +}); + +test("ordinary followthrough retains corrections and bounded coverage under source scope", () => { + const decision = reviewed(1); + const current = {goal_id: "goal-a", goal_instance_id: "instance-current"}; + decision._source_facts = {...decision._source_facts as JsonObject, planned_todo_ids: ["todo-a"], goal_ref: current}; + const actual: JsonObject = {...subsequent(2), progress_observation: null, health_check: "The candidate initially appeared useful."}; + actual._source_facts = {...actual._source_facts as JsonObject, goal_ref: current}; + const correction: JsonObject = {...subsequent(3), progress_observation: null, + health_check: "The delayed result belongs to the earlier candidate; retain the incumbent.", + generated_at: row(0).generated_at}; + correction._source_facts = {...correction._source_facts as JsonObject, goal_ref: current}; + const rows = [decision, actual, correction, + {...actual, goal_id: "foreign-goal"}, {...actual, agent_id: "foreign-agent"}, + {...actual, _source_facts: {...actual._source_facts as JsonObject, goal_ref: {...current, goal_instance_id: "retired"}}}, + {...actual, _source_facts: {...actual._source_facts as JsonObject, history_position: undefined}}]; + const input = {...strideRequest, goal_facts: {goal_ref: current}, rows}; + const follow = projectReplanContext(input).decision_followthrough as OrdinaryFollowthrough; + assert.equal(follow.ordinary_work.recorded_count, 2); + assert.equal(follow.ordinary_work.evidence[0].generated_at, correction.generated_at, + "source order, not the corrected record's older clock, decides subsequent evidence"); + assert.equal(follow.execution_and_goal_gain, "unknown"); + const ref = follow.ordinary_work.evidence[0].evidence_ref; + assert.equal((projectReplanContext({...input, operation: "resolve", evidence_ref: ref}).evidence as JsonObject).health_check, + correction.health_check); + const more = Array.from({length: 8}, (_, i) => { + const value: JsonObject = {...subsequent(i + 4), progress_observation: null}; + return {...value, _source_facts: {...value._source_facts as JsonObject, goal_ref: current}}; + }); + const bounded = projectReplanContext({...input, rows: [...rows, ...more]}).decision_followthrough as OrdinaryFollowthrough; + assert.equal(bounded.ordinary_work.evidence.length, 6); + assert.equal(bounded.ordinary_work.recorded_count, 10); + assert.equal(bounded.ordinary_work.omitted_count, 4); + assert.equal(bounded.recorded_progress_count, 0); +}); + test("cited outcome evidence replays full history beyond display caps and optional progress records", () => { const decision = reviewed(1); decision._source_facts.planned_todo_ids = ["todo-a"]; From a0b20f9865aab76bd8700d35974ff24de266864b Mon Sep 17 00:00:00 2001 From: song Date: Sat, 17 Oct 2026 10:49:17 +0800 Subject: [PATCH 20/22] docs(replan): define ordinary evidence readback and qualification limits Record the optional followthrough fields in the existing protocol and both stride RFC translations. Distinguish the qualified CLI mechanism from pending provider adoption and research utility. Signed-off-by: song --- .../hierarchical-agent-stride-control-v0.md | 12 +++++++++++ ...rarchical-agent-stride-control-v0.zh-CN.md | 8 +++++++ .../goal-vision-replan-contract-v0.md | 21 +++++++++++++++++++ 3 files changed, 41 insertions(+) diff --git a/docs/architecture/rfcs/hierarchical-agent-stride-control-v0.md b/docs/architecture/rfcs/hierarchical-agent-stride-control-v0.md index 151fae0b91..ca72e80821 100644 --- a/docs/architecture/rfcs/hierarchical-agent-stride-control-v0.md +++ b/docs/architecture/rfcs/hierarchical-agent-stride-control-v0.md @@ -771,6 +771,18 @@ The same bounded followthrough is included in `stride_control` so compact Host envelopes expose it without requiring another full-context read. Historical decision identities and off-mode output remain unchanged. +The next-review input also exposes causally later ordinary writebacks for the +declared work in `ordinary_work`, with per-Todo `ordinary_evidence_refs`. Its +six-record detail window retains total/omitted counts and stable history reads; +it does not fabricate a typed-progress fingerprint or increase progress counts. +This closes a source-readback gap where legal evidence could qualify a writeback +but was absent from the decision-followthrough input. Real File/SQLite CLI +coverage uses the returned reference to read a worse candidate result, retain +the incumbent, and author the next comparison through the existing Todo/replan +owners. Late corrections, replay, scope and negative outcomes remain separate +from model judgment. This is not complete M3 provider/product qualification or +evidence of research utility; the §11 actual-provider/adoption journey remains. + Outcome claims require at least one subsequent observation; earlier evidence can still be cited for comparison. Writeback replays up to three cited references against the complete scoped history through the existing typed replan owner. diff --git a/docs/architecture/rfcs/hierarchical-agent-stride-control-v0.zh-CN.md b/docs/architecture/rfcs/hierarchical-agent-stride-control-v0.zh-CN.md index 84dba93c07..7e10390cbf 100644 --- a/docs/architecture/rfcs/hierarchical-agent-stride-control-v0.zh-CN.md +++ b/docs/architecture/rfcs/hierarchical-agent-stride-control-v0.zh-CN.md @@ -639,6 +639,14 @@ Todo 来源缺失和省略数量明确保留;缺少观测不等于没有执行 `stride_control`,紧凑 Host envelope 可直接消费,避免为这些信息再读完整上下文。 历史决定的引用身份及 off 模式输出不变。 +下一轮输入还通过 `ordinary_work` 与各 Todo 的 `ordinary_evidence_refs` 返回已 +声明工作在决定之后的普通回写。详情最多六项,保留总数、省略数和稳定 history +读取,不伪造 typed-progress 指纹或增加其计数。这补齐了“合法证据能通过写回 +校验,却没有进入决定接续输入”的读回缺口。真实 File/SQLite CLI 用返回的引用 +读取较差候选结果、保留原方案,并经既有 Todo/replan owner 创建下一项对照。 +迟到纠正、重放、范围和负结果与模型判断仍明确区分;不宣称完整 M3 provider/ +产品验收或研究收益,§11 的真实 provider 与实际采用路径仍需独立资格化。 + 结果声明至少需要一条决定之后的观测,较早证据仍可作为对照。写回经既有类型化 replan owner 从完整、同范围的历史重放最多三条引用;六条反馈展示及主证据展示 上限均不改变证据资格。普通工作回写不必附带可选 typed-progress 才能作为证据, diff --git a/docs/reference/protocols/goal-vision-replan-contract-v0.md b/docs/reference/protocols/goal-vision-replan-contract-v0.md index 2a71929543..e369ad55b9 100644 --- a/docs/reference/protocols/goal-vision-replan-contract-v0.md +++ b/docs/reference/protocols/goal-vision-replan-contract-v0.md @@ -290,6 +290,19 @@ execution, candidate adoption, scientific improvement or Goal completion. A missing, rejected or malformed basis adds no followthrough projection. No selection, trigger, quota, acceptance, lease or scheduler rule changes. +When declared Todo work has later ordinary writebacks without the optional +typed-progress attachment, followthrough also returns `ordinary_work`: up to six +immutable `evidence` references with their `generated_at` and `todo_id`, plus +`recorded_count` and `omitted_count`. Each displayed Todo's +`ordinary_evidence_refs` links only its own delivered records. Accounting-only, +earlier, foreign-lane and retired-instance rows are excluded; append order also +retains delayed corrections without treating their clock as causal order. +Resolve the references through the existing scoped history read before +reviewing the outcome. Ordinary evidence has no `coverage_ref`, does not increase +`recorded_progress_count`, and certifies no execution or gain. Its absence retains +the legacy packet shape, not a claim that no work happened. Feature-off output, +historical identities and full-source writeback qualification are unchanged. + 启用 `evidence_conditioned_v1` 且历史中已有被接受、绑定证据的 Vision 路线时,replan context 返回上次决定与之后的 进度引用,按合格来源的顺序关联,而非根据时间戳或文案猜测因果。完整本地索引提供 追加位置;已知最新在前的紧凑载体与 `newest_first_runs` 入口转换位置,保留普通 @@ -299,6 +312,14 @@ selection, trigger, quota, acceptance, lease or scheduler rule changes. 不等于有效结算 Turn、独立试验或执行/收益证明;后者明确保持 unknown。缺失、 拒绝或损坏的依据不产生投影;选择、触发、结算及权限规则不变。 +已声明 Todo 的后续普通回写即使没有可选 typed-progress,也经 `ordinary_work` +返回最多六条不可变引用、时间和 Todo 身份,并保留总数与省略数。各 Todo 的 +`ordinary_evidence_refs` 只指向自己的已投递记录;排除记账、决定前、其他 Agent +及旧 Goal 实例的记录,迟到纠正仍按来源追加顺序保留。通过既有 history 入口读取 +原文后再评估结果;普通证据不伪造 `coverage_ref`,不增加 typed-progress 计数, +也不证明执行或收益。没有普通证据时保留旧包形状,不据此断言没有工作。off 输出、 +历史身份和完整来源的写回资格不变。 + ### Replan planning guidance The shared `replan_action_packet.planning_guidance` carries five existing Agent From a602f8d34ee4b7260a64517864ab55389637e301 Mon Sep 17 00:00:00 2001 From: song Date: Sat, 17 Oct 2026 11:50:31 +0800 Subject: [PATCH 21/22] feat(replan): retain unresolved work evidence through recorded followthrough Reconstruct result, accepted decision and declared later-work relations from existing scoped history. Citations and reviews alone do not consume results; exact replay cannot resurrect them, and replaced proposals retain their source evidence. Keep execution and utility unknown without a parallel ledger or authority. Carry the real File/SQLite CLI journey through candidate/probe child processes, negative Todo completion and a further evidence-linked review. Document the bounded relation and remaining native model/research qualification. Signed-off-by: song --- .../hierarchical-agent-stride-control-v0.md | 10 +++ ...rarchical-agent-stride-control-v0.zh-CN.md | 6 ++ .../goal-vision-replan-contract-v0.md | 18 ++++ .../work_items/replan_context.ts | 83 +++++++++++++++++++ .../test_effective_turn_replan.py | 52 ++++++++++-- tests/control_plane_ts/replan_context.test.ts | 30 +++++++ 6 files changed, 193 insertions(+), 6 deletions(-) diff --git a/docs/architecture/rfcs/hierarchical-agent-stride-control-v0.md b/docs/architecture/rfcs/hierarchical-agent-stride-control-v0.md index ca72e80821..a6e13211a3 100644 --- a/docs/architecture/rfcs/hierarchical-agent-stride-control-v0.md +++ b/docs/architecture/rfcs/hierarchical-agent-stride-control-v0.md @@ -783,6 +783,16 @@ owners. Late corrections, replay, scope and negative outcomes remain separate from model judgment. This is not complete M3 provider/product qualification or evidence of research utility; the §11 actual-provider/adoption journey remains. +The same source now retains unresolved declared-work evidence across later +reviews. A citation becomes a recorded followthrough relation only after its +decision's declared Todo has a subsequent material writeback; citations or Todo +creation alone do not consume the result. Replay and replaced unexecuted plans +preserve the original evidence without a second ledger. Real File/SQLite CLI +coverage executes isolated candidate/probe processes, reads their artifacts, +authors the next comparison, and reads that relation at another review. The +consumer is scripted: native Host/model adoption, candidate applicability and +scientific utility remain independent, unqualified parts of §11. + Outcome claims require at least one subsequent observation; earlier evidence can still be cited for comparison. Writeback replays up to three cited references against the complete scoped history through the existing typed replan owner. diff --git a/docs/architecture/rfcs/hierarchical-agent-stride-control-v0.zh-CN.md b/docs/architecture/rfcs/hierarchical-agent-stride-control-v0.zh-CN.md index 7e10390cbf..ddc5741752 100644 --- a/docs/architecture/rfcs/hierarchical-agent-stride-control-v0.zh-CN.md +++ b/docs/architecture/rfcs/hierarchical-agent-stride-control-v0.zh-CN.md @@ -647,6 +647,12 @@ Todo 来源缺失和省略数量明确保留;缺少观测不等于没有执行 迟到纠正、重放、范围和负结果与模型判断仍明确区分;不宣称完整 M3 provider/ 产品验收或研究收益,§11 的真实 provider 与实际采用路径仍需独立资格化。 +同一来源现保留跨后续复盘尚待跟进的声明工作证据。只有决定引用结果、声明的 Todo +随后发生材料回写,才记录跟进关联;引用或创建任务本身不消耗结果。重放、替换未执行 +计划均保留原证据,不另建账本。真实 File/SQLite CLI 执行隔离候选/探针进程、读取产物、 +创建下一项对照,并在再下一轮复盘读回关联。消费方仍是脚本,原生 Host/模型采用、 +候选适用性及科学收益继续是 §11 中独立、未资格化的部分。 + 结果声明至少需要一条决定之后的观测,较早证据仍可作为对照。写回经既有类型化 replan owner 从完整、同范围的历史重放最多三条引用;六条反馈展示及主证据展示 上限均不改变证据资格。普通工作回写不必附带可选 typed-progress 才能作为证据, diff --git a/docs/reference/protocols/goal-vision-replan-contract-v0.md b/docs/reference/protocols/goal-vision-replan-contract-v0.md index e369ad55b9..fb32a35fd4 100644 --- a/docs/reference/protocols/goal-vision-replan-contract-v0.md +++ b/docs/reference/protocols/goal-vision-replan-contract-v0.md @@ -303,6 +303,17 @@ reviewing the outcome. Ordinary evidence has no `coverage_ref`, does not increas the legacy packet shape, not a claim that no work happened. Feature-off output, historical identities and full-source writeback qualification are unchanged. +`decision_followthrough.work_result_use` also reconstructs unresolved declared-work +evidence across reviews from the existing append order. Another review or a bare +citation cannot consume it: a recorded decision must cite its immutable history +reference, declare subsequent Todo work, and be followed by a material writeback +on that Todo. `recorded_followthrough` exposes that source relation, not certified +execution, candidate applicability or utility. Complete/omitted counts accompany +the six-row windows; replay does not resurrect consumed evidence. A replaced +plan invalidates its unexecuted proposal, not the unresolved source. This is a +read model, not a pending-duty ledger, new write gate or forced continuation. +The Agent still checks the provider's exact candidate/input/result revision. + 启用 `evidence_conditioned_v1` 且历史中已有被接受、绑定证据的 Vision 路线时,replan context 返回上次决定与之后的 进度引用,按合格来源的顺序关联,而非根据时间戳或文案猜测因果。完整本地索引提供 追加位置;已知最新在前的紧凑载体与 `newest_first_runs` 入口转换位置,保留普通 @@ -320,6 +331,13 @@ historical identities and full-source writeback qualification are unchanged. 也不证明执行或收益。没有普通证据时保留旧包形状,不据此断言没有工作。off 输出、 历史身份和完整来源的写回资格不变。 +`work_result_use` 从既有追加历史重建跨复盘尚待跟进的声明工作证据。又做一次复盘 +或只引用一次都不会消耗结果:决定须引用其不可变 history 身份、声明后续 Todo, +并出现该 Todo 的实际材料回写,才返回 `recorded_followthrough` 关联。它不认证执行、 +候选适用性或收益;Agent 仍应核对提供方的候选/输入/结果版本。两个六项窗口保留 +完整/省略计数,重放不复活已关联证据,替换未执行计划不删除原结果。它不是新待办 +账本、写入门槛或强制续跑规则。 + ### Replan planning guidance The shared `replan_action_packet.planning_guidance` carries five existing Agent diff --git a/loopx/control_plane/work_items/replan_context.ts b/loopx/control_plane/work_items/replan_context.ts index 2e8400afc3..f909d478ad 100644 --- a/loopx/control_plane/work_items/replan_context.ts +++ b/loopx/control_plane/work_items/replan_context.ts @@ -198,6 +198,63 @@ function observationTodo(row: JsonObject): unknown { return work == null ? row.todo_id : row.todo_id == null || row.todo_id === work ? work : null; } +/** Recover the result -> decision -> subsequent work relation from the existing + * source history. A citation alone does not consume a result, and no extra + * ledger or execution authority is created. These are recorded associations, + * not certification that a candidate ran or improved the Goal. */ +function workResultUse(scoped: JsonObject[]): JsonObject | null { + const pending = new Map(); + const origin = new Map(); + const proposed = new Map(); + const followed: JsonObject[] = []; + const seen = new Set(); + for (const row of [...scoped].reverse()) { + const source = objectOrNull(row._source_facts)!; + if (source.quota_spend_commit) continue; + if (acceptedDecisionSource(row)) { + const decision = recordedDecision(row); + if (!decision) { origin.clear(); proposed.clear(); continue; } + const id = String(decision.evidence_ref), targets = plannedTodoIds(source); + const path = decision.path_delta as JsonObject; + const review = objectOrNull(objectOrNull(decision.stride_decision)?.previous_review); + const refs = [...(path.evidence_refs as string[]), + ...(Array.isArray(review?.evidence_refs) ? review.evidence_refs as string[] : [])] + .filter(ref => pending.has(ref)); + for (const todo of targets) { + origin.set(todo, id); + // A newer plan for the same work replaces its unexecuted proposal, + // not the source evidence still waiting for attributable followthrough. + proposed.delete(todo); + if (refs.length) proposed.set(todo, {decision: id, results: [...new Set(refs)]}); + } + continue; + } + const todo = String(observationTodo(row)); + if (!origin.has(todo)) continue; + const ref = "replan-evidence-" + digest(readableEvidenceRow(row)); + if (seen.has(ref)) continue; + seen.add(ref); + const adoption = proposed.get(todo); + if (adoption) { + for (const result of adoption.results) if (pending.delete(result)) followed.push({ + result_evidence_ref: result, decision_ref: adoption.decision, + work_todo_id: todo, work_evidence_ref: ref}); + proposed.delete(todo); + } + if (!pending.has(ref)) pending.set(ref, {evidence_ref: ref, todo_id: todo, + source_decision_ref: origin.get(todo), generated_at: row.generated_at}); + } + if (!pending.size && !followed.length) return null; + const unresolved = [...pending.values()].reverse(), visible = unresolved.slice(0, 6); + return {unresolved_count: unresolved.length, omitted_unresolved_count: unresolved.length - visible.length, + unresolved: visible, recorded_followthrough_count: followed.length, + recorded_followthrough: followed.slice(-6).reverse(), + omitted_followthrough_count: Math.max(0, followed.length - 6), + guidance: "Resolve unresolved evidence even when a newer review has occurred. A cited result stays unresolved until later recorded work is linked through that decision's declared Todo. " + + "Recorded followthrough is an attributable history relation, not verified execution, applicability to a current candidate, or Goal gain. " + + "Check the provider's exact candidate/input/result revision before adopting an outcome; retain valid negative and inconclusive evidence. Existing authority, stopping and settlement rules remain unchanged."}; +} + /** Resolve only the cited refs against full scoped source facts at writeback. * Display limits never decide evidence eligibility. Ordinary work rows do not * need an optional typed progress attachment to remain attributable. */ @@ -260,6 +317,7 @@ function decisionFollowthrough(rows: JsonObject[], goalFacts: unknown, fromFullI const observations = [...subsequent.values()]; const ordinaryEvidence = [...ordinary.values()]; const visibleOrdinary = ordinaryEvidence.slice(0, 6); + const resultUse = workResultUse(scoped); const todos = todoItems == null ? [] : objects(todoItems, "Todo source"); const plannedWork = todoIds.slice(0, 6).map(todoId => { const matched = todos.filter(todo => todo.todo_id === todoId && todo.role === "agent" @@ -297,6 +355,7 @@ function decisionFollowthrough(rows: JsonObject[], goalFacts: unknown, fromFullI // Keep existing progress counts and historical reference identities intact. ...(ordinaryEvidence.length ? {ordinary_work: {recorded_count: ordinaryEvidence.length, omitted_count: ordinaryEvidence.length - visibleOrdinary.length, evidence: visibleOrdinary}} : {}), + ...(resultUse ? {work_result_use: resultUse} : {}), todo_source_available: todoItems != null, planned_work: plannedWork, omitted_planned_work_count: Math.max(0, todoIds.length - plannedWork.length), @@ -331,6 +390,29 @@ function ordinaryWorkEvidence(follow: JsonObject): JsonObject[] { return evidence; } +function validateWorkResultUse(follow: JsonObject): void { + if (follow.work_result_use === undefined) return; + const use = requireJsonObject(follow.work_result_use, "work result use"); + const pending = objects(use.unresolved, "unresolved work evidence"); + const links = objects(use.recorded_followthrough, "recorded work followthrough"); + if (["unresolved_count", "omitted_unresolved_count", "recorded_followthrough_count", "omitted_followthrough_count"] + .some(key => !Number.isSafeInteger(use[key]) || Number(use[key]) < 0) + || pending.length > 6 || links.length > 6 + || Number(use.unresolved_count) !== pending.length + Number(use.omitted_unresolved_count) + || Number(use.recorded_followthrough_count) !== links.length + Number(use.omitted_followthrough_count)) { + throw new EffectRuntimeRequestError("work result use requires bounded source relations and complete counts"); + } + for (const row of pending) { + for (const key of ["evidence_ref", "todo_id", "source_decision_ref"]) identifier(row[key], key); + requireNonEmptyString(row.generated_at, "unresolved generated_at"); + } + for (const row of links) for (const key of ["result_evidence_ref", "decision_ref", "work_todo_id", "work_evidence_ref"]) identifier(row[key], key); + if (new Set(pending.map(row => row.evidence_ref)).size !== pending.length + || new Set(links.map(row => row.result_evidence_ref)).size !== links.length) { + throw new EffectRuntimeRequestError("work result use requires distinct source results"); + } +} + export function validateReplanContext(value: unknown): JsonObject { const context = requireJsonObject(value, "replan_context"); if (context.schema_version !== CONTEXT_SCHEMA) { @@ -361,6 +443,7 @@ export function validateReplanContext(value: unknown): JsonObject { throw new EffectRuntimeRequestError("decision followthrough requires the enabled strategy and unknown utility"); } const observations = objects(follow.observations, "decision_followthrough.observations"); + validateWorkResultUse(follow); const ordinary = ordinaryWorkEvidence(follow); if (observations.length > 6 || typeof follow.source_history_complete !== "boolean" || ["recorded_progress_count", "omitted_observation_count", "unsequenced_progress_count"].some( diff --git a/tests/control_plane/test_effective_turn_replan.py b/tests/control_plane/test_effective_turn_replan.py index 348c5cdde7..57cd117791 100644 --- a/tests/control_plane/test_effective_turn_replan.py +++ b/tests/control_plane/test_effective_turn_replan.py @@ -2,6 +2,8 @@ import json import shlex +import subprocess +import sys from pathlib import Path from datetime import datetime, timedelta, timezone @@ -312,7 +314,7 @@ def test_due_stride_review_survives_successor_transition_until_decision( isolate_sqlite_runtime(tmp_path, monkeypatch) project, runtime, registry = _write_fixture(tmp_path) data = json.loads(registry.read_text()) - data["goals"][0]["quota"]["allowed_slots"] = 8 + data["goals"][0]["quota"]["allowed_slots"] = 12 registry.write_text(json.dumps(data)) rc, listed = _run_cli(registry, runtime, "todo", "list", "--goal-id", GOAL_ID) assert rc == 0, listed @@ -455,10 +457,11 @@ def call(*args: str, expected_code: int = 0) -> dict: "--scan-path", str(project)) # Synthetic candidate work under the real selected Todo. A worse # result is still evidence; it must not be rewritten into goal gain. - (project / f"candidate-result-{index}.json").write_text(json.dumps({ - "reference_error": sum(value * value for value in (1, 1)), - "candidate_error": sum(value * value for value in (1, 2)), - })) + subprocess.run([sys.executable, "-c", + "import json,sys; from pathlib import Path; " + "Path(sys.argv[1]).write_text(json.dumps({'reference_error':sum(x*x for x in (1,1))," + "'candidate_error':sum(x*x for x in (1,2))}))", + str(project / f"candidate-result-{index}.json")], check=True) work = call("refresh-state", "--goal-id", GOAL_ID, "--agent-id", AGENT_ID, "--todo-id", successor["todo_id"], "--turn-instance-id", turn, "--classification", "validated_progress", "--delivery-batch-scale", "implementation", @@ -470,6 +473,8 @@ def call(*args: str, expected_code: int = 0) -> dict: call("quota", "spend-slot", "--goal-id", GOAL_ID, "--agent-id", AGENT_ID, "--todo-id", successor["todo_id"], "--turn-instance-id", turn, "--slots", "1", "--source", "heartbeat", "--execute") + call("todo", "complete", "--goal-id", GOAL_ID, "--agent-id", AGENT_ID, + "--todo-id", successor["todo_id"]) next_guard = call("quota", "should-run", "--codex-app", "--goal-id", GOAL_ID, "--agent-id", AGENT_ID, "--turn-instance-id", "stride-next-review", "--scan-path", str(project)) @@ -480,7 +485,8 @@ def call(*args: str, expected_code: int = 0) -> dict: assert "regressed (error 5 versus reference 2)" in observed_result["recommended_action"] # The actor can now discover the actual ordinary result from the review, # retain the reference, and choose a successor through the existing owner. - next_task = "Compare another source while retaining the reference" if "regressed" in observed_result["recommended_action"] else "Adopt the candidate" + measurement = json.loads((project / "candidate-result-1.json").read_text()) + next_task = "Compare another source while retaining the reference" if measurement["candidate_error"] > measurement["reference_error"] else "Adopt the candidate" next_successor = call("todo", "add", "--goal-id", GOAL_ID, "--role", "agent", "--claimed-by", AGENT_ID, "--text", next_task, "--task-class", "advancement_task", "--action-kind", "validate", @@ -502,6 +508,7 @@ def call(*args: str, expected_code: int = 0) -> dict: planned = follow["planned_work"] assert len(planned) == 1 and planned[0]["todo_id"] == successor["todo_id"] assert planned[0]["current"]["claimed_by"] == AGENT_ID + assert planned[0]["current"]["status"] == "done", "A negative experiment can end while the Goal continues" assert len(planned[0]["observation_refs"]) == 1 assert follow["unresolved_planned_work_count"] == 1 last_work = json.loads(Path(work["json_path"]).read_text()) @@ -523,6 +530,7 @@ def call(*args: str, expected_code: int = 0) -> dict: assert resolved["evidence"]["stride_decision"] == decision next_decision = {**decision, "reference_context_id": next_control["reference_context_id"], "reference_obligation_id": next_obligation["obligation_id"], "action": "switch"} + vision_packet = {**vision_packet, "todo_delta": [f"create:{next_successor['todo_id']}"]} refresh_args = ("refresh-state", "--goal-id", GOAL_ID, "--agent-id", AGENT_ID, "--replan-obligation-id", next_obligation["obligation_id"], "--turn-instance-id", "stride-next-review", "--agent-vision-json", str(decision_path), "--classification", "bounded_replan_progress", @@ -546,6 +554,38 @@ def call(*args: str, expected_code: int = 0) -> dict: assert next_accepted["autonomous_replan_ack"]["semantic_delta"]["accepted"] is True replay = call(*refresh_args) assert replay.get("appended") is not True + # A review/citation alone leaves this result pending. Actual subsequent + # work under the declared next Todo supplies the recorded association. + call("quota", "spend-slot", "--goal-id", GOAL_ID, "--agent-id", AGENT_ID, + "--replan-obligation-id", next_obligation["obligation_id"], "--turn-instance-id", "stride-next-review", + "--slots", "1", "--source", "heartbeat", "--execute") + for index in range(2): + turn = f"adopted-probe-{index}" + guard = call("quota", "should-run", "--codex-app", "--goal-id", GOAL_ID, + "--agent-id", AGENT_ID, "--todo-id", next_successor["todo_id"], "--turn-instance-id", turn, + "--scan-path", str(project)) + assert guard["selected_todo"]["todo_id"] == next_successor["todo_id"] + subprocess.run([sys.executable, "-c", + "import json,sys; from pathlib import Path; " + "old=json.loads(Path(sys.argv[1]).read_text()); " + "Path(sys.argv[2]).write_text(json.dumps({'retained_error':old['reference_error'],'alternative_error':1}))", + str(project / "candidate-result-1.json"), str(project / f"adopted-probe-{index}.json")], check=True) + call("refresh-state", "--goal-id", GOAL_ID, "--agent-id", AGENT_ID, + "--todo-id", next_successor["todo_id"], "--turn-instance-id", turn, + "--classification", "validated_progress", "--delivery-batch-scale", "implementation", + "--delivery-outcome", "outcome_progress", "--delivery-boundary", "in_flight_continuation", + "--recommended-action", "The alternate probe has an attributable artifact; validate transfer before replacing the incumbent.", + "--no-global-sync", "--suppress-external-sinks") + call("quota", "spend-slot", "--goal-id", GOAL_ID, "--agent-id", AGENT_ID, + "--todo-id", next_successor["todo_id"], "--turn-instance-id", turn, + "--slots", "1", "--source", "heartbeat", "--execute") + final = call("quota", "should-run", "--codex-app", "--goal-id", GOAL_ID, + "--agent-id", AGENT_ID, "--turn-instance-id", "review-adopted-probe", "--scan-path", str(project)) + use = final["autonomous_replan_obligation"]["replan_context"]["decision_followthrough"]["work_result_use"] + assert any(row["result_evidence_ref"] == ordinary_ref and row["work_todo_id"] == next_successor["todo_id"] + for row in use["recorded_followthrough"]) + assert final["autonomous_replan_obligation"]["replan_context"]["decision_followthrough"]["execution_and_goal_gain"] == "unknown" + assert json.loads((project / "adopted-probe-1.json").read_text())["retained_error"] == 2 @pytest.mark.parametrize("invalid", [0, 7, True, 2.5, "2"]) diff --git a/tests/control_plane_ts/replan_context.test.ts b/tests/control_plane_ts/replan_context.test.ts index 38bf52307e..2dd917d11a 100644 --- a/tests/control_plane_ts/replan_context.test.ts +++ b/tests/control_plane_ts/replan_context.test.ts @@ -131,6 +131,36 @@ function subsequent(position: number): JsonObject { const strideRequest = {...request, stride_strategy: "evidence_conditioned_v1"}; +test("unconsumed result survives later reviews and citations alone; recorded work closes only its source relation", () => { + const first = reviewed(1); + (first._source_facts as JsonObject).planned_todo_ids = ["todo-a"]; + const result = {...subsequent(2), progress_observation: null}; + const ref = (projectReplanContext({...request, rows: [result]}).evidence as JsonObject[])[0].evidence_ref; + const next = reviewed(3); + (next._source_facts as JsonObject).planned_todo_ids = ["todo-b"]; + const vision = (next._source_facts as JsonObject).agent_vision as JsonObject; + (vision.path_delta as JsonObject).evidence_refs = [ref]; + const readUse = (rows: JsonObject[]) => (projectReplanContext({...strideRequest, rows}) + .decision_followthrough as JsonObject).work_result_use as JsonObject; + const proposed = readUse([first, result, next]); + assert.equal(proposed.unresolved_count, 1); + assert.equal((proposed.unresolved as JsonObject[])[0].evidence_ref, ref); + assert.deepEqual(proposed.recorded_followthrough, []); + const actual = {...subsequent(4), todo_id: "todo-b", progress_observation: null}; + const adopted = readUse([first, result, next, actual]); + assert.equal(adopted.unresolved_count, 1, "the next result remains available for the next decision"); + assert.notEqual((adopted.unresolved as JsonObject[])[0].evidence_ref, ref); + const links = adopted.recorded_followthrough as JsonObject[]; + assert.equal(links.length, 1); assert.equal(links[0].result_evidence_ref, ref); + assert.equal(links[0].work_todo_id, "todo-b"); + assert.deepEqual(readUse([first, result, next, actual, actual]), adopted, "replay cannot consume or duplicate twice"); + const replayedResult = {...result, _source_facts: {...result._source_facts as JsonObject, history_position: 5}}; + assert.deepEqual(readUse([first, result, next, actual, replayedResult]), adopted, "a lost-response replay cannot resurrect consumed evidence"); + const omitted = reviewed(3); + (omitted._source_facts as JsonObject).planned_todo_ids = ["todo-b"]; + assert.equal(readUse([first, result, omitted]).unresolved_count, 1, "another review cannot silently drop an uncited result"); +}); + type OrdinaryFollowthrough = { recorded_progress_count: number; execution_and_goal_gain: string; observations: JsonObject[]; previous_decision: JsonObject; From 8d7d4aad5b1c96f917e5f1714ad983c9138f9722 Mon Sep 17 00:00:00 2001 From: song Date: Sat, 17 Oct 2026 14:42:12 +0800 Subject: [PATCH 22/22] test(benchmark): bind capture assertions to one phase deadline Keep the existing strict capture upper bound but stop creating a later five-second phase at invocation. Scheduling delay between the two test clock samples is not a product budget extension. Signed-off-by: song --- benchmark/tests/test_checkpoint_handoff.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/benchmark/tests/test_checkpoint_handoff.py b/benchmark/tests/test_checkpoint_handoff.py index b1000a33e9..40ffb5cae7 100644 --- a/benchmark/tests/test_checkpoint_handoff.py +++ b/benchmark/tests/test_checkpoint_handoff.py @@ -406,14 +406,18 @@ def artifact(method, params, **kwargs): assert params["expected_source_sha256"] == "witness" return {"sha256": "sealed"} monkeypatch.setattr("benchmark.runtime.checkpoint_wake.effect_runtime_result", artifact) + # Bind one monotonic phase before deriving its wall-clock upper bound. + # Starting a second five-second phase at the call can add scheduler delay + # and incorrectly look like the implementation extended the original one. + capture_deadline = time.monotonic() + 5 phase_deadline = time.time() + 5 if fault: with pytest.raises(ValueError, match="failure|cursor moved|source changed"): - hook._capture(held, time.monotonic() + 5) + hook._capture(held, capture_deadline) assert events[-1] == "closed" assert ("capture" in events) is (fault == "capture") else: - assert hook._capture(held, time.monotonic() + 5)["sha256"] == "sealed" + assert hook._capture(held, capture_deadline)["sha256"] == "sealed" assert events == ["open", "read", "probe", "read", "capture", "closed"]