{"record":{"id":"8524b7ed864b5665","repo":"usestrix/strix","slug":"scan-is-already-starting-or-running","errorCode":null,"errorMessage":"Scan is already starting or running","messagePattern":"Scan is already starting or running","errorType":"exception","errorClass":"RuntimeError","httpStatus":null,"severity":"warning","filePath":"strix/interface/tui/backend/controller.py","lineNumber":299,"sourceCode":"\n    async def _add_target(self, payload: dict[str, Any]) -> dict[str, Any]:\n        self._require_setup_mutable()\n        target = self._required_string(payload, \"target\")\n        if target not in self.targets:\n            self.targets.append(target)\n        return {\"target\": target, \"total\": len(self.targets)}\n\n    async def _set_instruction(self, payload: dict[str, Any]) -> dict[str, Any]:\n        self._require_setup_mutable()\n        instruction = payload.get(\"instruction\", \"\")\n        if not isinstance(instruction, str):\n            raise TypeError(\"instruction must be a string\")\n        self.instruction = instruction.strip()\n        return {\"instruction\": self.instruction}\n\n    async def _start(self, payload: dict[str, Any]) -> dict[str, Any]:\n        if self.scan_started or self._start_in_progress:\n            raise RuntimeError(\"Scan is already starting or running\")\n        # A bare prompt launches optimistically, like a coding agent: it skips\n        # the network model preflight and surfaces any model error live. A named\n        # target keeps the preflight so a real scan does not commit blind.\n        verify = payload.get(\"verify\", True)\n        if not isinstance(verify, bool):\n            raise TypeError(\"verify must be a boolean\")\n        # Launching with no target mounts the working directory, so it requires\n        # the user's explicit confirmation rather than happening silently.\n        mount_working_dir = payload.get(\"mount_working_dir\", False)\n        if not isinstance(mount_working_dir, bool):\n            raise TypeError(\"mount_working_dir must be a boolean\")\n        model = (load_settings().llm.model or \"\").strip()\n        if not model:\n            raise ValueError(\"No model configured. Set STRIX_LLM first.\")\n        if self._on_start is None:\n            raise RuntimeError(\"Scan start is unavailable\")\n        if not self.targets:\n            if not mount_working_dir:","sourceCodeStart":281,"sourceCodeEnd":317,"githubUrl":"https://github.com/usestrix/strix/blob/85513391305171ecc6faffe03da4a8bda5e3febb/strix/interface/tui/backend/controller.py#L281-L317","documentation":"Raised by the TUI controller's _start handler (strix/interface/tui/backend/controller.py:299) when a setup.start command arrives while scan_started is already true or _start_in_progress is true. It is a re-entrancy guard: one scan per controller instance, and the async start sequence (which awaits _begin_scan) must not run twice concurrently.","triggerScenarios":"Double-pressing Enter/Start in the TUI before the first press finishes preparing; a frontend retry on slow ack sending setup.start again; racing the workspace-mount confirmation path which also flips scan_started.","commonSituations":"Slow model preflight making users click start twice; frontend event handlers not debounced; scripted clients firing start on a timer without checking state.","solutions":["Treat it as benign in the UI: the first start is still progressing — do not retry, wait for the state change to 'preparing'/'running'.","Debounce/guard the start button in the frontend (disable once clicked until scan_started flips).","For programmatic drivers, poll a status/read endpoint and only send setup.start when no scan is active."],"exampleFix":"// before\nbutton.onClick = () => send(\"setup.start\", {});   // double-click -> RuntimeError\n\n// after\nlet started = false;\nbutton.onClick = () => { if (!started) { started = true; send(\"setup.start\", {}); } };","handlingStrategy":"try-catch","validationCode":"if controller.scan_started or getattr(controller, '_start_in_progress', False):\n    print('scan already starting/running; ignoring extra start')\nelse:\n    await controller.handle(\"setup.start\", {})","typeGuard":"def scan_is_idle(controller) -> bool:\n    return not controller.scan_started and not getattr(controller, '_start_in_progress', False)","tryCatchPattern":"try:\n    await controller.handle(\"setup.start\", {})\nexcept RuntimeError as exc:\n    if 'already starting or running' in str(exc):\n        pass  # benign duplicate start; first one is still in flight\n    else:\n        raise","preventionTips":["Disable the start button immediately after the first press.","Drive programmatic starts from a state poll, not a timer.","Treat this specific message as idempotent-start feedback, not an error."],"tags":["tui","reentrancy","state-machine","race-condition"],"backgroundTag":null,"analyzedSha":"85513391305171ecc6faffe03da4a8bda5e3febb","analyzedAt":"2026-08-15T05:03:57.275Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}