{"record":{"id":"f59ff38d301c73f4","repo":"usestrix/strix","slug":"agent-agent-id-cannot-be-stopped-while-status","errorCode":null,"errorMessage":"Agent '{agent_id}' cannot be stopped while {status or 'unknown'}","messagePattern":"Agent '(.+?)' cannot be stopped while (.+?)","errorType":"exception","errorClass":"RuntimeError","httpStatus":null,"severity":"warning","filePath":"strix/interface/tui/backend/controller.py","lineNumber":392,"sourceCode":"                self.coordinator.send(\n                    agent_id,\n                    {\"from\": \"user\", \"content\": message, \"type\": \"instruction\"},\n                ),\n                self.scan_loop,\n            )\n            delivered = await asyncio.wrap_future(future)\n        if not delivered:\n            raise RuntimeError(\"Message could not be delivered\")\n        return {\"sent\": True}\n\n    async def _stop_agent(self, payload: dict[str, Any]) -> dict[str, Any]:\n        agent_id = self._required_string(payload, \"agent_id\")\n        agent = self.live_view.agents.get(agent_id)\n        if agent is None:\n            raise ValueError(f\"Unknown agent: {agent_id}\")\n        status = str(agent.get(\"status\", \"\"))\n        if status not in _STOPPABLE_AGENT_STATUSES:\n            raise RuntimeError(f\"Agent '{agent_id}' cannot be stopped while {status or 'unknown'}\")\n        if self.coordinator is None or self.scan_loop is None or self.scan_loop.is_closed():\n            raise RuntimeError(\"Scan loop is not ready\")\n        if self.scan_loop is asyncio.get_running_loop():\n            accepted = await self.coordinator.cancel_descendants_graceful(agent_id)\n        else:\n            future = asyncio.run_coroutine_threadsafe(\n                self.coordinator.cancel_descendants_graceful(agent_id), self.scan_loop\n            )\n            accepted = await asyncio.wrap_future(future)\n        if not accepted:\n            raise RuntimeError(f\"Agent '{agent_id}' is no longer active\")\n        return {\"stopped\": True}\n\n    async def _open_viewer(self, _payload: dict[str, Any]) -> dict[str, Any]:\n        if self.viewer_url:\n            with contextlib.suppress(Exception):\n                webbrowser.open(self.viewer_url)\n            return {\"status\": \"running\", \"url\": self.viewer_url}","sourceCodeStart":374,"sourceCodeEnd":410,"githubUrl":"https://github.com/usestrix/strix/blob/85513391305171ecc6faffe03da4a8bda5e3febb/strix/interface/tui/backend/controller.py#L374-L410","documentation":"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).","triggerScenarios":"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.","commonSituations":"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.","solutions":["Check agent.get('status') is one of running/waiting/budget_paused before issuing stop","Refresh live_view.agents and re-evaluate; if the agent already finished, no stop is needed","Disable the stop control in the UI for non-stoppable statuses","Treat 'completed'/'failed' agents as already stopped and remove them from actionable lists"],"exampleFix":"# before\nawait controller._stop_agent({\"agent_id\": aid})\n\n# after\nSTOPPABLE = {\"running\", \"waiting\", \"budget_paused\"}\nstatus = controller.live_view.agents.get(aid, {}).get(\"status\", \"\")\nif status in STOPPABLE:\n    await controller._stop_agent({\"agent_id\": aid})","handlingStrategy":"validation","validationCode":"STOPPABLE = {\"running\", \"waiting\", \"budget_paused\"}\nstatus = controller.live_view.agents.get(aid, {}).get(\"status\", \"\")\nif status not in STOPPABLE:\n    logging.info(\"agent %s already %s; no stop needed\", aid, status or \"unknown\")\n    return","typeGuard":"def agent_stoppable(agents: dict, aid: str) -> bool:\n    return agents.get(aid, {}).get(\"status\", \"\") in {\"running\", \"waiting\", \"budget_paused\"}","tryCatchPattern":"try:\n    await controller._stop_agent({\"agent_id\": aid})\nexcept RuntimeError as e:\n    if \"cannot be stopped while\" in str(e):\n        pass  # already finished/failed — treat as done\n    else:\n        raise","preventionTips":["Disable stop controls for agents outside running/waiting/budget_paused","Refresh the status immediately before stopping","Treat stop as idempotent: a finished agent needs no stop"],"tags":["tui","agent-lifecycle","state","validation"],"backgroundTag":null,"analyzedSha":"85513391305171ecc6faffe03da4a8bda5e3febb","analyzedAt":"2026-08-15T05:03:57.275Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}