Hmbown/CodeWhale · error

No active turn on thread

Error message

No active turn on thread {thread_id}

What it means

RuntimeThreadManager::interrupt_turn (crates/tui/src/runtime_threads.rs:10335) looks up the thread's currently active turn under the active-state lock before issuing a cancel. When the thread is loaded but has no turn in flight (active_turn is None), it bails with this message. It means you called interrupt_turn for a thread that is idle.

Solutions

  1. Check the thread has an in-flight turn before calling interrupt_turn (track turn status from turn.* events).
  2. Treat this error as a no-op: the desired end state (no running turn) is already true; catch and ignore.
  3. Verify the thread_id is correct and that the turn you intend to interrupt was actually started on this thread.

Example fix

// before
manager.interrupt_turn(&thread_id, &turn_id).await?;
// after
if let Err(e) = manager.interrupt_turn(&thread_id, &turn_id).await {
    if !e.to_string().contains("No active turn") {
        return Err(e); // already-stopped turns are fine to ignore
    }
}
Defensive patterns

Strategy: try-catch

Validate before calling

// track turn state from events; only call when a turn is live
fn can_interrupt(thread: &ThreadState) -> bool { thread.active_turn_id.is_some() }

Type guard

fn has_active_turn(t: &ActiveThreadState) -> bool { t.active_turn.is_some() }

Try / catch

match manager.interrupt_turn(&tid, &turn_id).await {
    Ok(r) => /* interrupted */,
    Err(e) if e.to_string().contains("No active turn") => { /* already stopped: no-op */ }
    Err(e) => return Err(e),
}

Prevention

When it happens

Trigger: Calling interrupt_turn(thread_id, turn_id) when active_thread.active_turn is None: the turn already completed/failed, the turn never started, or the thread was just loaded from the store with no turn running.

Common situations: Double-clicking an interrupt key so the second interrupt races the first turn's completion; interrupting after a turn finished between listing turns and calling; automation scripts interrupting a thread whose turn already ended.

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

Appendix: source

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

        };

        let turn = acceptance_rx
            .await
            .map_err(|_| anyhow!("Turn lifecycle task ended before acknowledgement"))?
            .map_err(anyhow::Error::msg)?;
        Ok((turn, false))
        })
        .await
    }

    pub async fn interrupt_turn(&self, thread_id: &str, turn_id: &str) -> Result<TurnRecord> {
        {
            let mut active = self.active.lock().await;
            let Some(active_thread) = active.engines.get_mut(thread_id) else {
                bail!("Thread is not loaded");
            };
            let Some(active_turn) = active_thread.active_turn.as_mut() else {
                bail!("No active turn on thread {thread_id}");
            };
            if active_turn.turn_id != turn_id {
                bail!("Turn {turn_id} is not active on thread {thread_id}");
            }
            active_turn.interrupt_requested = true;
            // Wake the monitor's approval wait so it can consume the Engine's
            // terminal receipt. Revoke only this turn's minted capabilities.
            // Registration takes the same active lock, so a late event cannot
            // install a new waiter after this sweep.
            self.pending_approvals.lock().retain(|_, entry| {
                entry.thread_id != thread_id || entry.request.turn_id != turn_id
            });
            if let Some(compaction_id) = active_turn.compaction_id.as_deref() {
                active_thread.engine.cancel_compaction(compaction_id)?;
            } else {
                active_thread.engine.cancel();
            }
            touch_lru(&mut active.lru, thread_id);

View on GitHub (pinned to 73e0f67d83)