Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion benchmark/runtime/checkpoint.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
6 changes: 6 additions & 0 deletions docs/reference/protocols/turn-envelope-v0.md
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand Down
3 changes: 2 additions & 1 deletion loopx/cli_commands/quota_capture.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
12 changes: 9 additions & 3 deletions loopx/control_plane/quota/turn_envelope.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) : [];
Expand Down Expand Up @@ -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},
};
}

Expand All @@ -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",
Expand Down Expand Up @@ -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.
Expand Down
51 changes: 51 additions & 0 deletions tests/control_plane/test_replan_staged_read.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
Loading