{"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":"http","errorClass":"anyhow::Error","httpStatus":400,"severity":"error","filePath":"crates/tui/src/runtime_threads.rs","lineNumber":6289,"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                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) => {\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}\"\n                        )),","sourceCodeStart":6271,"sourceCodeEnd":6307,"githubUrl":"https://github.com/Hmbown/CodeWhale/blob/0c42157ee52f9d55af2b506d71b46249910f77d3/crates/tui/src/runtime_threads.rs#L6271-L6307","documentation":"Inside the turn_mutation lock, steer_turn reloads the persisted TurnRecord and finds status != RuntimeTurnStatus::InProgress (crates/tui/src/runtime_threads.rs:6289). Disk state outranks the in-memory active_turn slot, so even though memory says the turn is active, the store says it ended; the steer is refused before any item is saved.","triggerScenarios":"The turn completes and is persisted between the in-memory check and load_turn; recovery logic marks the turn Interrupted/Failed while a steer is being prepared; external or manual edits to the turn record during a run.","commonSituations":"Fast-finishing turns racing a queued steer; crash-recovery sweeping InProgress turns to Failed while the UI steers; tooling that rewrites store records out-of-band.","solutions":["Treat this as turn-finished: re-check status and send a new message instead of steering","Retry once only after confirming the turn is still InProgress in the store","Avoid mutating runtime store records externally while turns run","If it persists with a genuinely running turn, inspect for store write failures that flipped status"],"exampleFix":"// before\nruntime.steer_turn(&thread_id, &turn_id, prompt).await?;\n\n// after\nlet turn = store.load_turn(&turn_id)?;\nif turn.status == RuntimeTurnStatus::InProgress {\n    runtime.steer_turn(&thread_id, &turn_id, prompt).await?;\n} else {\n    runtime.send_message(&thread_id, prompt).await?;\n}","handlingStrategy":"validation","validationCode":"// Read the persisted status right before steering.\nlet turn = store.load_turn(turn_id)?;\nif turn.status != RuntimeTurnStatus::InProgress {\n    return runtime.send_message(thread_id, prompt).await; // turn already ended\n}\nruntime.steer_turn(thread_id, turn_id, prompt).await","typeGuard":"fn turn_is_steerable(status: &RuntimeTurnStatus) -> bool {\n    matches!(status, RuntimeTurnStatus::InProgress)\n}","tryCatchPattern":"match runtime.steer_turn(thread_id, turn_id, prompt).await {\n    Ok(receipt) => { /* steered */ }\n    Err(err) if err.to_string().contains(\"no longer in progress\") => {\n        // Disk says the turn ended: deliver as a new message instead.\n        runtime.send_message(thread_id, prompt).await?;\n    }\n    Err(err) => return Err(err),\n}","preventionTips":["Prefer checking persisted turn status over in-memory state for steer decisions","Never edit runtime store records out-of-band while turns run","After crash recovery, reconcile all InProgress turns before allowing steers","Fall back to send_message on any turn-ended error instead of surfacing it"],"tags":["rust","persistence","turn-lifecycle","steering","race-condition"],"backgroundTag":"stale-state-mismatch","analyzedSha":"0c42157ee52f9d55af2b506d71b46249910f77d3","analyzedAt":"2026-08-20T21:50:45.477Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}