{"record":{"id":"a2193698104130d2","repo":"Hmbown/CodeWhale","slug":"thread-engine-changed-while-preparing-steer-retry","errorCode":null,"errorMessage":"Thread engine changed while preparing steer; retry","messagePattern":"Thread engine changed while preparing steer; retry","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"warning","filePath":"crates/tui/src/runtime_threads.rs","lineNumber":10447,"sourceCode":"            started_at: Some(now),\n            ended_at: None,\n        };\n        let settle_rx = {\n            let mut active = self.active.lock().await;\n            let Some(active_thread) = active.engines.get(thread_id) else {\n                bail!(\"Thread is not loaded\");\n            };\n            let Some(active_turn) = active_thread.active_turn.as_ref() else {\n                bail!(\"No active turn on thread {thread_id}\");\n            };\n            if active_turn.turn_id != turn_id {\n                bail!(\"Turn {turn_id} is not active on thread {thread_id}\");\n            }\n            if active_turn.interrupt_requested {\n                bail!(\"Turn {turn_id} is stopping and cannot be steered\");\n            }\n            if !active_thread.engine.tx_op.same_channel(&engine.tx_op) {\n                bail!(\"Thread engine changed while preparing steer; retry\");\n            }\n            let _turn_mutation = self.store.turn_mutation.lock();\n            let persistence = (|| -> Result<TurnRecord> {\n                let mut turn = self.store.load_turn(turn_id)?;\n                if turn.status != RuntimeTurnStatus::InProgress {\n                    bail!(\"Turn {turn_id} is no longer in progress and cannot be steered\");\n                }\n                self.store.save_item(&item)?;\n                // `steer_count` counts steers the model actually received, so\n                // it rises in the settlement path, not here.\n                if !turn.item_ids.iter().any(|id| id == &item.id) {\n                    turn.item_ids.push(item.id.clone());\n                }\n                self.store.save_turn(&turn)?;\n                Ok(turn)\n            })();\n            let turn = match persistence {\n                Ok(turn) => turn,","sourceCodeStart":10429,"sourceCodeEnd":10465,"githubUrl":"https://github.com/Hmbown/CodeWhale/blob/73e0f67d83c59909b571efdfc88c4bc28c309cb1/crates/tui/src/runtime_threads.rs#L10429-L10465","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before\nlet engine = cached_engine.clone();\nruntime.steer_with_engine(thread_id, turn_id, &engine, item).await?;\n// after\n// steer is safe to retry: reload the engine fresh on 'Thread engine changed'\nmatch runtime.steer(thread_id, turn_id, item).await {\n    Err(e) if e.to_string().contains(\"engine changed\") => {\n        runtime.steer(thread_id, turn_id, item).await?; // re-fetched engine\n    }\n    other => other?,\n}","handlingStrategy":"retry","validationCode":"// Re-resolve the engine immediately before steering instead of caching it\nlet thread = runtime.get_thread(thread_id).await?;\nlet _engine = runtime.ensure_engine_loaded(&thread).await?; // fresh handle\n// then steer","typeGuard":"fn engine_is_current(active: &ActiveThreadState, engine: &EngineHandle) -> bool {\n    active.engine.tx_op.same_channel(&engine.tx_op)\n}","tryCatchPattern":"const MAX_RETRIES: usize = 2;\nfor _ in 0..MAX_RETRIES {\n    match runtime.steer(thread_id, turn_id, item.clone()).await {\n        Err(e) if e.to_string().contains(\"engine changed\") => continue, // re-fetch and retry\n        other => break other,\n    }\n}","preventionTips":["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."],"tags":["tui","steer","engine-reload","retry","race-condition"],"backgroundTag":"invalid-state-transition","analyzedSha":"73e0f67d83c59909b571efdfc88c4bc28c309cb1","analyzedAt":"2026-09-22T01:30:00.501Z","contentChangedAt":"2026-09-22T01:30:00.501Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}