Hmbown/CodeWhale · error · anyhow::Error

Thread engine changed while preparing steer; retry

Error message

Thread engine changed while preparing steer; retry

What it means

steer_turn captured an engine handle earlier, but under the active lock the registered engine's tx_op channel is no longer the same channel (crates/tui/src/runtime_threads.rs:6283). The thread's engine was swapped (reload after config/route change, recycle, unload/reload) while the steer was being prepared. The message explicitly says retry: this is a transient optimistic-concurrency failure, not corruption.

Source

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

            started_at: Some(now),
            ended_at: Some(now),
        };
        let receipt_rx = {
            let mut active = self.active.lock().await;
            let Some(active_thread) = active.engines.get(thread_id) else {
                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)?;
                turn.steer_count = turn.steer_count.saturating_add(1);
                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) => {

View on GitHub (pinned to 0c42157ee5)

Solutions

  1. Retry the steer after re-reading the thread/engine state (the error is designed to be retried)
  2. Serialize settings changes with in-flight turn operations per thread
  3. Keep steer paths short so the engine-generation window is small
  4. If persistent, check whether something is thrashing the engine (repeated reloads)

Example fix

// before
let receipt = runtime.steer_turn(&thread_id, &turn_id, prompt).await?;

// after
let receipt = loop {
    match runtime.steer_turn(&thread_id, &turn_id, prompt).await {
        Ok(r) => break r,
        Err(e) if e.to_string().contains("retry") => continue, // engine swapped; retry
        Err(e) => return Err(e),
    }
};
Defensive patterns

Strategy: retry

Validate before calling

// Cheap pre-check: ensure the engine generation you hold is still current.
let engine = runtime.current_engine(thread_id).await;
if engine.tx_op.same_channel(&captured_engine.tx_op) {
    runtime.steer_turn(thread_id, turn_id, prompt).await
} else {
    retry_with_fresh_state(thread_id, turn_id, prompt).await
}

Try / catch

let mut attempts = 0;
loop {
    match runtime.steer_turn(thread_id, turn_id, prompt).await {
        Ok(receipt) => break Ok(receipt),
        Err(err) if attempts < 3 && err.to_string().contains("retry") => {
            attempts += 1;
            tokio::time::sleep(Duration::from_millis(50)).await;
        }
        Err(err) => break Err(err),
    }
}

Prevention

When it happens

Trigger: Changing model/route/thread settings while a steer is in flight; engine recycled after an error; LRU eviction and reload of the thread engine racing the steer; two clients steering through different engine generations.

Common situations: Settings UI applies a route change concurrently with a user steering a running turn; tests that mutate config mid-turn; long steer paths (awaiting reserve_steer permit) overlapping an engine swap.

Related errors


AI-assisted analysis of Hmbown/CodeWhale@0c42157ee5 (2026-08-20). Data as JSON: /api/errors/a2193698104130d2. Report an issue: GitHub.