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

  1. Only discard threads with no turn history; skip or list threads whose `latest_turn_id` is set.
  2. If the thread must go, first delete its turn/conversation records (or use the store-level removal API that cascades), then discard the thread.
  3. Create a fresh thread instead of trying to reuse-and-discard one that already has turns.
  4. 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

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


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)