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
10 changes: 10 additions & 0 deletions changelog.d/tsk-y6x6s4-unquarantine-claimer.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
### Fixed

- **Un-quarantined cards return to a genuinely claimable pool**:
`unquarantine_task` set the card back to `open` but kept the old
`claimed_by`, and `claim_task` requires an unclaimed row -- so a
claimed-then-quarantined card came back permanently unclaimable.
Un-quarantine now clears the claimer, matching `reopen_task` and
`release_task`. The generic `update_task` edit path (owner/admin PATCH)
had the same gap when setting a claimed card's status back to `open`;
it now clears the claimer too.
30 changes: 30 additions & 0 deletions tests/projects/test_task_store.py
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,36 @@ async def test_reopen_task_is_noop_when_not_closed(store):
assert (await store.get_task(t["id"]))["status"] == "open"


@pytest.mark.asyncio
async def test_unquarantine_returns_claimed_task_to_claimable_pool(store):
t = await store.create_task(project_id="p", title="A", created_by="u")
await store.claim_task(t["id"], claimer_id="agent-1")
assert await store.quarantine_task(t["id"], actor="lead") is True
assert await store.unquarantine_task(t["id"], actor="lead") is True
back = await store.get_task(t["id"])
assert back["status"] == "open"
# unquarantined task must return to the claimable pool, so the old claimer clears
assert back["claimed_by"] is None
assert back["claimed_at"] is None
# the pool must be real: claim_task requires claimed_by IS NULL, so a stale
# claimer would leave the card open-but-unclaimable forever
assert await store.claim_task(t["id"], claimer_id="agent-2") is True


@pytest.mark.asyncio
async def test_update_task_status_open_returns_task_to_claimable_pool(store):
# Same class via the generic edit path: the owner/admin PATCH route can
# set status='open' on a claimed card directly through update_task.
t = await store.create_task(project_id="p", title="A", created_by="u")
await store.claim_task(t["id"], claimer_id="agent-1")
await store.update_task(t["id"], status="open")
back = await store.get_task(t["id"])
assert back["status"] == "open"
assert back["claimed_by"] is None
assert back["claimed_at"] is None
assert await store.claim_task(t["id"], claimer_id="agent-2") is True


@pytest.mark.asyncio
async def test_add_relationship_and_list(store):
a = await store.create_task(project_id="p", title="A", created_by="u")
Expand Down
14 changes: 11 additions & 3 deletions tinyagentos/projects/task_store.py
Original file line number Diff line number Diff line change
Expand Up @@ -287,6 +287,12 @@ async def update_task(
patch["element_id"] = element_id
if not sets:
return
# A generic edit back to 'open' must also clear the claimer (as the
# dedicated to-open transitions do): claim_task requires
# claimed_by IS NULL, so a stale claimer leaves the card unclaimable.
if status == "open":
sets.append("claimed_by = ?"); params.append(None); patch["claimed_by"] = None
sets.append("claimed_at = ?"); params.append(None); patch["claimed_at"] = None
sets.append("updated_at = ?"); params.append(time.time())
params.append(task_id)
await self._db.execute(
Expand Down Expand Up @@ -462,13 +468,15 @@ async def unquarantine_task(self, task_id: str, actor: str) -> bool:

This is the explicit un-quarantine / retry action: the card re-enters
the ready pool so the fleet may pick it up again. Strikes are cleared
so a fresh failure count starts from zero. Only acts on a quarantined
task; returns False otherwise.
so a fresh failure count starts from zero, and the claimer clears (as
in ``reopen_task``) -- claim_task requires ``claimed_by IS NULL``, so
a stale claimer would leave the card open but unclaimable forever.
Only acts on a quarantined task; returns False otherwise.
"""
now = time.time()
cursor = await self._db.execute(
"""UPDATE project_tasks
SET status = 'open', updated_at = ?
SET status = 'open', claimed_by = NULL, claimed_at = NULL, updated_at = ?
Comment thread
coderabbitai[bot] marked this conversation as resolved.
WHERE id = ? AND status = 'quarantined'""",
(now, task_id),
)
Expand Down
Loading