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

  1. Retry the turn preparation after the concurrent goal mutation completes — the error is explicitly designed as a retry signal.
  2. Serialize goal edits and turn starts from the UI so they cannot overlap on the same thread.
  3. 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

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


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, &current_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)