fix(gateway): hold pre-assigned dev at give_me_work's list_assigned fallback when deps are unmet

This commit is contained in:
Renn F
2026-06-03 21:13:09 +02:00
parent a14e870ebb
commit 3f1904825f
2 changed files with 45 additions and 4 deletions
+26 -1
View File
@@ -613,6 +613,29 @@ class Choreographer:
return f"call i_will_plan(task_id='{tid}', plan='<plan>') to start" return f"call i_will_plan(task_id='{tid}', plan='<plan>') to start"
return f"call i_will_work_on(task_id='{tid}', plan='<plan>') to start" return f"call i_will_work_on(task_id='{tid}', plan='<plan>') to start"
async def _drop_dependency_held(self, tasks: list[Any]) -> list[Any]:
"""Drop pre-assigned PENDING tasks whose non-terminal dependencies are
still unresolved.
``give_me_work``'s ``list_assigned_for_agent`` fallback includes PENDING
rows with no dependency filter, so without this a held pre-assigned
subtask (e.g. a frontend dev's task waiting on the UX/UI design) would
still be offered and the agent only bounced at claim time. Mirrors the
gate in ``TaskService.list_pending_for_agent`` and ``_run_claim_guards``.
Only PENDING rows are gated — an already-claimed task is past the gate.
"""
offerable: list[Any] = []
for task in tasks:
dep_ids = list(getattr(task, "dependency_ids", []) or [])
if (
str(task.status) == "pending"
and dep_ids
and await self._deps.task.unmet_dependency_ids(dep_ids)
):
continue
offerable.append(task)
return offerable
async def give_me_work(self, agent_id: UUID) -> Envelope: async def give_me_work(self, agent_id: UUID) -> Envelope:
"""Return the agent's most-actionable task or signal idle.""" """Return the agent's most-actionable task or signal idle."""
agent = await self._deps.task.agent_for(agent_id) agent = await self._deps.task.agent_for(agent_id)
@@ -632,7 +655,9 @@ class Choreographer:
next=self._claim_verb_hint(role, t), next=self._claim_verb_hint(role, t),
context_briefing=await self._briefing_for(agent_id, t.id), context_briefing=await self._briefing_for(agent_id, t.id),
).with_introspection(task=t, role=role) ).with_introspection(task=t, role=role)
assigned = await self._deps.task.list_assigned_for_agent(agent_id) assigned = await self._drop_dependency_held(
await self._deps.task.list_assigned_for_agent(agent_id)
)
if assigned: if assigned:
t = assigned[0] t = assigned[0]
return Envelope.ok( return Envelope.ok(
@@ -274,10 +274,20 @@ async def test_all_three_dev_paths_gate_then_release(dep_gate_setup: dict) -> No
assert issue is not None, "spawn validation must hold the dev while UX is unmet" assert issue is not None, "spawn validation must hold the dev while UX is unmet"
assert "dependency" in issue assert "dependency" in issue
# --- (b) give_me_work: list_pending_for_agent --- # --- (b) give_me_work has TWO offer paths and both must exclude the held
# subtask: list_pending_for_agent (primary) and the
# list_assigned_for_agent fallback (PENDING rows, no built-in dep filter).
offered = await svc.list_pending_for_agent(fe_dev_db_id) offered = await svc.list_pending_for_agent(fe_dev_db_id)
assert dev_subtask.id not in {t.id for t in offered}, ( assert dev_subtask.id not in {t.id for t in offered}, (
"give_me_work must not offer the dev subtask while UX is unmet" "list_pending_for_agent must not offer the held dev subtask"
)
assigned = await svc.list_assigned_for_agent(fe_dev_db_id)
assert dev_subtask.id in {t.id for t in assigned}, (
"sanity: the held subtask is assigned+pending, so the fallback path is real"
)
offerable = await choreo._drop_dependency_held(assigned)
assert dev_subtask.id not in {t.id for t in offerable}, (
"give_me_work's list_assigned_for_agent fallback must not offer it"
) )
# --- (c) claim: _run_claim_guards (the i_will_work_on guard set) --- # --- (c) claim: _run_claim_guards (the i_will_work_on guard set) ---
@@ -300,7 +310,13 @@ async def test_all_three_dev_paths_gate_then_release(dep_gate_setup: dict) -> No
) )
offered_after = await svc.list_pending_for_agent(fe_dev_db_id) offered_after = await svc.list_pending_for_agent(fe_dev_db_id)
assert dev_subtask.id in {t.id for t in offered_after}, ( assert dev_subtask.id in {t.id for t in offered_after}, (
"give_me_work must offer the dev subtask once UX is terminal" "list_pending_for_agent must offer the dev subtask once UX is terminal"
)
offerable_after = await choreo._drop_dependency_held(
await svc.list_assigned_for_agent(fe_dev_db_id)
)
assert dev_subtask.id in {t.id for t in offerable_after}, (
"the assigned-fallback gate must release the subtask once UX is terminal"
) )
released = await svc.get(dev_subtask.id) released = await svc.get(dev_subtask.id)
assert ( assert (