{"record":{"id":"097baa29644c8945","repo":"usestrix/strix","slug":"scan-loop-is-not-ready","errorCode":null,"errorMessage":"Scan loop is not ready","messagePattern":"Scan loop is not ready","errorType":"exception","errorClass":"RuntimeError","httpStatus":null,"severity":"error","filePath":"strix/interface/tui/backend/controller.py","lineNumber":365,"sourceCode":"            raise RuntimeError(\"No mount confirmation is pending\")\n        approved = payload.get(\"approved\")\n        if not isinstance(approved, bool):\n            raise TypeError(\"approved must be a boolean\")\n        self.pending_workspace_mount = None\n        # Declining skips the mount, it does not abandon the scan. The prompt is\n        # the whole of the input either way; the working directory is only an\n        # extra the agent may look at, so the run goes ahead without one.\n        self.workspace_mount = mount if approved else None\n        await self._begin_scan(self._pending_verify)\n        return {\"approved\": approved}\n\n    async def _send_message(self, payload: dict[str, Any]) -> dict[str, Any]:\n        agent_id = self._required_string(payload, \"agent_id\")\n        message = self._required_string(payload, \"message\")\n        if self.coordinator is None:\n            raise RuntimeError(\"Agent coordinator is unavailable\")\n        if self.scan_loop is None or self.scan_loop.is_closed():\n            raise RuntimeError(\"Scan loop is not ready\")\n        self.live_view.record_user_message(agent_id, message)\n        if self.scan_loop is asyncio.get_running_loop():\n            delivered = await self.coordinator.send(\n                agent_id,\n                {\"from\": \"user\", \"content\": message, \"type\": \"instruction\"},\n            )\n        else:\n            future = asyncio.run_coroutine_threadsafe(\n                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}","sourceCodeStart":347,"sourceCodeEnd":383,"githubUrl":"https://github.com/usestrix/strix/blob/85513391305171ecc6faffe03da4a8bda5e3febb/strix/interface/tui/backend/controller.py#L347-L383","documentation":"Raised by _send_message when the asyncio event loop reserved for the scan (self.scan_loop) is either None or already closed. The controller routes coordinator.send onto that loop via run_coroutine_threadsafe when called from another thread, so a dead loop means there is no executor to deliver the message on. It fires after the coordinator check passed, i.e. the scan infrastructure half-exists.","triggerScenarios":"Sending an agent message after the scan loop was closed (scan finished and loop shut down), or before it was assigned (loop is None) while a coordinator object is present. Also triggered by calling _send_message from a thread after asyncio.run completed.","commonSituations":"Message sent during scan teardown; scan crashed and the loop was cleaned up while the UI stayed open; embedding the controller in a process where the scan loop runs under asyncio.run and has already returned.","solutions":["Check controller.scan_loop and .is_closed() before sending, or gate on scan_state == 'running'","Restart or re-initialize the scan to obtain a fresh loop","Keep the scan loop alive for the whole controller lifetime if post-scan chat must work","In embedded usage, run the controller on a dedicated long-lived loop instead of asyncio.run per operation"],"exampleFix":"loop = controller.scan_loop\nif loop is None or loop.is_closed():\n    show_status(\"Scan is not running; start a scan to chat with agents\")\nelse:\n    await controller._send_message({\"agent_id\": aid, \"message\": msg})","handlingStrategy":"validation","validationCode":"loop = controller.scan_loop\nif loop is None or loop.is_closed():\n    show_status(\"Scan loop is down; message not sent\")\n    return","typeGuard":"def loop_alive(loop) -> bool:\n    return loop is not None and not loop.is_closed()","tryCatchPattern":"try:\n    await controller._send_message(payload)\nexcept RuntimeError as e:\n    if \"Scan loop is not ready\" in str(e):\n        await restart_scan_or_notify()\n    else:\n        raise","preventionTips":["Check scan_loop.is_closed() before cross-thread dispatch","Keep one long-lived loop for the controller lifetime in embedded setups","Cancel pending UI dispatches during scan teardown"],"tags":["tui","asyncio","lifecycle","agent-communication"],"backgroundTag":null,"analyzedSha":"85513391305171ecc6faffe03da4a8bda5e3febb","analyzedAt":"2026-08-15T05:03:57.275Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}