{"record":{"id":"276df767c34beb34","repo":"Hmbown/CodeWhale","slug":"thread-id-already-has-an-active-turn-finish-it-before","errorCode":null,"errorMessage":"thread '{id}' already has an active turn; finish it before enabling shell commands","messagePattern":"thread '(.+?)' already has an active turn; finish it before enabling shell commands","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/tui/src/runtime_threads.rs","lineNumber":7936,"sourceCode":"            // and evict the cached engine atomically with the durable update.\n            // Using the same order as start/compact avoids lock inversion.\n            let mut active = self.active.lock().await;\n            let _thread_mutation = self.store.thread_mutation.lock();\n            let mut thread = self\n                .store\n                .load_thread(id)\n                .with_context(|| format!(\"Thread not found: {id}\"))?;\n            // Shell opt-in broadens only an idle conversation. Check while\n            // holding the same active + record locks used by turn admission.\n            if req.allow_shell == Some(true)\n                && !thread.allow_shell\n                && active\n                    .engines\n                    .get(id)\n                    .and_then(|state| state.active_turn.as_ref())\n                    .is_some()\n            {\n                bail!(\n                    \"thread '{id}' already has an active turn; finish it before enabling shell commands\"\n                );\n            }\n            if req.allow_shell.unwrap_or(thread.allow_shell)\n                && (req.allow_shell.is_some() || req.workspace.is_some())\n                && shell_policy_workspace.as_deref()\n                    != Some(req.workspace.as_deref().unwrap_or(&thread.workspace))\n            {\n                bail!(\n                    \"thread permissions changed during update; refresh the conversation before trying again\"\n                );\n            }\n            let mut changes = serde_json::Map::new();\n            let policy_patch = if req.mode.is_some()\n                || req.permission_posture.is_some()\n                || req.auto_approve.is_some()\n            {\n                Some(runtime_policy_with_overrides(","sourceCodeStart":7918,"sourceCodeEnd":7954,"githubUrl":"https://github.com/Hmbown/CodeWhale/blob/73e0f67d83c59909b571efdfc88c4bc28c309cb1/crates/tui/src/runtime_threads.rs#L7918-L7954","documentation":"Enabling shell commands (`allow_shell`) on a thread is refused while that thread already has an active turn running. The library requires the turn loop to be idle so the new shell authority snapshot is picked up cleanly rather than mid-execution.","triggerScenarios":"Calling update-thread with `allow_shell: Some(true)` on a thread whose engine state has `active_turn: Some(_)` — i.e. a turn is currently executing in that thread.","commonSituations":"User flips a 'enable shell' toggle while the assistant is mid-turn; automation grants shell access in parallel with a running request; a retry of the update right after starting a long turn.","solutions":["Wait for the current turn to finish, then retry the update.","Subscribe to turn-completion events and re-issue the allow_shell update once idle.","If urgent, cancel/abort the active turn, then enable shell.","Poll the thread state for `active_turn == None` before calling."],"exampleFix":"// before\napi.update_thread(id, req).await?; // may race with an active turn\n\n// after\nwhile thread_is_busy(id) { tokio::time::sleep(Duration::from_millis(200)).await; }\napi.update_thread(id, req).await?;","handlingStrategy":"retry","validationCode":"let busy = api.thread_state(id).await?.active_turn.is_some();\nif busy { /* defer the allow_shell update */ }","typeGuard":null,"tryCatchPattern":"match api.update_thread(id, req).await {\n    Err(e) if e.to_string().contains(\"active turn\") => {\n        tokio::time::sleep(Duration::from_secs(1)).await;\n        // retry after the turn finishes\n    },\n    other => other?,\n}","preventionTips":["Hook turn-completion events and defer permission grants until idle","Disable shell/workspace toggles in the UI while a turn is running","Retry with backoff on 'active turn' errors"],"tags":["concurrency","turn-loop","permissions"],"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"}