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
- Check the thread has an in-flight turn before calling interrupt_turn (track turn status from turn.* events).
- Treat this error as a no-op: the desired end state (no running turn) is already true; catch and ignore.
- 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
- Only offer interrupt in the UI while a turn is visibly running
- Derive interrupt availability from turn lifecycle events, not cached turn IDs
- Treat 'no active turn' interrupts as success — the goal state is already reached
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
- Turn is not active on thread
- Turn is no longer in progress and cannot be steered
- Turn is stopping and cannot be steered
- a window change is already in progress
- Automation admission execution ownership is unverified or…
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)