{"record":{"id":"0495eb3471d3eaf3","repo":"Hmbown/CodeWhale","slug":"goal-changed-while-preparing-the-turn-retry","errorCode":null,"errorMessage":"Goal changed while preparing the turn; retry","messagePattern":"Goal changed while preparing the turn; retry","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/tui/src/runtime_threads.rs","lineNumber":9425,"sourceCode":"                .transpose()?;\n            // A concurrent exact retry may have crossed the first lookup\n            // before the original request committed its binding. Recheck\n            // under the same claim lock before inspecting active-turn state or\n            // persisting/sending anything.\n            if let Some(operation) = operation.as_ref()\n                && let Some(original_turn) = self.replay_turn_for_operation(operation)?\n            {\n                return Ok(original_turn);\n            }\n            let Some(state) = active.engines.get_mut(thread_id) else {\n                bail!(\"Thread engine not loaded\");\n            };\n            if state.active_turn.is_some() {\n                bail!(\"Thread already has an active turn\");\n            }\n            let _goal_mutation = self.store.goal_mutation.lock();\n            if self.store.load_goal(thread_id)? != turn_goal {\n                bail!(\"Goal changed while preparing the turn; retry\");\n            }\n            engine.restore_runtime_goal(turn_goal.as_ref())?;\n            let _thread_mutation = self.store.thread_mutation.lock();\n            let mut current_thread = self.store.load_thread(thread_id)?;\n            if !thread_execution_state_matches(&thread, &current_thread) {\n                bail!(\"Thread execution settings changed while preparing the turn; retry\");\n            }\n            let previous_active_route = (state.route_identity.clone(), state.route_model.clone());\n            state.active_turn = Some(ActiveTurnState {\n                goal_id: turn_goal.as_ref().map(|goal| goal.goal_id.clone()),\n                turn_id: turn_id.clone(),\n                interrupt_requested: false,\n                compaction_id: None,\n            });\n            state.route_identity = provider_identity;\n            state.route_model.clone_from(&model);\n\n            let persistence_result = (|| -> Result<()> {","sourceCodeStart":9407,"sourceCodeEnd":9443,"githubUrl":"https://github.com/Hmbown/CodeWhale/blob/433685b2024e7bc4c99e1e2e326bcad39b4d9d65/crates/tui/src/runtime_threads.rs#L9407-L9443","documentation":"Thrown while preparing a turn when the goal re-read from the store under the goal_mutation lock differs from the turn_goal snapshot captured earlier. The runtime serializes goal mutations, so a divergent goal means another actor changed the thread's goal mid-preparation and the caller must retry with fresh state.","triggerScenarios":"Calling the turn-preparation path (runtime_threads.rs:9425) while another task/user concurrently calls a goal-mutating operation (set/update goal) on the same thread between snapshot and lock acquisition.","commonSituations":"Editing the thread's goal in another pane while a turn is being prepared; a sync/agent process updating goals concurrently; retry storms after stale-goal failures.","solutions":["Retry the turn preparation after the concurrent goal mutation completes — the error is explicitly designed as a retry signal.","Serialize goal edits and turn starts from the UI so they cannot overlap on the same thread.","Re-read the thread's current goal and revalidate it before resubmitting the turn."],"exampleFix":"// before\nengine.prepare_turn(thread_id, turn_goal)?; // races goal edit\n// after\nloop {\n    match engine.prepare_turn(thread_id, &load_goal(thread_id)?) {\n        Ok(t) => break t,\n        Err(e) if is_retryable_preparation(&e) => continue,\n        Err(e) => return Err(e),\n    }\n}","handlingStrategy":"retry","validationCode":"let goal_now = store.load_goal(thread_id)?;\nif goal_now != turn_goal { /* resnapshot goal before preparing */ }","typeGuard":null,"tryCatchPattern":"for _ in 0..3 {\n    match engine.prepare_turn(&id, &turn_goal) {\n        Ok(t) => break t,\n        Err(e) if e.to_string().contains(\"Goal changed\") => { turn_goal = load_goal(&id)?; continue; }\n        Err(e) => return Err(e),\n    }\n}","preventionTips":["Serialize goal edits with turn submission in the UI","Always snapshot the goal immediately before preparing the turn","Treat this as a bounded retry, not a fatal error"],"tags":["concurrency","optimistic-concurrency","retryable"],"backgroundTag":"invalid-state-transition","analyzedSha":"433685b2024e7bc4c99e1e2e326bcad39b4d9d65","analyzedAt":"2026-09-15T12:24:24.634Z","contentChangedAt":"2026-09-15T12:24:24.634Z","schemaVersion":2},"datasetVersion":"2026-09-22T01:17:13.364Z"}