{"record":{"id":"62b87faebd9dc966","repo":"Hmbown/CodeWhale","slug":"workspace-already-has-an-active-turn-thread-id-wait-for-it","errorCode":null,"errorMessage":"Workspace already has an active turn (thread {id}); wait for it to finish before restoring files","messagePattern":"Workspace already has an active turn \\(thread (.+?)\\); wait for it to finish before restoring files","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/tui/src/runtime_threads.rs","lineNumber":8173,"sourceCode":"        let workspace = workspace\n            .canonicalize()\n            .unwrap_or_else(|_| workspace.to_path_buf());\n        // Collect ids under the async lock, then do filesystem and store\n        // I/O without holding it.\n        let busy: Vec<String> = {\n            let active = self.active.lock().await;\n            active\n                .engines\n                .iter()\n                .filter(|(_, state)| state.active_turn.is_some())\n                .map(|(id, _)| id.clone())\n                .collect()\n        };\n        for id in busy {\n            let other = self.store.load_thread(&id)?.workspace;\n            let other = other.canonicalize().unwrap_or(other);\n            if workspace.starts_with(&other) || other.starts_with(&workspace) {\n                bail!(\n                    \"Workspace already has an active turn (thread {id}); wait for it to finish before restoring files\"\n                );\n            }\n        }\n        Ok(())\n    }\n\n    /// Test seam: mark or clear an active turn on an installed test engine so\n    /// route-level tests can exercise restore admission.\n    #[cfg(test)]\n    pub(crate) async fn set_active_turn_for_test(\n        &self,\n        thread_id: &str,\n        turn_id: Option<&str>,\n    ) -> Result<()> {\n        let mut active = self.active.lock().await;\n        let state = active\n            .engines","sourceCodeStart":8155,"sourceCodeEnd":8191,"githubUrl":"https://github.com/Hmbown/CodeWhale/blob/73e0f67d83c59909b571efdfc88c4bc28c309cb1/crates/tui/src/runtime_threads.rs#L8155-L8191","documentation":"File restoration for a workspace is refused when another active turn overlaps that workspace — either the busy thread's workspace contains the target workspace or vice versa (path prefix check in both directions). This prevents restoring files that a running turn may be reading or writing.","triggerScenarios":"Calling the restore-files API for workspace W while any thread with an active turn has a workspace that is a path-prefix ancestor or descendant of W (after canonicalization).","commonSituations":"User restores a snapshot into the project root while a subagent turn runs in `project/src`; nested workspaces share a directory tree; an automation restores files on a timer that collides with running turns.","solutions":["Wait for all turns in overlapping workspaces (thread {id} named in the message) to finish, then restore.","Cancel the overlapping thread's turn if immediate restore is required.","Restore into a workspace that does not nest under or contain any busy thread's workspace.","Query busy threads first and skip/pause restore for overlapping paths."],"exampleFix":"// before\napi.restore_files(&workspace).await?; // may overlap a busy thread\n\n// after\nfor t in api.list_threads().await? {\n    if t.active_turn.is_some() && workspaces_overlap(&t.workspace, &workspace) {\n        return Err(format!(\"wait for thread {} to finish\", t.id));\n    }\n}\napi.restore_files(&workspace).await?;","handlingStrategy":"validation","validationCode":"for t in api.list_threads().await? {\n    if t.active_turn.is_some() {\n        let other = t.workspace.canonicalize()?;\n        if workspace.starts_with(&other) || other.starts_with(&workspace) {\n            return Err(format!(\"thread {} is busy in an overlapping workspace\", t.id));\n        }\n    }\n}","typeGuard":null,"tryCatchPattern":"match api.restore_files(&workspace).await {\n    Err(e) if e.to_string().contains(\"already has an active turn\") => {\n        // parse thread id from message, await its completion, then retry\n    },\n    other => other?,\n}","preventionTips":["Check all busy threads for workspace path overlap (both directions) before restoring","Avoid nested workspaces across concurrently active threads","Schedule file restores for idle windows"],"tags":["concurrency","workspace","file-restore"],"backgroundTag":"invalid-state-transition","analyzedSha":"73e0f67d83c59909b571efdfc88c4bc28c309cb1","analyzedAt":"2026-09-22T01:30:00.501Z","contentChangedAt":"2026-09-22T01:30:00.501Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}