{"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":"http","errorClass":"anyhow::Error","httpStatus":400,"severity":"error","filePath":"crates/tui/src/runtime_threads.rs","lineNumber":6283,"sourceCode":"            started_at: Some(now),\n            ended_at: Some(now),\n        };\n        let receipt_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                turn.steer_count = turn.steer_count.saturating_add(1);\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,\n                Err(error) => {","sourceCodeStart":6265,"sourceCodeEnd":6301,"githubUrl":"https://github.com/Hmbown/CodeWhale/blob/0c42157ee52f9d55af2b506d71b46249910f77d3/crates/tui/src/runtime_threads.rs#L6265-L6301","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Retry the steer after re-reading the thread/engine state (the error is designed to be retried)","Serialize settings changes with in-flight turn operations per thread","Keep steer paths short so the engine-generation window is small","If persistent, check whether something is thrashing the engine (repeated reloads)"],"exampleFix":"// before\nlet receipt = runtime.steer_turn(&thread_id, &turn_id, prompt).await?;\n\n// after\nlet receipt = loop {\n    match runtime.steer_turn(&thread_id, &turn_id, prompt).await {\n        Ok(r) => break r,\n        Err(e) if e.to_string().contains(\"retry\") => continue, // engine swapped; retry\n        Err(e) => return Err(e),\n    }\n};","handlingStrategy":"retry","validationCode":"// Cheap pre-check: ensure the engine generation you hold is still current.\nlet engine = runtime.current_engine(thread_id).await;\nif engine.tx_op.same_channel(&captured_engine.tx_op) {\n    runtime.steer_turn(thread_id, turn_id, prompt).await\n} else {\n    retry_with_fresh_state(thread_id, turn_id, prompt).await\n}","typeGuard":null,"tryCatchPattern":"let mut attempts = 0;\nloop {\n    match runtime.steer_turn(thread_id, turn_id, prompt).await {\n        Ok(receipt) => break Ok(receipt),\n        Err(err) if attempts < 3 && err.to_string().contains(\"retry\") => {\n            attempts += 1;\n            tokio::time::sleep(Duration::from_millis(50)).await;\n        }\n        Err(err) => break Err(err),\n    }\n}","preventionTips":["Treat 'engine changed ... retry' as transient by contract: always have a bounded retry wrapper","Apply model/route/settings changes only while the thread is idle","Keep steer call paths short so the engine-swap window stays narrow","Investigate engine thrash if retries routinely exceed one attempt"],"tags":["rust","concurrency","retryable","engine-lifecycle","steering"],"backgroundTag":"optimistic-concurrency-retry","analyzedSha":"0c42157ee52f9d55af2b506d71b46249910f77d3","analyzedAt":"2026-08-20T21:50:45.477Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-14T00:17:10.932Z"}