{"record":{"id":"4c90d5e1097f9e75","repo":"Hmbown/CodeWhale","slug":"turn-turn-id-is-no-longer-in-progress-and-cannot","errorCode":null,"errorMessage":"Turn {turn_id} is no longer in progress and cannot be steered","messagePattern":"Turn (.+?) is no longer in progress and cannot be steered","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"warning","filePath":"crates/tui/src/runtime_threads.rs","lineNumber":10453,"sourceCode":"                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,\n                Err(error) => {\n                    let cleanup = self.store.remove_item(&item.id);\n                    return match cleanup {\n                        Ok(()) => Err(error),\n                        Err(cleanup_error) => Err(anyhow!(\n                            \"Failed to persist steer: {error}; cleanup also failed: {cleanup_error}\"","sourceCodeStart":10435,"sourceCodeEnd":10471,"githubUrl":"https://github.com/Hmbown/CodeWhale/blob/73e0f67d83c59909b571efdfc88c4bc28c309cb1/crates/tui/src/runtime_threads.rs#L10435-L10471","documentation":"Thrown in the steer persistence step under the turn mutation lock: the stored turn's status is no longer `InProgress`, so the steer cannot be recorded or delivered. The turn finished (completed, failed, or was interrupted) between the initial active-turn check and the persistence closure.","triggerScenarios":"Calling steer with a turn_id whose persisted RuntimeTurnRecord status has transitioned away from InProgress (e.g. Completed, Failed, Interrupted) by the time `store.load_turn` runs inside the steer path.","commonSituations":"Model finished its answer while the user was composing a steering message; turn errored out concurrently; a queued steer raced turn settlement and lost the lock race.","solutions":["Treat the message as content for a new turn: start a fresh turn with the same item instead of steering.","Check the turn's status (via load_turn or turn state events) before steering and fall back to a new turn when it is not InProgress.","Have the caller handle this error by surfacing 'turn already finished' to the user and offering to continue the conversation with a new turn."],"exampleFix":"// before\nruntime.steer(thread_id, turn_id, item).await?;\n// after\nif runtime.turn_status(thread_id, turn_id) != Some(TurnStatus::InProgress) {\n    runtime.start_turn(thread_id, item).await?;\n} else {\n    runtime.steer(thread_id, turn_id, item).await?;\n}","handlingStrategy":"validation","validationCode":"let turn = runtime.load_turn(turn_id)?;\nif turn.status != RuntimeTurnStatus::InProgress {\n    // fall back to a new turn instead of steering\n    runtime.start_turn(thread_id, item).await?;\n    return Ok(());\n}","typeGuard":"fn is_steerable(status: &RuntimeTurnStatus) -> bool {\n    matches!(status, RuntimeTurnStatus::InProgress)\n}","tryCatchPattern":"match runtime.steer(thread_id, turn_id, item).await {\n    Err(e) if e.to_string().contains(\"no longer in progress\") => {\n        runtime.start_turn(thread_id, item).await // deliver as a new turn\n    }\n    other => other,\n}","preventionTips":["Subscribe to turn status events and steer only while the turn is InProgress.","Treat finished turns as new-conversation context, not steer targets.","Handle the steer error by falling back to a new turn so user input is never dropped."],"tags":["tui","steer","turn-lifecycle","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"}