usestrix/strix · warning · RuntimeError

Agent ' ' cannot be stopped while

Error message

Agent '{agent_id}' cannot be stopped while {status or 'unknown'}

What it means

Raised by _stop_agent when the agent exists but its current status is not in _STOPPABLE_AGENT_STATUSES = {'running', 'waiting', 'budget_paused'} (controller.py:36). Strix only allows graceful cancellation of agents that are actively working or paused; agents that already completed, failed, or were cancelled cannot be stopped again. The message interpolates the actual status (or 'unknown' when the status field is empty).

Solutions

  1. Check agent.get('status') is one of running/waiting/budget_paused before issuing stop
  2. Refresh live_view.agents and re-evaluate; if the agent already finished, no stop is needed
  3. Disable the stop control in the UI for non-stoppable statuses
  4. Treat 'completed'/'failed' agents as already stopped and remove them from actionable lists

Example fix

# before
await controller._stop_agent({"agent_id": aid})

# after
STOPPABLE = {"running", "waiting", "budget_paused"}
status = controller.live_view.agents.get(aid, {}).get("status", "")
if status in STOPPABLE:
    await controller._stop_agent({"agent_id": aid})
Defensive patterns

Strategy: validation

Validate before calling

STOPPABLE = {"running", "waiting", "budget_paused"}
status = controller.live_view.agents.get(aid, {}).get("status", "")
if status not in STOPPABLE:
    logging.info("agent %s already %s; no stop needed", aid, status or "unknown")
    return

Type guard

def agent_stoppable(agents: dict, aid: str) -> bool:
    return agents.get(aid, {}).get("status", "") in {"running", "waiting", "budget_paused"}

Try / catch

try:
    await controller._stop_agent({"agent_id": aid})
except RuntimeError as e:
    if "cannot be stopped while" in str(e):
        pass  # already finished/failed — treat as done
    else:
        raise

Prevention

When it happens

Trigger: Calling stop on an agent whose live_view status is e.g. 'completed', 'failed', 'cancelled', or missing/empty (renders as 'unknown'). Common double-click scenario: user hits stop right as the agent finishes.

Common situations: UI stop button not disabled for finished agents; racing a stop request with agent completion; stale agent row still showing in the panel; status field missing in custom agent snapshots.

Related errors


AI-assisted analysis of usestrix/strix@8551339130 (2026-08-15). Data as JSON: /api/errors/f59ff38d301c73f4. Report an issue: GitHub.

Appendix: source

Thrown at strix/interface/tui/backend/controller.py:392

                self.coordinator.send(
                    agent_id,
                    {"from": "user", "content": message, "type": "instruction"},
                ),
                self.scan_loop,
            )
            delivered = await asyncio.wrap_future(future)
        if not delivered:
            raise RuntimeError("Message could not be delivered")
        return {"sent": True}

    async def _stop_agent(self, payload: dict[str, Any]) -> dict[str, Any]:
        agent_id = self._required_string(payload, "agent_id")
        agent = self.live_view.agents.get(agent_id)
        if agent is None:
            raise ValueError(f"Unknown agent: {agent_id}")
        status = str(agent.get("status", ""))
        if status not in _STOPPABLE_AGENT_STATUSES:
            raise RuntimeError(f"Agent '{agent_id}' cannot be stopped while {status or 'unknown'}")
        if self.coordinator is None or self.scan_loop is None or self.scan_loop.is_closed():
            raise RuntimeError("Scan loop is not ready")
        if self.scan_loop is asyncio.get_running_loop():
            accepted = await self.coordinator.cancel_descendants_graceful(agent_id)
        else:
            future = asyncio.run_coroutine_threadsafe(
                self.coordinator.cancel_descendants_graceful(agent_id), self.scan_loop
            )
            accepted = await asyncio.wrap_future(future)
        if not accepted:
            raise RuntimeError(f"Agent '{agent_id}' is no longer active")
        return {"stopped": True}

    async def _open_viewer(self, _payload: dict[str, Any]) -> dict[str, Any]:
        if self.viewer_url:
            with contextlib.suppress(Exception):
                webbrowser.open(self.viewer_url)
            return {"status": "running", "url": self.viewer_url}

View on GitHub (pinned to 8551339130)