fix(dispatch): stop burning breaker strikes on dependency-held blocked tasks; collision notify rides the delegate transaction (#726)

* fix(dispatch): stop burning breaker strikes on dependency-held blocked tasks; collision notify rides the delegate transaction

Two follow-ups from the 2026-07-29 blocked-task wave:

- _dispatch_blocker_work now skips a blocked task whose dependencies are
  non-terminal BEFORE the respawn gate. spawn_agent's readiness gate was
  refusing these anyway, but each refused attempt burned a respawn-
  breaker strike and, once tripped at 4, a duplicate CEO escalation
  every tick (seen live on the eslint-audit task held behind a paused
  backend dependency). The dispatcher re-checks each tick and proceeds
  the moment the dependency goes terminal — same resume path as before,
  minus the noise.

- send_collision_sequencing_notification gains the db_session threading
  its sibling senders already have, and TaskService passes its own
  session. delegate wires collision edges inside a transaction whose
  held-back child row is not yet committed; the notification INSERT ran
  on a separate auto-commit connection and died on the related_task_id
  FK (ForeignKeyViolationError seen live in cell_pm/delegate), silently
  losing the coordination alert. Riding the caller's transaction makes
  the row visible and commits both atomically.

* test(notification): cast the fake session for the gate's tests-scope mypy

---------

Co-authored-by: Renn F <rennf93@users.noreply.github.com>
This commit is contained in:
Renzo F
2026-07-29 23:23:29 +02:00
committed by GitHub
co-authored by Renn F
parent d7a1c2d203
commit 8b18dc3e95
6 changed files with 86 additions and 1 deletions
+17
View File
@@ -618,6 +618,23 @@ async def test_wire_sibling_collision_dag_notifies_only_for_new_edges() -> None:
assert add_dep_mock.await_count == wiring_passes
@pytest.mark.asyncio
async def test_collision_sequencing_notify_rides_task_service_session() -> None:
"""2026-07-29: the held-back task row is uncommitted in this transaction —
the notification must ride the SAME session or its related_task_id FK
fails (ForeignKeyViolationError seen live in cell_pm/delegate)."""
session = MagicMock(flush=AsyncMock())
svc = TaskService(session)
mock_ns = MagicMock()
mock_ns.send_collision_sequencing_notification = AsyncMock()
with patch(
"roboco.services.notification.NotificationService", return_value=mock_ns
):
await svc._notify_collision_sequencing(uuid4(), uuid4(), None)
kwargs = mock_ns.send_collision_sequencing_notification.await_args.kwargs
assert kwargs["db_session"] is session
@pytest.mark.asyncio
async def test_mark_agent_idle_sets_status_idle() -> None:
agent = MagicMock(id=uuid4(), status=AgentStatus.ACTIVE, current_task_id=uuid4())