mirror of
https://github.com/rennf93/roboco.git
synced 2026-08-03 07:23:24 +02:00
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:
@@ -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
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user