aaif-goose/goose · error

Failed to lock transcriber: {}

Error message

Failed to lock transcriber: {}

What it means

The local Whisper transcriber is cached in a global std::sync::Mutex (LOCAL_TRANSCRIBER) shared across calls so the model loads once. `.lock()` returns Err only when the mutex is poisoned — another thread panicked while holding it (model load, file read, or inference panic). The {error} is the std::sync::PoisonError.

Source

Thrown at crates/goose/src/dictation/providers.rs:150

}

#[cfg(feature = "local-inference")]
pub async fn transcribe_local(audio_bytes: Vec<u8>) -> Result<String> {
    tokio::task::spawn_blocking(move || {
        let config = Config::global();
        let model_id = config
            .get(LOCAL_WHISPER_MODEL_CONFIG_KEY, false)
            .ok()
            .and_then(|v| v.as_str().map(|s| s.to_string()))
            .ok_or_else(|| anyhow::anyhow!("Local Whisper model not configured"))?;

        let model = super::whisper::get_model(&model_id)
            .ok_or_else(|| anyhow::anyhow!("Unknown model: {}", model_id))?;
        let model_path = model.local_path();

        let mut transcriber_lock = LOCAL_TRANSCRIBER
            .lock()
            .map_err(|e| anyhow::anyhow!("Failed to lock transcriber: {}", e))?;

        let model_path_str = model_path.to_string_lossy().to_string();
        let needs_reload = match transcriber_lock.as_ref() {
            None => true,
            Some((cached_path, _)) => cached_path != &model_path_str,
        };

        if needs_reload {
            tracing::info!("Loading Whisper model from: {}", model_path.display());

            let transcriber = super::whisper::WhisperTranscriber::new_with_tokenizer(
                &model_id,
                &model_path,
                WHISPER_TOKENIZER_JSON,
            )?;

            *transcriber_lock = Some((model_path_str, transcriber));
        }

View on GitHub (pinned to 3810898a74)

Solutions

  1. Look upward in the logs for the ORIGINAL panic that poisoned the mutex — fixing that is the real fix
  2. Restart the process: poisoning is per-process state and a fresh start clears it
  3. If the model file is corrupt (partial download), remove it so the next start re-downloads cleanly
Defensive patterns

Strategy: try-catch

Try / catch

match transcribe_local(audio).await {
    Err(e) if e.to_string().contains("Failed to lock transcriber") => {
        // mutex poisoned by an earlier panic: surface the original panic from logs,
        // stop accepting dictation, and restart the process to recover
    }
    other => other,
}

Prevention

When it happens

Trigger: A previous transcribe_local call panicked inside spawn_blocking while holding the lock (corrupted model file, decode error, IO panic); every subsequent transcription in the same process then fails immediately with this error.

Common situations: Interrupted model download leaving a truncated file; disk errors while mmapping the model; a panic during transcription after which the process keeps serving requests.

Related errors


AI-assisted analysis of aaif-goose/goose@3810898a74 (2026-08-16). Data as JSON: /api/errors/a154ae02a272d6b8. Report an issue: GitHub.