{"record":{"id":"a9b7446e1f75d437","repo":"Hmbown/CodeWhale","slug":"cannot-discard-a-runtime-thread-that-owns-turns","errorCode":null,"errorMessage":"cannot discard a Runtime thread that owns turns","messagePattern":"cannot discard a Runtime thread that owns turns","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/tui/src/runtime_threads.rs","lineNumber":7397,"sourceCode":"            )\n            .await\n        {\n            self.active.lock().await.shell_managers.remove(&thread.id);\n            let _ = self.store.remove_thread(&thread.id);\n            return Err(error);\n        }\n        Ok(thread)\n    }\n\n    pub(crate) async fn discard_empty_thread(&self, thread_id: &str) -> Result<()> {\n        let mut active = self.active.lock().await;\n        if active.engines.contains_key(thread_id) {\n            bail!(\"cannot discard a loaded Runtime thread\");\n        }\n        let _thread_mutation = self.store.thread_mutation.lock();\n        let thread = self.store.load_thread(thread_id)?;\n        if thread.latest_turn_id.is_some() {\n            bail!(\"cannot discard a Runtime thread that owns turns\");\n        }\n        // Drop the thread's shell authority with it: the manager owns any\n        // API-created jobs, and dropping the last handle kills them.\n        active.shell_managers.remove(thread_id);\n        drop(active);\n        self.store.remove_thread(thread_id)\n    }\n\n    pub async fn list_threads(\n        &self,\n        filter: ThreadListFilter,\n        limit: Option<usize>,\n    ) -> Result<Vec<ThreadRecord>> {\n        let mut threads = self.store.list_threads()?;\n        match filter {\n            ThreadListFilter::ActiveOnly => threads.retain(|t| !t.archived),\n            ThreadListFilter::ArchivedOnly => threads.retain(|t| t.archived),\n            ThreadListFilter::IncludeArchived => {}","sourceCodeStart":7379,"sourceCodeEnd":7415,"githubUrl":"https://github.com/Hmbown/CodeWhale/blob/73e0f67d83c59909b571efdfc88c4bc28c309cb1/crates/tui/src/runtime_threads.rs#L7379-L7415","documentation":"Discarding a Runtime thread refuses to proceed when the thread still owns turns (`thread.latest_turn_id` is set). The library treats a thread with turn history as stateful and will not delete it, since its conversation record would be orphaned. Only empty threads (never run, or with no persisted turn) may be discarded.","triggerScenarios":"Calling the discard/remove-thread operation on a thread_id whose loaded thread has `latest_turn_id.is_some()` — i.e. any thread that has executed at least one turn.","commonSituations":"Calling a delete/cleanup API after a user has chatted in the thread; a cleanup job that assumes freshly-created threads; re-running a discard after a partial reset that kept turn records; tests that create a thread, run a turn, then try to drop it.","solutions":["Only discard threads with no turn history; skip or list threads whose `latest_turn_id` is set.","If the thread must go, first delete its turn/conversation records (or use the store-level removal API that cascades), then discard the thread.","Create a fresh thread instead of trying to reuse-and-discard one that already has turns.","If the intent is just to free the engine/shell manager handles, drop those instead of discarding the whole thread."],"exampleFix":"// before\nruntime.discard_thread(&thread_id)?; // fails: thread ran a turn\n\n// after\nlet thread = runtime.load_thread(&thread_id)?;\nif thread.latest_turn_id.is_none() {\n    runtime.discard_thread(&thread_id)?;\n} else {\n    runtime.remove_thread_with_history(&thread_id)?; // cascade-capable removal\n}","handlingStrategy":"validation","validationCode":"let thread = runtime.load_thread(&id)?;\nif thread.latest_turn_id.is_some() { /* handle: thread not discardable */ }","typeGuard":"fn is_discardable(t: &Thread) -> bool { t.latest_turn_id.is_none() }","tryCatchPattern":"match runtime.discard_thread(&id) {\n    Ok(()) => {},\n    Err(e) if e.to_string().contains(\"owns turns\") => {\n        // delete history via cascade API, then retry\n    },\n    Err(e) => return Err(e.into()),\n}","preventionTips":["Check latest_turn_id before any discard call","Use cascade removal APIs for threads with history","Never assume a freshly fetched thread is turn-free in shared sessions"],"tags":["runtime","thread-lifecycle","state-conflict"],"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-22T21:17:16.096Z"}