From ced5d763dc6c00138b64593267e1e3bc5713c010 Mon Sep 17 00:00:00 2001 From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> Date: Mon, 5 Oct 2026 06:18:52 +0800 Subject: [PATCH] fix(quota): honor explicit selection for host-owned turns Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> --- docs/quota-allocation.md | 6 ++ loopx/cli_commands/quota_action_selection.py | 5 +- .../test_quota_settlement_cli.py | 83 +++++++++++++++++-- 3 files changed, 85 insertions(+), 9 deletions(-) diff --git a/docs/quota-allocation.md b/docs/quota-allocation.md index ed325557b3..bb65603a73 100644 --- a/docs/quota-allocation.md +++ b/docs/quota-allocation.md @@ -578,6 +578,12 @@ empty results; missing/stale display and provider failure never authorize a Markdown fallback. The guard does not append a second Markdown candidate list. Historical receipt-bound recovery remains separate from new work admission. +Hosts supplying their own `--turn-instance-id` use this same selection contract, +including `generic_cli`, `outer_controller`, and native CLI profiles. An explicit +`--todo-id` is qualified before receipt binding; it must not leave the response +asking for the selection it just accepted. This does not enable `--begin-turn` +for those hosts or change legacy calls without a Turn identity. + A single-candidate response keeps the direct execution path and does not add an extra selection round trip. diff --git a/loopx/cli_commands/quota_action_selection.py b/loopx/cli_commands/quota_action_selection.py index 749737e23a..36ffb5b565 100644 --- a/loopx/cli_commands/quota_action_selection.py +++ b/loopx/cli_commands/quota_action_selection.py @@ -68,7 +68,10 @@ def _requested_quota_action_todo_id( args: argparse.Namespace, ) -> str | None: if not ( - bool(args.codex_app) + # Hosts may supply their own Turn identity without guided-start support. + # Forward their explicit selection to the same typed admission owner. + bool(getattr(args, "turn_instance_id", None)) + or bool(args.codex_app) or bool(getattr(args, "trae_app", False)) or args.runtime_profile in {profile.value for profile in GUIDED_START_TURN_RUNTIME_PROFILES} diff --git a/tests/control_plane/test_quota_settlement_cli.py b/tests/control_plane/test_quota_settlement_cli.py index 5034c35904..e4864e3722 100644 --- a/tests/control_plane/test_quota_settlement_cli.py +++ b/tests/control_plane/test_quota_settlement_cli.py @@ -3567,6 +3567,69 @@ def test_begin_turn_rejects_a_non_receipt_runtime_profile(tmp_path: Path) -> Non ) +@pytest.mark.parametrize("profile", ["generic_cli", "outer_controller", "codex_cli"]) +def test_host_owned_turn_executes_projected_selection_and_replays_identity( + tmp_path: Path, profile: str, +) -> None: + project, runtime, registry_path = _write_fixture(tmp_path) + _configure_selectable_alternative(project) + guard_args = ( + "quota", "should-run", "--runtime-profile", profile, + "--goal-id", GOAL_ID, "--agent-id", AGENT_ID, + "--turn-instance-id", TURN_ID, "--scan-path", str(project), + ) + first_rc, first = _run_cli(registry_path, runtime, *guard_args) + assert first_rc == 0, first + assert first["interaction_contract"]["agent_channel"]["selection_required"] is True + selection = first["interaction_contract"]["cli_channel"]["selection_command"] + command = selection["route_prefix"] + " " + selection[ + "command_args_template" + ].replace("{todo_id}", ALTERNATIVE_TODO_ID) + selected_rc, selected = _run_generated_cli(command, registry_path=registry_path) + assert selected_rc == 0, selected + assert selected["interaction_contract"]["agent_channel"]["delivery_allowed"] is True + assert selected["selected_todo"]["todo_id"] == ALTERNATIVE_TODO_ID + identity = selected["heartbeat_receipt"]["settlement_identity"] + assert identity["todo_id"] == ALTERNATIVE_TODO_ID + assert identity["turn_instance_id"] == TURN_ID + cli_channel = selected["interaction_contract"]["cli_channel"] + if profile == "outer_controller": + # Settlement stays with the controller, not the inner agent. + assert "settlement_plan" not in cli_channel + else: + assert cli_channel["settlement_plan"]["identity"] == identity + + replay_rc, replay = _run_cli(registry_path, runtime, *guard_args) + assert replay_rc == 0, replay + assert replay["interaction_contract"]["agent_channel"]["delivery_allowed"] is True + assert replay["heartbeat_receipt"]["settlement_identity"] == identity + conflict_rc, conflict = _run_cli( + registry_path, runtime, *guard_args, "--todo-id", TODO_ID, + ) + assert conflict_rc != 0, conflict + assert _heartbeat_receipt_count(runtime, TURN_ID) == 2 + if profile == "outer_controller": + return + + refresh_rc, refresh = _run_cli( + registry_path, runtime, "refresh-state", "--goal-id", GOAL_ID, + "--agent-id", AGENT_ID, "--todo-id", ALTERNATIVE_TODO_ID, + "--turn-instance-id", TURN_ID, "--classification", "validated_progress", + "--delivery-batch-scale", "implementation", "--delivery-outcome", "outcome_progress", + "--delivery-boundary", "in_flight_continuation", + "--no-global-sync", "--suppress-external-sinks", + ) + assert refresh_rc == 0, refresh + spend_command = refresh["settlement_owed"]["command"] + spend_rc, spend = _run_cli(registry_path, runtime, *shlex.split(spend_command)[1:]) + assert spend_rc == 0, spend + assert spend["settlement_identity"] == identity + retry_rc, retry = _run_cli(registry_path, runtime, *shlex.split(spend_command)[1:]) + assert retry_rc == 0, retry + assert retry["appended"] is False + assert _spend_run_count(runtime) == 1 + + def test_agent_can_select_eligible_todo_outside_bounded_suggestions( tmp_path: Path, ) -> None: @@ -3753,14 +3816,15 @@ def test_same_turn_can_select_eligible_todo_created_after_unbound_receipt( assert _heartbeat_receipt_count(runtime, turn_instance_id) == 2 -def test_agent_selection_rejects_unprojected_todo(tmp_path: Path) -> None: +@pytest.mark.parametrize("profile", ["codex_app_heartbeat", "generic_cli"]) +def test_agent_selection_rejects_unprojected_todo(tmp_path: Path, profile: str) -> None: project, runtime, registry_path = _write_fixture(tmp_path) _configure_selectable_alternative(project) turn_instance_id = "turn-agent-selection-unprojected" guard_args = ( "quota", "should-run", - "--codex-app", + "--runtime-profile", profile, "--goal-id", GOAL_ID, "--agent-id", @@ -3798,8 +3862,9 @@ def test_agent_selection_rejects_unprojected_todo(tmp_path: Path) -> None: assert _heartbeat_receipt_count(runtime, turn_instance_id) == 1 +@pytest.mark.parametrize("profile", ["codex_app_heartbeat", "generic_cli"]) def test_unsuggested_selection_revalidates_current_capability_readiness( - tmp_path: Path, + tmp_path: Path, profile: str, ) -> None: project, runtime, registry_path = _write_fixture(tmp_path) _configure_selectable_alternative(project) @@ -3817,7 +3882,7 @@ def test_unsuggested_selection_revalidates_current_capability_readiness( guard_args = ( "quota", "should-run", - "--codex-app", + "--runtime-profile", profile, "--goal-id", GOAL_ID, "--agent-id", @@ -3847,8 +3912,9 @@ def test_unsuggested_selection_revalidates_current_capability_readiness( assert _heartbeat_receipt_count(runtime, turn_instance_id) == 1 +@pytest.mark.parametrize("profile", ["codex_app_heartbeat", "generic_cli"]) def test_first_call_rejected_selection_does_not_commit_a_false_receipt( - tmp_path: Path, + tmp_path: Path, profile: str, ) -> None: project, runtime, registry_path = _write_fixture(tmp_path) turn_instance_id = "turn-agent-selection-first-call-rejected" @@ -3858,7 +3924,7 @@ def test_first_call_rejected_selection_does_not_commit_a_false_receipt( runtime, "quota", "should-run", - "--codex-app", + "--runtime-profile", profile, "--goal-id", GOAL_ID, "--agent-id", @@ -3884,8 +3950,9 @@ def test_first_call_rejected_selection_does_not_commit_a_false_receipt( assert _heartbeat_receipt_count(runtime, turn_instance_id) == 0 +@pytest.mark.parametrize("profile", ["codex_app_heartbeat", "generic_cli"]) def test_first_call_agent_selection_is_qualified_before_receipt_commit( - tmp_path: Path, + tmp_path: Path, profile: str, ) -> None: project, runtime, registry_path = _write_fixture(tmp_path) _configure_selectable_alternative(project) @@ -3896,7 +3963,7 @@ def test_first_call_agent_selection_is_qualified_before_receipt_commit( runtime, "quota", "should-run", - "--codex-app", + "--runtime-profile", profile, "--goal-id", GOAL_ID, "--agent-id",