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
@@ -156,6 +156,40 @@ async def test_dispatch_blocker_work_spawns_non_hitl_blocked_task(
spawn.assert_awaited_once()
@pytest.mark.asyncio
async def test_dispatch_blocker_work_skips_dependency_held_task(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""2026-07-29: a blocked task waiting on a non-terminal dependency must
be skipped BEFORE the respawn gate — spawn_agent's readiness gate was
going to refuse anyway, and each refused attempt burned a breaker strike
plus, once tripped, a CEO escalation notification."""
orch = _orch()
task: dict[str, Any] = {
"id": "t1",
"status": "blocked",
"blocker_resolver_type": None,
"team": "backend",
"assigned_to": AGENT_UUIDS["be-pm"],
"dependency_ids": ["d1"],
}
monkeypatch.setattr(orch, "_fetch_tasks", AsyncMock(return_value=[task]))
monkeypatch.setattr(
orch,
"_check_dependencies_terminal",
AsyncMock(return_value="Task t1 waiting on non-terminal dependency d1"),
)
gate = AsyncMock(return_value=False)
monkeypatch.setattr(orch, "_pm_respawn_should_gate", gate)
spawn = AsyncMock()
monkeypatch.setattr(orch, "spawn_agent", spawn)
await orch._dispatch_blocker_work(client=MagicMock())
spawn.assert_not_awaited()
gate.assert_not_awaited()
# ---------------------------------------------------------------------------
# _claimed_task_needs_agent — claimed-but-no-agent detection
# ---------------------------------------------------------------------------