From dc5aeb86e1ac93a9053285408d44a388837eba8d Mon Sep 17 00:00:00 2001 From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> Date: Thu, 5 Nov 2026 20:02:17 +0800 Subject: [PATCH] Route staged replan reviews through the current Todo selection Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> --- benchmark/runtime/checkpoint.py | 2 +- docs/reference/protocols/turn-envelope-v0.md | 6 +++ loopx/cli_commands/quota_capture.py | 3 +- loopx/control_plane/quota/turn_envelope.ts | 12 +++-- .../control_plane/test_replan_staged_read.py | 51 +++++++++++++++++++ 5 files changed, 69 insertions(+), 5 deletions(-) diff --git a/benchmark/runtime/checkpoint.py b/benchmark/runtime/checkpoint.py index 677aed262c..164dd0f010 100644 --- a/benchmark/runtime/checkpoint.py +++ b/benchmark/runtime/checkpoint.py @@ -257,7 +257,7 @@ def capture_before_replan(env: dict[str, str], receipt: dict[str, Any], body: st view_path.write_text(guard_run.stdout, encoding="utf-8") view_path.chmod(0o600) reading = (f"Read the complete CLI view at {view_path} before work, then follow its interaction_contract " - "and replan review/writeback reads when present. ") + "and the next step in the replan review when present. ") if env.get("LOOPX_CONTEXT_DELIVERY") == "on" and env.get("LOOPX_CODEX_CONTEXT_DIR"): from .context_delivery import preflight_context_instruction diff --git a/docs/reference/protocols/turn-envelope-v0.md b/docs/reference/protocols/turn-envelope-v0.md index 35275f91e3..aae8fbbecb 100644 --- a/docs/reference/protocols/turn-envelope-v0.md +++ b/docs/reference/protocols/turn-envelope-v0.md @@ -1034,6 +1034,12 @@ identity-less and delivery-blocked until the agent explicitly chooses. The compact envelope does not truncate those executable commands into unusable strings. It carries non-exhaustive `writeback.suggested_todo_ids` plus `selection_command_ref`; the full decision remains the authority for exact argv. +When a captured heartbeat replan still requires a choice, the review's +`next.selection_command_ref` points into the current guard's CLI channel. Use +that selection command with the chosen eligible Todo. It carries the caller's +context delivery settings into the same Turn. Read the writeback format from +the newly bound capture. A replan already bound to a Todo or a standalone +replan obligation goes directly from review to its captured writeback reader. Packet-bearing historical decisions and `contract_capsule.protocol_action_packet` witnesses are unsupported under [the retirement contract](protocol-action-packet-decision-v0.md). diff --git a/loopx/cli_commands/quota_capture.py b/loopx/cli_commands/quota_capture.py index dbdabeacb9..72a4a89113 100644 --- a/loopx/cli_commands/quota_capture.py +++ b/loopx/cli_commands/quota_capture.py @@ -948,7 +948,8 @@ def bind_capture_selection_transport(payload: dict[str, Any], args: argparse.Nam options["--decision-output-root"] = str(Path(selected_root).expanduser().absolute()) if getattr(args, "decision_handoff_socket", None): options["--decision-handoff-socket"] = args.decision_handoff_socket - if getattr(args, "context_delivery", "off") != "off": + # Selection is part of the same Turn, including a caller's explicit opt-out. + if getattr(args, "context_delivery", None) is not None: options["--context-delivery"] = args.context_delivery if getattr(args, "delta_delivery", None) is not None: options["--delta-delivery"] = args.delta_delivery diff --git a/loopx/control_plane/quota/turn_envelope.ts b/loopx/control_plane/quota/turn_envelope.ts index 508e710673..f2eff52e9a 100644 --- a/loopx/control_plane/quota/turn_envelope.ts +++ b/loopx/control_plane/quota/turn_envelope.ts @@ -550,6 +550,7 @@ function replanView(request: JsonObject): JsonObject { } if (request.stage !== "review") throw new EffectRuntimeRequestError("unsupported replan read stage"); const {core, horizon, acceptanceIndex} = replanGoalContext(payload, reads); + const cli = object(object(payload.interaction_contract).cli_channel); // Index every captured outcome, including differing context/horizon revisions. // Full-content equality alone shares a row; identical IDs do not erase edits. const coverage = Array.isArray(context.coverage_ledger) ? context.coverage_ledger.map(object) : []; @@ -586,7 +587,12 @@ function replanView(request: JsonObject): JsonObject { uncovered_frontier: context.uncovered_frontier, // Facts and their provenance are the review input. Do not preselect a // diagnostic method, candidate route or judgment from observation counts. - next: {writeback: reads?.writeback ?? null}, + // A suggested Todo can already be present before the guard binds it. + // Follow the entry's command: its host may already have bound the current + // presentation options after this immutable decision was captured. + next: cli.selection_required === true + ? {selection_required: true, selection_command_ref: "$.interaction_contract.cli_channel.selection_command"} + : {writeback: reads?.writeback ?? null}, }; } @@ -595,7 +601,7 @@ function replanActionPacket(payload: JsonObject, reads = replanReadCommands(payl if (Object.keys(source).length === 0) return null; if (reads) return {...compactFields(source, ["schema_version", "decision", "obligation_id", "required_outcome", "allowed_terminal"]), review: reads.review, writeback: reads.writeback, - guidance: "Read review state and applicable acceptance/evidence; choose the next work and route before reading writeback for its format. Reads grant no authority and require no acknowledgement."}; + guidance: "Read review state and applicable acceptance/evidence; choose the next work, then follow review.next for Todo selection or writeback. Reads grant no authority and require no acknowledgement."}; const compact = compactFields(source, [ "schema_version", "decision", "obligation_id", "uncovered_frontier", "required_outcome", "allowed_terminal", "bounded_frontier", "settlement_only", "successor_todo_id", "stride_control", @@ -904,7 +910,7 @@ function actionProjection(payload: JsonObject, deferSettlement = true): JsonObje const recommendedAction = replanPacket ? replanSettlementOnly ? scalarString(object(object(payload.replan_action_packet).writeback_contract).rule, "replan settlement rule") - : replanReadCommands(payload) ? "read replan_action_packet.review, decide from state, then read writeback" + : replanReadCommands(payload) ? "read replan_action_packet.review, decide from state, then follow review.next" : "apply replan_action_packet and emit one required semantic outcome" : text(turn.observation.recommended_action || payload.recommended_action, 480); // The signed host action is executable authority, not a display summary. diff --git a/tests/control_plane/test_replan_staged_read.py b/tests/control_plane/test_replan_staged_read.py index c5da60a849..9a53c8b85a 100644 --- a/tests/control_plane/test_replan_staged_read.py +++ b/tests/control_plane/test_replan_staged_read.py @@ -293,6 +293,57 @@ def observe(command, **kwargs): restart_effect_runtime(wait_for_exit=True) +@pytest.mark.parametrize("mode", ["off", "on"]) +def test_review_routes_to_selection_before_the_bound_writer(tmp_path, monkeypatch, mode): + tmp_path = tmp_path.resolve() + isolate_sqlite_runtime(tmp_path, monkeypatch) + fixture = make_progress_cli_fixture(tmp_path, "sqlite", "advanced") + captures, native = tmp_path / "captures", tmp_path / "native" + captures.mkdir(mode=0o700) + native.mkdir(mode=0o700) + pinned_cli = str(Path(sys.executable).with_name("loopx")) + monkeypatch.setenv("LOOPX_CONTEXT_DELIVERY", "on") + + def invoke(argv): + return subprocess.run(argv, cwd=fixture["project"], capture_output=True, text=True, timeout=60) + + try: + entry = call_progress_cli(fixture, "--cli-bin", pinned_cli, "quota", "should-run", + "--codex-app", "--goal-id", GOAL, "--agent-id", AGENT, "--turn-instance-id", TURN, + "--context-delivery", mode, "--delta-delivery", "auto" if mode == "on" else "off", + "--decision-output-root", str(captures), + "--codex-context-dir", str(native), *(["--defer-context-presentation"] if mode == "on" else [])) + review_argv = shlex.split(entry["replan_action_packet"]["review"]) + review = json.loads(invoke(review_argv).stdout) + assert review["next"]["selection_required"] is True and "writeback" not in review["next"] + selection = entry + for key in review["next"]["selection_command_ref"].removeprefix("$.").split("."): + selection = selection[key] + selected = shlex.split(selection["route_prefix"]) + shlex.split(selection["command_args_template"]) + selected[selected.index("{todo_id}")] = TODO + result = invoke(selected) + assert result.returncode == 0, result.stdout + view = json.loads(result.stdout) + if mode == "on": + assert view["schema_version"] == "context_capture_navigation_v1" + else: + assert view["selected_todo"]["todo_id"] == TODO + assert view["decision"] == "autonomous_replan_required" + + # Follow the new observation. A recommendation on the initial capture + # can already name the same Todo without owning this settlement. + decisions = (json.loads(path.read_bytes()) for path in captures.glob("*/decision.json")) + bound = next(item for item in decisions if item["interaction_contract"]["cli_channel"].get("settlement_plan")) + bound_packet = _turn_envelope_result("replan_cli_view", payload=bound)["replan_action_packet"] + bound_review = json.loads(invoke(shlex.split(bound_packet["review"])).stdout) + assert bound_review["source_hash"] != review["source_hash"] + writer = json.loads(invoke(shlex.split(bound_review["next"]["writeback"])).stdout) + identity = writer["writeback"]["settlement_plan"]["identity"] + assert (identity["todo_id"], identity["turn_instance_id"]) == (TODO, TURN) + finally: + restart_effect_runtime(wait_for_exit=True) + + def test_review_retains_scoped_dependencies_and_unknown_outcomes(): source = _quota_payload() source["replan_read_path"] = "/synthetic/decision.json"