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
8 changes: 8 additions & 0 deletions loopx/control_plane/quota/settlement_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,14 @@ def quota_rollout_settlement_binding(
packet is only a diagnostic fallback when no concrete Todo is selected.
"""

if payload.get("effective_action") == "unsettled_host_turn_recovery":
# This Turn only repairs the preceding Turn's closeout. A concurrently
# projected Todo or autonomous replan belongs to the post-recovery
# decision and must not become this receipt's settlement identity.
# Keeping the recovery receipt identity-less lets a verified same-Turn
# reentry bind the next independent action without rewriting history.
return None, None

explicit_todo_id = normalize_todo_id(getattr(args, "todo_id", None))
explicit_replan_obligation_id = normalize_todo_replan_obligation_id(
getattr(args, "replan_obligation_id", None)
Expand Down
30 changes: 29 additions & 1 deletion tests/control_plane/test_quota_settlement_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -1033,7 +1033,7 @@ def test_in_flight_progress_preserves_todo_across_heartbeat_settlements(
}


def test_next_heartbeat_forces_recovery_for_unsettled_must_attempt_turn(
def test_recovery_does_not_bind_current_replan_and_reenters_same_turn(
tmp_path: Path,
) -> None:
project, runtime, registry_path = _write_fixture(tmp_path)
Expand Down Expand Up @@ -1061,6 +1061,21 @@ def test_next_heartbeat_forces_recovery_for_unsettled_must_attempt_turn(
assert prior_rc == 0, prior
assert prior["heartbeat_receipt"]["closeout_required"] is True

state_path = project / f".codex/goals/{GOAL_ID}/ACTIVE_GOAL_STATE.md"
state_before_replan = state_path.read_text(encoding="utf-8")
replan_backlog = "\n".join(
f"- [ ] [P1] Review recovery breadth {index}.\n"
" <!-- loopx:todo "
f"todo_id=todo_recovery_replan_{index:012d} status=open "
"task_class=advancement_task action_kind=validate "
f"claimed_by={AGENT_ID} -->"
for index in range(15)
)
state_path.write_text(
state_before_replan.rstrip() + "\n\n" + replan_backlog + "\n",
encoding="utf-8",
)

recovery_rc, recovery = _run_cli(
registry_path,
runtime,
Expand All @@ -1077,6 +1092,7 @@ def test_next_heartbeat_forces_recovery_for_unsettled_must_attempt_turn(
)
assert recovery_rc == 0, recovery
assert recovery["effective_action"] == "unsettled_host_turn_recovery"
replan_obligation_id = recovery["replan_action_packet"]["obligation_id"]
packet = recovery["unsettled_host_turn_recovery"]
assert packet["prior_turn_instance_id"] == prior_turn_id
assert packet["binding_id"] == TODO_ID
Expand All @@ -1101,6 +1117,14 @@ def test_next_heartbeat_forces_recovery_for_unsettled_must_attempt_turn(
"next_cli_actions"
][1]
assert "settlement_identity" not in recovery["heartbeat_receipt"]
assert recovery["heartbeat_receipt"]["semantic_replan_obligation_id"] == (
replan_obligation_id
)

# Model the separately verified replan settlement without touching the
# append-only heartbeat receipt. The same recovery Turn must then be able
# to bind an independent successor instead of preserving an obsolete replan.
state_path.write_text(state_before_replan, encoding="utf-8")

wait_rc, wait = _run_cli(
registry_path,
Expand Down Expand Up @@ -1145,8 +1169,12 @@ def test_next_heartbeat_forces_recovery_for_unsettled_must_attempt_turn(
)
assert resumed_rc == 0, resumed
assert resumed["effective_action"] != "unsettled_host_turn_recovery"
assert resumed.get("autonomous_replan_obligation") is None
assert resumed["selected_todo"]["todo_id"] == ALTERNATIVE_TODO_ID
assert resumed["heartbeat_receipt"]["status"] == "upgraded"
assert resumed["heartbeat_receipt"]["settlement_identity"]["todo_id"] == (
ALTERNATIVE_TODO_ID
)


def test_standard_codex_app_settlement_is_receipted_and_idempotent(
Expand Down