Hmbown/CodeWhale · warning
Turn is no longer in progress and cannot be steered
Error message
Turn {turn_id} is no longer in progress and cannot be steered What it means
Thrown in the steer persistence step under the turn mutation lock: the stored turn's status is no longer `InProgress`, so the steer cannot be recorded or delivered. The turn finished (completed, failed, or was interrupted) between the initial active-turn check and the persistence closure.
Solutions
- Treat the message as content for a new turn: start a fresh turn with the same item instead of steering.
- Check the turn's status (via load_turn or turn state events) before steering and fall back to a new turn when it is not InProgress.
- Have the caller handle this error by surfacing 'turn already finished' to the user and offering to continue the conversation with a new turn.
Example fix
// before
runtime.steer(thread_id, turn_id, item).await?;
// after
if runtime.turn_status(thread_id, turn_id) != Some(TurnStatus::InProgress) {
runtime.start_turn(thread_id, item).await?;
} else {
runtime.steer(thread_id, turn_id, item).await?;
} Defensive patterns
Strategy: validation
Validate before calling
let turn = runtime.load_turn(turn_id)?;
if turn.status != RuntimeTurnStatus::InProgress {
// fall back to a new turn instead of steering
runtime.start_turn(thread_id, item).await?;
return Ok(());
} Type guard
fn is_steerable(status: &RuntimeTurnStatus) -> bool {
matches!(status, RuntimeTurnStatus::InProgress)
} Try / catch
match runtime.steer(thread_id, turn_id, item).await {
Err(e) if e.to_string().contains("no longer in progress") => {
runtime.start_turn(thread_id, item).await // deliver as a new turn
}
other => other,
} Prevention
- Subscribe to turn status events and steer only while the turn is InProgress.
- Treat finished turns as new-conversation context, not steer targets.
- Handle the steer error by falling back to a new turn so user input is never dropped.
When it happens
Trigger: Calling steer with a turn_id whose persisted RuntimeTurnRecord status has transitioned away from InProgress (e.g. Completed, Failed, Interrupted) by the time `store.load_turn` runs inside the steer path.
Common situations: Model finished its answer while the user was composing a steering message; turn errored out concurrently; a queued steer raced turn settlement and lost the lock race.
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
- Thread engine changed while preparing steer; retry
- Turn is stopping and cannot be steered
- Turn moved on before the steer reached the model;
- No active turn on thread
- Steer settlement task ended before acknowledgement
AI-assisted analysis of Hmbown/CodeWhale@73e0f67d83 (2026-09-22).
Data as JSON: /api/errors/4c90d5e1097f9e75.
Report an issue: GitHub.
Appendix: source
Thrown at crates/tui/src/runtime_threads.rs:10453
bail!("Thread is not loaded");
};
let Some(active_turn) = active_thread.active_turn.as_ref() 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}");
}
if active_turn.interrupt_requested {
bail!("Turn {turn_id} is stopping and cannot be steered");
}
if !active_thread.engine.tx_op.same_channel(&engine.tx_op) {
bail!("Thread engine changed while preparing steer; retry");
}
let _turn_mutation = self.store.turn_mutation.lock();
let persistence = (|| -> Result<TurnRecord> {
let mut turn = self.store.load_turn(turn_id)?;
if turn.status != RuntimeTurnStatus::InProgress {
bail!("Turn {turn_id} is no longer in progress and cannot be steered");
}
self.store.save_item(&item)?;
// `steer_count` counts steers the model actually received, so
// it rises in the settlement path, not here.
if !turn.item_ids.iter().any(|id| id == &item.id) {
turn.item_ids.push(item.id.clone());
}
self.store.save_turn(&turn)?;
Ok(turn)
})();
let turn = match persistence {
Ok(turn) => turn,
Err(error) => {
let cleanup = self.store.remove_item(&item.id);
return match cleanup {
Ok(()) => Err(error),
Err(cleanup_error) => Err(anyhow!(
"Failed to persist steer: {error}; cleanup also failed: {cleanup_error}"View on GitHub (pinned to 73e0f67d83)