Hmbown/CodeWhale · error
Turn is not active on thread
Error message
Turn {turn_id} is not active on thread {thread_id} What it means
In interrupt_turn (crates/tui/src/runtime_threads.rs:10344), after finding an active turn, the manager compares its turn_id to the requested one. If they differ, it bails: each thread runs at most one turn, and you asked to interrupt a turn that is not the one currently executing on that thread.
Solutions
- Fetch the thread's current active turn_id (from turn lifecycle events) and pass that instead of the cached one.
- Re-read the turn record from the store to confirm the turn is still in progress before interrupting.
- If the stale turn already finished, treat the interrupt as done and skip the call.
Example fix
// before
let turn_id = last_seen_turn_id.clone();
manager.interrupt_turn(&thread_id, &turn_id).await?;
// after
let turn_id = current_active_turn_id(&thread_id).ok_or_else(|| anyhow!("no active turn"))?;
manager.interrupt_turn(&thread_id, &turn_id).await?; Defensive patterns
Strategy: validation
Validate before calling
// confirm the turn you hold is the one in flight
anyhow::ensure!(
current_active_turn_id(&thread_id) == Some(turn_id.to_string()),
"turn {} is not the active turn",
turn_id
); Type guard
fn is_current_turn(active: &ActiveTurn, turn_id: &str) -> bool { active.turn_id == turn_id } Try / catch
if let Err(e) = manager.interrupt_turn(&tid, &turn_id).await {
if e.to_string().contains("is not active") { refresh_turn_id(&tid).await?; }
else { return Err(e); }
} Prevention
- Keep one authoritative 'current turn id' per thread updated from turn events
- Never cache turn_id across automatic retries or follow-up turns
- Read the turn record from the store before interrupting to confirm InProgress status
When it happens
Trigger: Calling interrupt_turn(thread_id, turn_id) with a stale or mismatched turn_id while a different turn_id is active on the thread (e.g. the old turn ended and a new one started).
Common situations: Caching turn IDs across turns; interrupting from a UI row bound to an older turn after the user already started a follow-up turn; concurrent automations each tracking their own turn IDs.
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
- No active turn 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/e387c37b253defd2.
Report an issue: GitHub.
Appendix: source
Thrown at crates/tui/src/runtime_threads.rs:10345
.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);
}
self.emit_event(View on GitHub (pinned to 73e0f67d83)