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

  1. Treat the message as content for a new turn: start a fresh turn with the same item instead of steering.
  2. 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.
  3. 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

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


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)