Hmbown/CodeWhale · warning

Thread engine changed while preparing steer; retry

Error message

Thread engine changed while preparing steer; retry

What it means

Thrown during steer preparation when the engine operation channel bound to the active thread no longer matches the engine channel the caller holds. This means the thread's engine was reloaded or replaced between the caller fetching the engine and the steer being prepared, so the steer would be sent to a stale engine.

Solutions

  1. Retry the steer: re-fetch the thread and its current engine and call steer again — the error message explicitly says 'retry'.
  2. Re-resolve the route/engine immediately before steering instead of caching the engine handle across awaits.
  3. Serialize engine reload operations with steer preparation using the store's mutation locks or an engine-generation check.

Example fix

// before
let engine = cached_engine.clone();
runtime.steer_with_engine(thread_id, turn_id, &engine, item).await?;
// after
// steer is safe to retry: reload the engine fresh on 'Thread engine changed'
match runtime.steer(thread_id, turn_id, item).await {
    Err(e) if e.to_string().contains("engine changed") => {
        runtime.steer(thread_id, turn_id, item).await?; // re-fetched engine
    }
    other => other?,
}
Defensive patterns

Strategy: retry

Validate before calling

// Re-resolve the engine immediately before steering instead of caching it
let thread = runtime.get_thread(thread_id).await?;
let _engine = runtime.ensure_engine_loaded(&thread).await?; // fresh handle
// then steer

Type guard

fn engine_is_current(active: &ActiveThreadState, engine: &EngineHandle) -> bool {
    active.engine.tx_op.same_channel(&engine.tx_op)
}

Try / catch

const MAX_RETRIES: usize = 2;
for _ in 0..MAX_RETRIES {
    match runtime.steer(thread_id, turn_id, item.clone()).await {
        Err(e) if e.to_string().contains("engine changed") => continue, // re-fetch and retry
        other => break other,
    }
}

Prevention

When it happens

Trigger: Calling steer with a pre-resolved engine whose `tx_op` fails `same_channel()` against the active thread's current engine — typically caused by a concurrent engine reload, model/route switch, or thread reconnect while the steer was being prepared.

Common situations: User switches model or the runtime reloads the engine while a steer request is in flight; background compaction or reconnect replaces the engine; retry loop in the caller races a config change.

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/a2193698104130d2. Report an issue: GitHub.

Appendix: source

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

            started_at: Some(now),
            ended_at: None,
        };
        let settle_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)?;
                // `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,

View on GitHub (pinned to 73e0f67d83)