Hmbown/CodeWhale · error
cannot discard a Runtime thread that owns turns
Error message
cannot discard a Runtime thread that owns turns
What it means
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.
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.
Example fix
// before
runtime.discard_thread(&thread_id)?; // fails: thread ran a turn
// after
let thread = runtime.load_thread(&thread_id)?;
if thread.latest_turn_id.is_none() {
runtime.discard_thread(&thread_id)?;
} else {
runtime.remove_thread_with_history(&thread_id)?; // cascade-capable removal
} Defensive patterns
Strategy: validation
Validate before calling
let thread = runtime.load_thread(&id)?;
if thread.latest_turn_id.is_some() { /* handle: thread not discardable */ } Type guard
fn is_discardable(t: &Thread) -> bool { t.latest_turn_id.is_none() } Try / catch
match runtime.discard_thread(&id) {
Ok(()) => {},
Err(e) if e.to_string().contains("owns turns") => {
// delete history via cascade API, then retry
},
Err(e) => return Err(e.into()),
} Prevention
- 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
When it happens
Trigger: 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.
Common situations: 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.
Understand the failure class
Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.
Related errors
- cannot discard a loaded Runtime thread
- Automation admission execution ownership is unverified or…
- bundle already installed; use
- Cannot continue agent
- Codewhale runtime thread panicked
AI-assisted analysis of Hmbown/CodeWhale@73e0f67d83 (2026-09-22).
Data as JSON: /api/errors/a9b7446e1f75d437.
Report an issue: GitHub.
Appendix: source
Thrown at crates/tui/src/runtime_threads.rs:7397
)
.await
{
self.active.lock().await.shell_managers.remove(&thread.id);
let _ = self.store.remove_thread(&thread.id);
return Err(error);
}
Ok(thread)
}
pub(crate) async fn discard_empty_thread(&self, thread_id: &str) -> Result<()> {
let mut active = self.active.lock().await;
if active.engines.contains_key(thread_id) {
bail!("cannot discard a loaded Runtime thread");
}
let _thread_mutation = self.store.thread_mutation.lock();
let thread = self.store.load_thread(thread_id)?;
if thread.latest_turn_id.is_some() {
bail!("cannot discard a Runtime thread that owns turns");
}
// Drop the thread's shell authority with it: the manager owns any
// API-created jobs, and dropping the last handle kills them.
active.shell_managers.remove(thread_id);
drop(active);
self.store.remove_thread(thread_id)
}
pub async fn list_threads(
&self,
filter: ThreadListFilter,
limit: Option<usize>,
) -> Result<Vec<ThreadRecord>> {
let mut threads = self.store.list_threads()?;
match filter {
ThreadListFilter::ActiveOnly => threads.retain(|t| !t.archived),
ThreadListFilter::ArchivedOnly => threads.retain(|t| t.archived),
ThreadListFilter::IncludeArchived => {}View on GitHub (pinned to 73e0f67d83)