{"record":{"id":"9c3dbfa554e55729","repo":"usestrix/strix","slug":"agent-agent-id-is-no-longer-active","errorCode":null,"errorMessage":"Agent '{agent_id}' is no longer active","messagePattern":"Agent '(.+?)' is no longer active","errorType":"exception","errorClass":"RuntimeError","httpStatus":null,"severity":"info","filePath":"strix/interface/tui/backend/controller.py","lineNumber":403,"sourceCode":"    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}\n        if self.report_state is None:\n            self.viewer_status = \"failed\"\n            return {\"status\": self.viewer_status, \"error\": \"Scan output is not ready\"}\n        try:\n            from strix.interface.tui.backend.messages import (\n                send_user_message_to_agent,\n            )\n            from strix.interface.viewer.server import (\n                authorized_url,\n                bundle_is_built,\n                serve,","sourceCodeStart":385,"sourceCodeEnd":421,"githubUrl":"https://github.com/usestrix/strix/blob/85513391305171ecc6faffe03da4a8bda5e3febb/strix/interface/tui/backend/controller.py#L385-L421","documentation":"Raised by _stop_agent when coordinator.cancel_descendants_graceful(agent_id) completes but returns false. The graceful-cancel API returns a boolean acceptance: false means the coordinator no longer knows the agent as an active cancellable node (it finished, was already cancelled, or was reaped between the UI status check and the coordinator call). Distinct from error 45: this is the coordinator's own verdict after the live_view snapshot said the agent was stoppable.","triggerScenarios":"Stop request racing agent completion: live_view still showed 'running' when the handler started, but by the time the coordinator processed the cancel the agent had exited. Also after a coordinator restart invalidated agent bookkeeping.","commonSituations":"User clicks stop as an agent finishes its task; concurrent stop requests (first succeeds, second gets false); long scheduling delay on the scan loop letting the agent complete first.","solutions":["Treat it as success-with-no-op: the agent is no longer active, which is the desired end state","Refresh live_view.agents to confirm the agent's final status instead of retrying the stop","Suppress or downgrade this specific message in UI handling to avoid alarming users","Avoid double-stop: disable the stop control immediately after the first request"],"exampleFix":"# before\nawait controller._stop_agent({\"agent_id\": aid})\n\n# after\ntry:\n    await controller._stop_agent({\"agent_id\": aid})\nexcept RuntimeError as e:\n    if \"no longer active\" in str(e):\n        pass  # already finished; nothing to stop\n    else:\n        raise","handlingStrategy":"fallback","validationCode":"status = controller.live_view.agents.get(aid, {}).get(\"status\", \"\")\nif status not in (\"running\", \"waiting\", \"budget_paused\"):\n    return  # goal state already reached","typeGuard":"def already_gone(e: RuntimeError) -> bool:\n    return \"no longer active\" in str(e)","tryCatchPattern":"try:\n    await controller._stop_agent({\"agent_id\": aid})\nexcept RuntimeError as e:\n    if \"no longer active\" not in str(e):\n        raise\n    # agent finished concurrently — desired end state achieved, no-op","preventionTips":["Treat 'no longer active' as success (idempotent stop semantics)","Debounce/duplicate-guard the stop button to avoid concurrent stop requests","Refresh live_view after the race instead of retrying the stop"],"tags":["tui","agent-lifecycle","race-condition","idempotency"],"backgroundTag":null,"analyzedSha":"85513391305171ecc6faffe03da4a8bda5e3febb","analyzedAt":"2026-08-15T05:03:57.275Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}