Hmbown/CodeWhale · error

cannot discard a loaded Runtime thread

Error message

cannot discard a loaded Runtime thread

What it means

`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.

Solutions

  1. Unload/close the engine for the thread first, then call discard_empty_thread.
  2. Check `active.engines.contains_key(thread_id)` before attempting discard.
  3. If the thread has turns, use a non-empty-thread deletion path or accept that it cannot be discarded as empty.
  4. Serialize cleanup with thread-loading so the check-then-discard is not racing an open.

Example fix

// before
runtime.discard_empty_thread(thread_id).await?; // engine may be loaded
// after
runtime.unload_engine(thread_id).await?;
runtime.discard_empty_thread(thread_id).await?;
Defensive patterns

Strategy: validation

Validate before calling

if runtime.is_thread_loaded(thread_id) {
    anyhow::bail!("unload the engine before discarding thread {}", thread_id);
}

Try / catch

match runtime.discard_empty_thread(thread_id).await {
    Err(e) if e.to_string().contains("loaded Runtime thread") => {
        runtime.unload_engine(thread_id).await?;
        runtime.discard_empty_thread(thread_id).await?;
    }
    other => other,
}

Prevention

When it happens

Trigger: Calling discard_empty_thread while the thread's engine is loaded (present in active.engines); also fails if the thread owns any turns.

Common situations: 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.

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/d23069780c0434ce. Report an issue: GitHub.

Appendix: source

Thrown at crates/tui/src/runtime_threads.rs:7392

                &thread.id,
                None,
                None,
                "thread.started",
                json!({ "thread": thread.clone() }),
            )
            .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>> {

View on GitHub (pinned to 73e0f67d83)