From b1174ed95a96f3e1889b0fd224d5fbd7a0410feb Mon Sep 17 00:00:00 2001 From: 0xallam Date: Fri, 28 Aug 2026 21:30:56 +0300 Subject: [PATCH] revert(core): drop the agent revival machinery; the engine already resumes A user message already resumes a failed agent's lifecycle from where it stopped. `send` lifts that agent's human-wake gate, `wait_for_message` returns, the loop resets the nudge and idle-resume budgets - "real input is real progress" - and `mark_running` retires the error before the next turn. Any agent can be addressed: `_send_message` takes an arbitrary agent id, so a failed child is reachable by selecting it, exactly as a parked one is. So there was nothing here to fix. The premise behind these commits - that only the root is reachable, stranding every other agent that dies - was wrong, and each revision built a larger mechanism on it: a scan-wide revival, then an attempt budget to bound the runaway that revival introduced. What remains is the TUI, which is where the reported symptom always was: an error that had already resolved stayed on the screen. Co-Authored-By: Claude Opus 5 (1M context) --- strix/core/agents.py | 43 --------- tests/test_execution_agent_revival.py | 130 -------------------------- 2 files changed, 173 deletions(-) delete mode 100644 tests/test_execution_agent_revival.py diff --git a/strix/core/agents.py b/strix/core/agents.py index d679ddee..c96204df 100644 --- a/strix/core/agents.py +++ b/strix/core/agents.py @@ -22,15 +22,6 @@ if TYPE_CHECKING: logger = logging.getLogger(__name__) -_REVIVED_NOTICE = { - "from": "system", - "type": "revived", - "content": ( - "[Recovery] Your last turn failed and the user has since sent a message. " - "Pick your current task back up from where it stopped." - ), -} - Status = Literal["running", "waiting", "completed", "stopped", "crashed", "failed", "budget_paused"] # Why an agent parked. The user can message any agent, so this - not the agent's @@ -129,36 +120,6 @@ class AgentCoordinator: }, ) - async def revive_failed_agents(self, *, exclude: str | None = None) -> list[str]: - """Lift the human-wake gate on every agent that died, on the user's word. - - A failed agent parks until a human tells it to go again. The user can only - realistically reach the root, so that word never arrives for anything below - it and a child that dies stays dead for the rest of the scan - however it - died. A message from the user is that word for all of them: it says a person - is present and has changed something. Nothing else revives an agent, so - there is no runaway here to bound - the person deciding whether to try again - is the limit. - """ - revived: list[str] = [] - async with self._lock: - for agent_id, status in self.statuses.items(): - if agent_id == exclude or status not in {"failed", "crashed"}: - continue - self.statuses[agent_id] = "waiting" - self.errors.pop(agent_id, None) - runtime = self.runtimes.setdefault(agent_id, AgentRuntime()) - runtime.user_wake_required = False - # An empty mailbox parks the agent straight back where it was, so the - # note is what actually resumes it as well as what explains the gap. - runtime.mailbox.append(dict(_REVIVED_NOTICE)) - self.pending_counts[agent_id] = self.pending_counts.get(agent_id, 0) + 1 - runtime.wake.set() - revived.append(agent_id) - if revived: - logger.info("user message revived %d failed agent(s)", len(revived)) - return revived - async def reset_budget_stops( self, *, @@ -321,10 +282,6 @@ class AgentCoordinator: from_user = message.get("from") == "user" if from_user and self._budget_paused: await self.resume_from_budget_pause(exclude=target_agent_id) - if from_user: - # The gate below only unsticks the agent addressed. Everything else that - # died is waiting on the same human, who has just spoken. - await self.revive_failed_agents(exclude=target_agent_id) async with self._lock: if target_agent_id not in self.statuses: logger.debug("agent.send dropped unknown target=%s", target_agent_id) diff --git a/tests/test_execution_agent_revival.py b/tests/test_execution_agent_revival.py deleted file mode 100644 index 5834bab6..00000000 --- a/tests/test_execution_agent_revival.py +++ /dev/null @@ -1,130 +0,0 @@ -"""A user's message revives every agent that died, not just the one addressed. - -A failed agent parks until a human tells it to go again, and the user can only -realistically reach the root. Drawn from a run where a provider outage failed -eighteen agents in thirteen seconds and only the root ever came back. -""" - -from __future__ import annotations - -import pytest - -from strix.core.agents import AgentCoordinator - - -async def _fleet(children: int = 3) -> AgentCoordinator: - coordinator = AgentCoordinator() - await coordinator.register("root", "strix", parent_id=None) - for index in range(children): - await coordinator.register(f"child{index}", f"hunter {index}", parent_id="root") - return coordinator - - -def _user_message(text: str = "credits are back, keep going") -> dict[str, str]: - return {"from": "user", "content": text, "type": "instruction"} - - -@pytest.mark.asyncio -async def test_a_user_message_revives_the_whole_fleet() -> None: - """The reported scenario: everything is down and only the root is reachable.""" - coordinator = await _fleet() - for agent_id in ("root", "child0", "child1", "child2"): - await coordinator.set_status(agent_id, "failed", error="provider said no") - assert all(coordinator.runtimes[f"child{i}"].user_wake_required for i in range(3)) - - await coordinator.send("root", _user_message()) - - for index in range(3): - child = f"child{index}" - assert coordinator.statuses[child] == "waiting", f"{child} was left for dead" - assert coordinator.runtimes[child].user_wake_required is False - assert coordinator.pending_counts[child] == 1 - assert child not in coordinator.errors - # The addressed agent is woken by the user's own message, not a notice. - assert coordinator.runtimes["root"].user_wake_required is False - assert coordinator.runtimes["root"].mailbox == [_user_message()] - - -@pytest.mark.asyncio -async def test_the_revived_agent_wakes_on_the_notice() -> None: - """An empty mailbox parks the agent straight back where it was.""" - coordinator = await _fleet(children=1) - await coordinator.set_status("child0", "failed", error="down") - - await coordinator.send("root", _user_message()) - - assert await coordinator.wait_for_message("child0", timeout=0.1) is True - count, _items = await coordinator.consume_pending("child0") - assert count == 1 - - -@pytest.mark.asyncio -async def test_revival_is_not_rationed() -> None: - """A human deciding to try again is the only limit there needs to be.""" - coordinator = await _fleet(children=1) - - for attempt in range(6): - await coordinator.set_status("child0", "failed", error="down again") - await coordinator.send("root", _user_message()) - assert coordinator.statuses["child0"] == "waiting", f"attempt {attempt} was refused" - - -@pytest.mark.parametrize("status", ["failed", "crashed"]) -@pytest.mark.asyncio -async def test_revival_does_not_care_how_the_agent_died(status: str) -> None: - coordinator = await _fleet(children=1) - await coordinator.set_status("child0", status, error="whatever it was") - - await coordinator.send("root", _user_message()) - - assert coordinator.statuses["child0"] == "waiting" - - -@pytest.mark.parametrize("status", ["running", "waiting", "completed", "stopped", "budget_paused"]) -@pytest.mark.asyncio -async def test_agents_that_did_not_die_are_left_alone(status: str) -> None: - """A deliberate stop - budget, user request - is not a failure to recover from.""" - coordinator = await _fleet(children=1) - await coordinator.set_status("child0", status) - - await coordinator.send("root", _user_message()) - - assert coordinator.statuses["child0"] == status - assert coordinator.pending_counts["child0"] == 0 - assert coordinator.runtimes["child0"].mailbox == [] - - -@pytest.mark.asyncio -async def test_messaging_a_child_revives_its_failed_siblings() -> None: - """A person is present either way; which agent they addressed is incidental.""" - coordinator = await _fleet(children=2) - await coordinator.set_status("child0", "failed", error="down") - await coordinator.set_status("child1", "failed", error="down") - - await coordinator.send("child0", _user_message("look at this instead")) - - assert coordinator.statuses["child1"] == "waiting" - assert coordinator.runtimes["child0"].user_wake_required is False - - -@pytest.mark.asyncio -async def test_an_agent_message_revives_nobody() -> None: - """Only a human clears the gate; agents talking among themselves must not.""" - coordinator = await _fleet(children=2) - await coordinator.set_status("child1", "failed", error="down") - - await coordinator.send("child0", {"from": "root", "content": "status?"}) - - assert coordinator.statuses["child1"] == "failed" - assert coordinator.runtimes["child1"].user_wake_required is True - - -@pytest.mark.asyncio -async def test_a_message_to_an_unknown_agent_still_revives_the_fleet() -> None: - """The user spoke; a stale target id should not swallow that.""" - coordinator = await _fleet(children=1) - await coordinator.set_status("child0", "failed", error="down") - - assert await coordinator.send("gone", _user_message()) is False - - assert coordinator.statuses["child0"] == "waiting"