cjpais/Handy · error · anyhow::Error

Model failed to load after auto-load attempt. Please check y

Error message

Model failed to load after auto-load attempt. Please check your model settings.

What it means

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.

Source

Thrown at src-tauri/src/managers/transcription.rs:1236

        // Non-whisper archs (e.g. Voxtral Small) can advertise
        // Feature::InitialPrompt yet reject the whisper-kind run extension
        // with INVALID_ARG, so the whisper extension must be gated on the
        // arch, not on the feature (see #1601).
        let mut model_is_whisper = false;

        // Perform transcription with the appropriate engine.
        // We use catch_unwind to prevent engine panics from poisoning the mutex,
        // which would make the app hang indefinitely on subsequent operations.
        let (result, output_language, model_languages) = {
            let mut engine_guard = self.lock_engine();

            // Take the engine out so we own it during transcription.
            // If the engine panics, we simply don't put it back (effectively unloading it)
            // instead of poisoning the mutex.
            let mut engine = match engine_guard.take() {
                Some(e) => e,
                None => {
                    return Err(anyhow::anyhow!(
                        "Model failed to load after auto-load attempt. Please check your model settings."
                    ));
                }
            };

            // Release the lock before transcribing — no mutex held during the engine call
            drop(engine_guard);

            // Probe live transcribe-cpp capabilities once (cheap GGUF-metadata
            // reads); the loaded session is the source of truth, not the
            // ModelManager copy. The whisper run extension is kind-tagged, so
            // non-whisper archs (parakeet, voxtral, …) reject it with
            // INVALID_ARG; attach it — and translate — only where supported.
            let mut model_supports_translate = false;
            let mut model_languages = self
                .model_manager
                .get_model_info(&active_model)
                .map(|info| info.supported_languages)

View on GitHub (pinned to 98a4d80cce)

Solutions

  1. Simply retry the transcription — the next attempt re-checks and auto-loads the model
  2. Avoid switching models while a recording is in progress; queue the switch until after output
  3. Lengthen or disable idle auto-unload if the race recurs
  4. If it happens without any switching, inspect logs for a preceding 'engine panicked' unload

Example fix

// before
let text = tm.transcribe(audio)?; // can hit the take() race mid-call

// after — retry once; the retry path waits for/starts a fresh load
let text = match tm.transcribe(audio.clone()) {
    Ok(t) => t,
    Err(e) if e.to_string().contains("auto-load attempt") => {
        tm.initiate_model_load();
        wait_for_loading_completion(&app_handle);
        tm.transcribe(audio)?
    }
    Err(e) => return Err(e),
};
Defensive patterns

Strategy: retry

Validate before calling

// Reduce the race window: re-check right before the call and avoid model switches while recording
if recording_in_progress() { defer_model_switch_until_idle(); }
if !tm.is_model_loaded() { tm.initiate_model_load(); wait_for_loading_completion(&app_handle); }

Try / catch

match tm.transcribe(audio.clone()) {
    Err(e) if e.to_string().contains("auto-load attempt") => {
        // Engine was swapped/unloaded mid-call; next attempt reloads — retry once
        thread::sleep(Duration::from_millis(250));
        tm.initiate_model_load();
        wait_for_loading_completion(&app_handle);
        tm.transcribe(audio)
    }
    other => other,
}

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Related errors


AI-assisted analysis of cjpais/Handy@98a4d80cce (2026-08-16). Data as JSON: /api/errors/095fd41e7e3af802. Report an issue: GitHub.