{"record":{"id":"095fd41e7e3af802","repo":"cjpais/Handy","slug":"model-failed-to-load-after-auto-load-attempt-plea","errorCode":null,"errorMessage":"Model failed to load after auto-load attempt. Please check your model settings.","messagePattern":"Model failed to load after auto-load attempt\\. Please check your model settings\\.","errorType":"exception","errorClass":"anyhow::Error","httpStatus":null,"severity":"error","filePath":"src-tauri/src/managers/transcription.rs","lineNumber":1236,"sourceCode":"        // Non-whisper archs (e.g. Voxtral Small) can advertise\n        // Feature::InitialPrompt yet reject the whisper-kind run extension\n        // with INVALID_ARG, so the whisper extension must be gated on the\n        // arch, not on the feature (see #1601).\n        let mut model_is_whisper = false;\n\n        // Perform transcription with the appropriate engine.\n        // We use catch_unwind to prevent engine panics from poisoning the mutex,\n        // which would make the app hang indefinitely on subsequent operations.\n        let (result, output_language, model_languages) = {\n            let mut engine_guard = self.lock_engine();\n\n            // Take the engine out so we own it during transcription.\n            // If the engine panics, we simply don't put it back (effectively unloading it)\n            // instead of poisoning the mutex.\n            let mut engine = match engine_guard.take() {\n                Some(e) => e,\n                None => {\n                    return Err(anyhow::anyhow!(\n                        \"Model failed to load after auto-load attempt. Please check your model settings.\"\n                    ));\n                }\n            };\n\n            // Release the lock before transcribing — no mutex held during the engine call\n            drop(engine_guard);\n\n            // Probe live transcribe-cpp capabilities once (cheap GGUF-metadata\n            // reads); the loaded session is the source of truth, not the\n            // ModelManager copy. The whisper run extension is kind-tagged, so\n            // non-whisper archs (parakeet, voxtral, …) reject it with\n            // INVALID_ARG; attach it — and translate — only where supported.\n            let mut model_supports_translate = false;\n            let mut model_languages = self\n                .model_manager\n                .get_model_info(&active_model)\n                .map(|info| info.supported_languages)","sourceCodeStart":1218,"sourceCodeEnd":1254,"githubUrl":"https://github.com/cjpais/Handy/blob/98a4d80cce8ad41efec2a419b59d9e81229a35d7/src-tauri/src/managers/transcription.rs#L1218-L1254","documentation":"A check-then-use race inside transcribe(): the engine existed at the first check (engine_guard.is_none() was false), but by the time the code re-locked and called engine_guard.take() the slot was empty. Something unloaded or switched the model between the two locks — load_model drops the old engine before building the new one, the idle watcher can unload, and the panic-recovery path deliberately leaves the slot empty. The (slightly stale) message tells the user to check model settings; the next transcription attempt will trigger a fresh load.","triggerScenarios":"Another thread calls load_model() (model switch) between the loaded-check at line 1181 and the take() at line 1233; idle watcher unloads the model mid-transcribe setup; a prior panicking call cleared current_model_id and the engine; CLI --transcribe-file races a settings-triggered reload.","commonSituations":"User changes the model in the UI right as a recording ends; push-to-talk released at the same moment the idle timeout fires; automated scripts toggling models while transcribing; flaky retest right after a panic unload.","solutions":["Simply retry the transcription — the next attempt re-checks and auto-loads the model","Avoid switching models while a recording is in progress; queue the switch until after output","Lengthen or disable idle auto-unload if the race recurs","If it happens without any switching, inspect logs for a preceding 'engine panicked' unload"],"exampleFix":"// before\nlet text = tm.transcribe(audio)?; // can hit the take() race mid-call\n\n// after — retry once; the retry path waits for/starts a fresh load\nlet text = match tm.transcribe(audio.clone()) {\n    Ok(t) => t,\n    Err(e) if e.to_string().contains(\"auto-load attempt\") => {\n        tm.initiate_model_load();\n        wait_for_loading_completion(&app_handle);\n        tm.transcribe(audio)?\n    }\n    Err(e) => return Err(e),\n};","handlingStrategy":"retry","validationCode":"// Reduce the race window: re-check right before the call and avoid model switches while recording\nif recording_in_progress() { defer_model_switch_until_idle(); }\nif !tm.is_model_loaded() { tm.initiate_model_load(); wait_for_loading_completion(&app_handle); }","typeGuard":null,"tryCatchPattern":"match tm.transcribe(audio.clone()) {\n    Err(e) if e.to_string().contains(\"auto-load attempt\") => {\n        // Engine was swapped/unloaded mid-call; next attempt reloads — retry once\n        thread::sleep(Duration::from_millis(250));\n        tm.initiate_model_load();\n        wait_for_loading_completion(&app_handle);\n        tm.transcribe(audio)\n    }\n    other => other,\n}","preventionTips":["Never switch models while a recording is being finalized; queue the switch","Lengthen the idle-unload timeout in settings for long pause/resume workflows","In automation scripts, serialize load and transcribe steps rather than running them concurrently","Treat repeat occurrences as a symptom of an underlying load failure and inspect the loading_failed event"],"tags":["rust","race-condition","model-lifecycle","transcription","state"],"backgroundTag":"model-not-loaded","analyzedSha":"98a4d80cce8ad41efec2a419b59d9e81229a35d7","analyzedAt":"2026-08-16T20:58:09.966Z","schemaVersion":2},"datasetVersion":"2026-08-16T23:17:17.608Z"}