mirror of
https://github.com/rennf93/roboco.git
synced 2026-08-03 07:23:24 +02:00
fix(gateway): unclaim releases a pending-assigned task — escape trap (#176)
An agent assigned a `pending` task it never claimed was structurally
trapped: from pending-assigned, unclaim returned None ("cannot unclaim
from status pending"), i_am_idle rejected ("assigned but never claimed"),
i_am_blocked rejected ("block requires in_progress"). Any persistent
claim-time rejection (a gate the agent cannot satisfy, a transient
validation error) therefore looped the agent until budget-reap AND left
the task orphaned (pending, assigned, no progress). Observed in smoke-16
and smoke-17.
unclaim_for_agent now releases a pending task assigned to the caller:
no status change (already pending → no lifecycle transition, no
WorkSession to abandon since it was never claimed), just clear
assigned_to/active_claimant_id so the dispatcher can reassign. The
choreographer spec gate already permits unclaim from pending (composes=()
— role-only), so the service branch is the whole fix. Updated the now-
stale unclaim remediate string; rewrote the test that encoded the buggy
trap and added a paused-status negative case.
This commit is contained in:
@@ -247,14 +247,36 @@ async def test_unclaim_for_agent_returns_none_when_wrong_assignee(
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_unclaim_for_agent_returns_none_when_not_claimed(
|
||||
async def test_unclaim_for_agent_releases_pending_assignment(
|
||||
task_setup: dict, db_session: AsyncSession
|
||||
) -> None:
|
||||
"""#176: an agent assigned a pending task it never claimed must be
|
||||
able to unclaim it (escape the pending-assigned trap). The row stays
|
||||
pending; only the assignment is released so the dispatcher can
|
||||
reassign it instead of orphaning the task + looping the agent."""
|
||||
svc = task_setup["svc"]
|
||||
task = await svc.create(_req(task_setup))
|
||||
task.assigned_to = task_setup["agent_id"]
|
||||
await db_session.flush()
|
||||
# status is PENDING, not CLAIMED/IN_PROGRESS
|
||||
# status is PENDING, never claimed — the smoke-17 trap scenario.
|
||||
out = await svc.unclaim_for_agent(task.id, agent_id=task_setup["agent_id"])
|
||||
assert out is not None
|
||||
assert out.status == TaskStatus.PENDING
|
||||
assert out.assigned_to is None
|
||||
assert out.active_claimant_id is None
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_unclaim_for_agent_returns_none_when_paused(
|
||||
task_setup: dict, db_session: AsyncSession
|
||||
) -> None:
|
||||
"""A non-pending/claimed/in_progress status (e.g. paused — which has
|
||||
`resume`, not `unclaim`) is still not unclaimable."""
|
||||
svc = task_setup["svc"]
|
||||
task = await svc.create(_req(task_setup))
|
||||
task.status = TaskStatus.PAUSED
|
||||
task.assigned_to = task_setup["agent_id"]
|
||||
await db_session.flush()
|
||||
assert await svc.unclaim_for_agent(task.id, agent_id=task_setup["agent_id"]) is None
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user