{"record":{"id":"a4e42789aac49fc8","repo":"Hmbown/CodeWhale","slug":"thread-execution-settings-changed-while-preparing-a4e427","errorCode":null,"errorMessage":"Thread execution settings changed while preparing compaction; retry","messagePattern":"Thread execution settings changed while preparing compaction; retry","errorType":"http","errorClass":"anyhow::Error","httpStatus":400,"severity":"error","filePath":"crates/tui/src/runtime_threads.rs","lineNumber":6431,"sourceCode":"            route: Box::new(route),\n            compaction: Box::new(compaction),\n        };\n        let permit = engine.tx_op.clone().reserve_owned().await.map_err(|_| {\n            anyhow!(\"Failed to trigger compaction: engine operation channel closed\")\n        })?;\n\n        let acceptance_rx = {\n            let mut active = self.active.lock().await;\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 _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 compaction; retry\");\n            }\n            let previous_active_route = (state.route_identity.clone(), state.route_model.clone());\n            state.active_turn = Some(ActiveTurnState {\n                turn_id: turn_id.clone(),\n                interrupt_requested: false,\n                compaction_id: Some(compaction_id),\n            });\n            state.route_identity = route_identity;\n            state.route_model = route_model;\n\n            let persistence_result = (|| -> Result<()> {\n                self.store.save_turn(&turn)?;\n                current_thread.latest_turn_id = Some(turn_id.clone());\n                current_thread.updated_at = now;\n                self.store.save_thread(&current_thread)\n            })();\n            if let Err(persistence_error) = persistence_result {\n                let cleanup_error = self.cleanup_unaccepted_turn_records(&turn_id, None).err();","sourceCodeStart":6413,"sourceCodeEnd":6449,"githubUrl":"https://github.com/Hmbown/CodeWhale/blob/0c42157ee52f9d55af2b506d71b46249910f77d3/crates/tui/src/runtime_threads.rs#L6413-L6449","documentation":"compact_thread re-loads the thread under the thread_mutation lock and compares it with the snapshot taken at call entry via thread_execution_state_matches (crates/tui/src/runtime_threads.rs:6431). A mismatch means execution-relevant settings (route/model/sandbox and friends) were changed on disk while the compaction was being prepared, so the armed compaction would run against stale settings; abort and retry.","triggerScenarios":"User changes model/route/sandbox settings while compaction start is in flight; another client edits the thread record; settings sync applying concurrently with compact.","commonSituations":"Settings UI applying changes just as the user clicks Compact; automation flipping routes mid-operation; shared thread edited from a second session.","solutions":["Retry compaction — it re-reads thread settings from the fresh record","Apply execution-settings changes only when the thread is idle to avoid racing turns/compactions","Serialize settings updates with compaction via the same mutation queue","If persistent, verify nothing is rewriting thread records in a loop"],"exampleFix":"// before\nlet turn = runtime.compact_thread(&thread_id, req).await?;\n\n// after\nlet turn = loop {\n    match runtime.compact_thread(&thread_id, req.clone()).await {\n        Ok(t) => break t,\n        Err(e) if e.to_string().contains(\"settings changed\") => continue, // re-read + retry\n        Err(e) => return Err(e),\n    }\n};","handlingStrategy":"retry","validationCode":"// Refresh the thread snapshot right before compacting so settings are current.\nlet thread = runtime.get_thread(thread_id).await?;\nassert_execution_settings_stable(&thread).await?;\nruntime.compact_thread(thread_id, req).await","typeGuard":null,"tryCatchPattern":"let mut attempts = 0;\nloop {\n    match runtime.compact_thread(thread_id, req.clone()).await {\n        Ok(turn) => break Ok(turn),\n        Err(err) if attempts < 3 && err.to_string().contains(\"settings changed\") => {\n            attempts += 1;\n            continue; // next attempt re-reads thread settings\n        }\n        Err(err) => return Err(err),\n    }\n}","preventionTips":["Apply execution-settings changes only when the thread is idle","Route settings mutations and compaction through the same serialization point","Re-read thread state immediately before expensive operations","Alert if settings flap repeatedly — something is rewriting thread records"],"tags":["rust","concurrency","compaction","settings","retryable"],"backgroundTag":"optimistic-concurrency-retry","analyzedSha":"0c42157ee52f9d55af2b506d71b46249910f77d3","analyzedAt":"2026-08-20T21:50:45.477Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-14T00:17:10.932Z"}