Hmbown/CodeWhale · error

thread ' ' already has an active turn; finish it before…

Error message

thread '{id}' already has an active turn; finish it before enabling shell commands

What it means

Enabling shell commands (`allow_shell`) on a thread is refused while that thread already has an active turn running. The library requires the turn loop to be idle so the new shell authority snapshot is picked up cleanly rather than mid-execution.

Solutions

  1. Wait for the current turn to finish, then retry the update.
  2. Subscribe to turn-completion events and re-issue the allow_shell update once idle.
  3. If urgent, cancel/abort the active turn, then enable shell.
  4. Poll the thread state for `active_turn == None` before calling.

Example fix

// before
api.update_thread(id, req).await?; // may race with an active turn

// after
while thread_is_busy(id) { tokio::time::sleep(Duration::from_millis(200)).await; }
api.update_thread(id, req).await?;
Defensive patterns

Strategy: retry

Validate before calling

let busy = api.thread_state(id).await?.active_turn.is_some();
if busy { /* defer the allow_shell update */ }

Try / catch

match api.update_thread(id, req).await {
    Err(e) if e.to_string().contains("active turn") => {
        tokio::time::sleep(Duration::from_secs(1)).await;
        // retry after the turn finishes
    },
    other => other?,
}

Prevention

When it happens

Trigger: Calling update-thread with `allow_shell: Some(true)` on a thread whose engine state has `active_turn: Some(_)` — i.e. a turn is currently executing in that thread.

Common situations: User flips a 'enable shell' toggle while the assistant is mid-turn; automation grants shell access in parallel with a running request; a retry of the update right after starting a long turn.

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

Appendix: source

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

            // and evict the cached engine atomically with the durable update.
            // Using the same order as start/compact avoids lock inversion.
            let mut active = self.active.lock().await;
            let _thread_mutation = self.store.thread_mutation.lock();
            let mut thread = self
                .store
                .load_thread(id)
                .with_context(|| format!("Thread not found: {id}"))?;
            // Shell opt-in broadens only an idle conversation. Check while
            // holding the same active + record locks used by turn admission.
            if req.allow_shell == Some(true)
                && !thread.allow_shell
                && active
                    .engines
                    .get(id)
                    .and_then(|state| state.active_turn.as_ref())
                    .is_some()
            {
                bail!(
                    "thread '{id}' already has an active turn; finish it before enabling shell commands"
                );
            }
            if req.allow_shell.unwrap_or(thread.allow_shell)
                && (req.allow_shell.is_some() || req.workspace.is_some())
                && shell_policy_workspace.as_deref()
                    != Some(req.workspace.as_deref().unwrap_or(&thread.workspace))
            {
                bail!(
                    "thread permissions changed during update; refresh the conversation before trying again"
                );
            }
            let mut changes = serde_json::Map::new();
            let policy_patch = if req.mode.is_some()
                || req.permission_posture.is_some()
                || req.auto_approve.is_some()
            {
                Some(runtime_policy_with_overrides(

View on GitHub (pinned to 73e0f67d83)