{"record":{"id":"d23069780c0434ce","repo":"Hmbown/CodeWhale","slug":"cannot-discard-a-loaded-runtime-thread","errorCode":null,"errorMessage":"cannot discard a loaded Runtime thread","messagePattern":"cannot discard a loaded Runtime thread","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/tui/src/runtime_threads.rs","lineNumber":7392,"sourceCode":"                &thread.id,\n                None,\n                None,\n                \"thread.started\",\n                json!({ \"thread\": thread.clone() }),\n            )\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>> {","sourceCodeStart":7374,"sourceCodeEnd":7410,"githubUrl":"https://github.com/Hmbown/CodeWhale/blob/73e0f67d83c59909b571efdfc88c4bc28c309cb1/crates/tui/src/runtime_threads.rs#L7374-L7410","documentation":"`discard_empty_thread` only removes threads that are unloaded and have no turns. If an engine for the thread is currently loaded in `active.engines`, discarding would yank state out from under a live engine, so the call bails immediately. A second guard rejects threads whose `latest_turn_id` is set.","triggerScenarios":"Calling discard_empty_thread while the thread's engine is loaded (present in active.engines); also fails if the thread owns any turns.","commonSituations":"Cleanup routines sweeping stale threads that race with a user re-opening the thread; UI calling discard on the thread currently displayed; automated tests not unloading engines before cleanup.","solutions":["Unload/close the engine for the thread first, then call discard_empty_thread.","Check `active.engines.contains_key(thread_id)` before attempting discard.","If the thread has turns, use a non-empty-thread deletion path or accept that it cannot be discarded as empty.","Serialize cleanup with thread-loading so the check-then-discard is not racing an open."],"exampleFix":"// before\nruntime.discard_empty_thread(thread_id).await?; // engine may be loaded\n// after\nruntime.unload_engine(thread_id).await?;\nruntime.discard_empty_thread(thread_id).await?;","handlingStrategy":"validation","validationCode":"if runtime.is_thread_loaded(thread_id) {\n    anyhow::bail!(\"unload the engine before discarding thread {}\", thread_id);\n}","typeGuard":null,"tryCatchPattern":"match runtime.discard_empty_thread(thread_id).await {\n    Err(e) if e.to_string().contains(\"loaded Runtime thread\") => {\n        runtime.unload_engine(thread_id).await?;\n        runtime.discard_empty_thread(thread_id).await?;\n    }\n    other => other,\n}","preventionTips":["Unload engines before running thread cleanup sweeps.","Check latest_turn_id before discarding; threads with turns need a different deletion path.","Coordinate cleanup with the UI so it cannot race a thread being opened."],"tags":["thread-lifecycle","runtime","precondition"],"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-22T16:17:23.217Z"}