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
- Retry the steer: re-fetch the thread and its current engine and call steer again — the error message explicitly says 'retry'.
- Re-resolve the route/engine immediately before steering instead of caching the engine handle across awaits.
- 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
- Never cache engine handles across awaits in interactive paths.
- Retry once on 'engine changed' before surfacing the error to the user.
- Serialize engine reloads with steer/turn preparation using the store mutation locks.
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
- Thread execution settings changed while preparing…
- Turn is no longer in progress and cannot be steered
- Turn moved on before the steer reached the model;
- Steer settlement task ended before acknowledgement
- Turn is stopping and cannot be steered
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)