Hmbown/CodeWhale · error
Goal changed while preparing the turn; retry
Error message
Goal changed while preparing the turn; retry
What it means
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.
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.
Example fix
// before
engine.prepare_turn(thread_id, turn_goal)?; // races goal edit
// after
loop {
match engine.prepare_turn(thread_id, &load_goal(thread_id)?) {
Ok(t) => break t,
Err(e) if is_retryable_preparation(&e) => continue,
Err(e) => return Err(e),
}
} Defensive patterns
Strategy: retry
Validate before calling
let goal_now = store.load_goal(thread_id)?;
if goal_now != turn_goal { /* resnapshot goal before preparing */ } Try / catch
for _ in 0..3 {
match engine.prepare_turn(&id, &turn_goal) {
Ok(t) => break t,
Err(e) if e.to_string().contains("Goal changed") => { turn_goal = load_goal(&id)?; continue; }
Err(e) => return Err(e),
}
} Prevention
- 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
When it happens
Trigger: 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.
Common situations: 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.
Understand the failure class
Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.
Related errors
- Runtime turn operation is already being claimed; retry
- Thread execution settings changed while preparing the turn…
- A saved trace identifier collided. Try saving again.
- a window change is already in progress
- Another pet owner is running
AI-assisted analysis of Hmbown/CodeWhale@433685b202 (2026-09-15).
Data as JSON: /api/errors/0495eb3471d3eaf3.
Report an issue: GitHub.
Appendix: source
Thrown at crates/tui/src/runtime_threads.rs:9425
.transpose()?;
// A concurrent exact retry may have crossed the first lookup
// before the original request committed its binding. Recheck
// under the same claim lock before inspecting active-turn state or
// persisting/sending anything.
if let Some(operation) = operation.as_ref()
&& let Some(original_turn) = self.replay_turn_for_operation(operation)?
{
return Ok(original_turn);
}
let Some(state) = active.engines.get_mut(thread_id) else {
bail!("Thread engine not loaded");
};
if state.active_turn.is_some() {
bail!("Thread already has an active turn");
}
let _goal_mutation = self.store.goal_mutation.lock();
if self.store.load_goal(thread_id)? != turn_goal {
bail!("Goal changed while preparing the turn; retry");
}
engine.restore_runtime_goal(turn_goal.as_ref())?;
let _thread_mutation = self.store.thread_mutation.lock();
let mut current_thread = self.store.load_thread(thread_id)?;
if !thread_execution_state_matches(&thread, ¤t_thread) {
bail!("Thread execution settings changed while preparing the turn; retry");
}
let previous_active_route = (state.route_identity.clone(), state.route_model.clone());
state.active_turn = Some(ActiveTurnState {
goal_id: turn_goal.as_ref().map(|goal| goal.goal_id.clone()),
turn_id: turn_id.clone(),
interrupt_requested: false,
compaction_id: None,
});
state.route_identity = provider_identity;
state.route_model.clone_from(&model);
let persistence_result = (|| -> Result<()> {View on GitHub (pinned to 433685b202)