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
5 changes: 4 additions & 1 deletion loopx/cli_commands/quota_action_selection.py
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,10 @@ def _requested_quota_action_selection_preflight(
QuotaActionSelectionConflictKind.CONFLICT,
requested_todo_id=requested_todo_id,
selected_todo_id=receipt_pending_action_todo_id,
qualification_state="retained_selection",
qualification_state=(
"retained_selection" if receipt_pending_action_todo_id else None
),
receipt_replan_obligation_id=receipt_bound_replan_obligation_id,
)
if admitted:
return None
Expand Down
7 changes: 7 additions & 0 deletions loopx/cli_commands/quota_failure_report.py
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,13 @@ def quota_failure_payload(
"agent_must_attempt": error.admission_must_attempt,
"delivery_allowed": error.admission_delivery_allowed,
}
if error.retained_selection:
selection_conflict["retained_selection"] = True
selection_conflict["retained_selection_todo_id"] = error.selected_todo_id
if error.receipt_replan_obligation_id:
selection_conflict["receipt_replan_obligation_id"] = (
error.receipt_replan_obligation_id
)
payload.update(
{
"reason": str(error),
Expand Down
45 changes: 43 additions & 2 deletions loopx/control_plane/quota/error_codes.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,8 +72,8 @@ class QuotaActionSelectionConflictError(RuntimeError):

A guard bound to a ``--todo-id`` has to agree with the current projection.
When it cannot, this error names what was requested, what the projection
currently selects, and what the caller should do next, so the failure is not
reported as an opaque quota collection failure.
selects or the Turn retains, and what the caller should do next, so the
failure is not reported as an opaque quota collection failure.
"""

error_code = "quota_action_selection_conflict"
Expand All @@ -89,6 +89,7 @@ def __init__(
unsettled_repair: str | None = None,
admission_must_attempt: bool | None = None,
admission_delivery_allowed: bool | None = None,
receipt_replan_obligation_id: str | None = None,
) -> None:
self.kind = kind
self.requested_todo_id = requested_todo_id
Expand All @@ -98,6 +99,12 @@ def __init__(
self.unsettled_repair = unsettled_repair
self.admission_must_attempt = admission_must_attempt
self.admission_delivery_allowed = admission_delivery_allowed
self.receipt_replan_obligation_id = receipt_replan_obligation_id
self.retained_selection = bool(
kind is QuotaActionSelectionConflictKind.CONFLICT
and receipt_replan_obligation_id
and selected_todo_id
)
if kind is QuotaActionSelectionConflictKind.UNQUALIFIED:
reason = (
"the current projection carries no typed action-selection "
Expand Down Expand Up @@ -135,6 +142,28 @@ def __init__(
else ""
)
)
elif self.retained_selection:
# The Turn's receipt is bound to an autonomous replan obligation, so
# the requested Todo cannot replace the selection that Turn already
# retains. The default conflict sentence calls that id "the
# projection's current selection", which is not what the caller is
# up against: name the retained selection and the obligation that
# owns the Turn.
reason = (
f"requested Todo {requested_todo_id or '(none)'} cannot replace "
"the retained pending selection "
f"{selected_todo_id or 'none'} on this Turn: the Turn's receipt is "
"bound to the autonomous replan obligation "
f"{receipt_replan_obligation_id or '(unnamed)'}, which owns its "
"settlement"
)
elif receipt_replan_obligation_id:
reason = (
f"requested Todo {requested_todo_id or '(none)'} cannot replace "
"this Turn's settlement identity: it is bound to the autonomous "
f"replan obligation {receipt_replan_obligation_id}, and no pending "
"Todo selection is retained"
)
else:
reason = (
f"requested Todo {requested_todo_id or '(none)'} is neither the "
Expand All @@ -148,6 +177,18 @@ def __init__(
"the prior Turn named in the reason is the blocker, settle that "
"Turn first and then rerun this Turn"
)
elif self.retained_selection:
self.recommended_action = (
"settle the autonomous replan obligation that owns this Turn, or "
"rerun `loopx quota should-run` without --todo-id to read the "
"selection the Turn retains; do not rebind the retained selection"
)
elif receipt_replan_obligation_id:
self.recommended_action = (
"settle the autonomous replan obligation that owns this Turn, "
"then start a fresh Turn and rerun `loopx quota should-run` to "
"select a Todo; do not rebind this Turn"
)
else:
self.recommended_action = (
"rerun `loopx quota should-run` without --todo-id to read the current "
Expand Down
49 changes: 49 additions & 0 deletions tests/control_plane/test_quota_action_selection_conflict.py
Original file line number Diff line number Diff line change
Expand Up @@ -230,3 +230,52 @@ def test_refused_delivery_boundary_is_published_as_a_typed_admission_fact() -> N
"agent_must_attempt": True,
"delivery_allowed": False,
}


def test_retained_selection_names_the_replan_obligation_that_owns_the_turn() -> None:
"""A receipt-bound replan Turn cannot hand its settlement to another Todo."""

retained_todo_id = "todo_retained_selection"
replan_obligation_id = "replan-retained-selection-fixture"
with pytest.raises(QuotaActionSelectionConflictError) as raised:
_requested_quota_action_selection_preflight(
_payload(selected_todo={"todo_id": retained_todo_id}),
requested_todo_id=REQUESTED_TODO_ID,
receipt_bound_todo_id=None,
receipt_bound_replan_obligation_id=replan_obligation_id,
receipt_pending_action_todo_id=retained_todo_id,
receipt_identity_upgraded=True,
)
error = raised.value

assert error.kind is QuotaActionSelectionConflictKind.CONFLICT
assert error.retained_selection is True
assert error.receipt_replan_obligation_id == replan_obligation_id
assert retained_todo_id in str(error)
assert replan_obligation_id in str(error)
assert "retained pending selection" in str(error)
assert "own" in str(error.recommended_action)

args = argparse.Namespace(
quota_command="should-run",
goal_id="quota-conflict-fixture",
agent_id="agent-fixture",
runtime_root=None,
verbose=False,
)
payload = quota_failure_payload(
args,
registry_path=Path("/tmp/quota-conflict-registry.json"),
runtime_root_arg=None,
error=error,
)

assert payload["action_selection_conflict"] == {
"kind": "conflict",
"requested_todo_id": REQUESTED_TODO_ID,
"selected_todo_id": retained_todo_id,
"qualification_state": "retained_selection",
"retained_selection": True,
"retained_selection_todo_id": retained_todo_id,
"receipt_replan_obligation_id": replan_obligation_id,
}
22 changes: 22 additions & 0 deletions tests/control_plane/test_selection_replan_reentry.py
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,20 @@ def test_deferred_selection_recovers_same_turn_and_settles_once(tmp_path, bindin
assert settled["heartbeat_receipt"]["settlement_identity"] == identity
rc, conflict = _run_cli(registry, runtime, *guard, "--todo-id", "todo_another_selection")
assert rc == 1 and conflict["ok"] is False
if binding == "autonomous_replan":
facts = conflict["action_selection_conflict"]
assert facts["receipt_replan_obligation_id"] == identity[
"replan_obligation_id"
]
if selection_deferred:
assert facts["retained_selection"] is True
assert facts["retained_selection_todo_id"] == selected_id
else:
assert "retained_selection" not in facts
assert "retained_selection_todo_id" not in facts
assert facts["qualification_state"] is None
assert "no pending Todo selection" in conflict["reason"]
assert "selection the Turn retains" not in conflict["recommended_action"]
assert _heartbeat_receipt_count(runtime, turn) == expected_after_resume
assert _spend_run_count(runtime) == 1

Expand Down Expand Up @@ -201,3 +215,11 @@ def test_reentry_never_replaces_retained_selection_with_recommended_todo(tmp_pat
"--replan-obligation-id" in action and "--todo-id" not in action
for action in resumed["interaction_contract"]["cli_channel"]["next_cli_actions"]
)
rc, conflict = _run_cli(
registry, runtime, *guard, "--todo-id", SELECTED_REPLAN_TODO_ID
)
assert rc == 1, conflict
facts = conflict["action_selection_conflict"]
assert facts["retained_selection"] is True
assert facts["retained_selection_todo_id"] == retained_todo_id
assert facts["receipt_replan_obligation_id"] == identity["replan_obligation_id"]
Loading