Kuberwastaken/claurst · error

voice thread runtime

Error message

voice thread runtime

What it means

This panic wraps creation of a dedicated single-threaded tokio runtime on the voice recording OS thread: `tokio::runtime::Builder::new_current_thread().enable_all().build().expect(...)`. Runtime build only fails if tokio's I/O and timer drivers cannot be initialized (e.g. epoll/kqueue unavailable), so the code treats it as unrecoverable on the voice thread.

Solutions

  1. Verify the process runs on a supported platform where mio can create its event loop (Linux epoll / macOS kqueue); check seccomp/container policies blocking `epoll_create1`/`eventfd`.
  2. Test voice startup early with a minimal current_thread runtime probe to surface driver issues at boot.
  3. Ensure tokio's `rt`, `time`, and `net` features are enabled consistently across the workspace (mixed tokio versions can cause driver conflicts).
  4. Make the thread send `VoiceEvent::Error("failed to start voice runtime: ...")` instead of panicking, so the UI degrades gracefully.

Example fix

// before
let rt = tokio::runtime::Builder::new_current_thread()
    .enable_all()
    .build()
    .expect("voice thread runtime");
// after
let rt = match tokio::runtime::Builder::new_current_thread().enable_all().build() {
    Ok(rt) => rt,
    Err(e) => {
        let _ = event_tx.blocking_send(VoiceEvent::Error(format!("voice runtime init failed: {e}")));
        return;
    }
};
Defensive patterns

Strategy: try-catch

Validate before calling

// Probe driver availability before enabling voice:
fn voice_runtime_ok() -> bool {
    tokio::runtime::Builder::new_current_thread()
        .enable_all()
        .build()
        .is_ok()
}

Try / catch

// The panic happens on the spawned thread — observe it via the event channel:
while let Some(ev) = voice_events.recv().await {
    match ev {
        VoiceEvent::Error(msg) if msg.contains("voice thread runtime") => {
            // disable voice feature, show user-facing message
        }
        _ => {}
    }
}

Prevention

When it happens

Trigger: Calling `start_recording` when the spawned thread's `tokio::runtime::Builder::new_current_thread().enable_all().build()` returns Err — driver setup failure such as running under a sandbox without eventfd/epoll support, or resource exhaustion creating the driver.

Common situations: Running inside restrictive sandboxes (some seccomp profiles, WebAssembly-ish or受限 environments) where epoll_create/eventfd are blocked; heavily restricted CI runners; exotic platforms unsupported by mio.

Related errors


AI-assisted analysis of Kuberwastaken/claurst@b0637c97ec (2026-09-10). Data as JSON: /api/errors/aa96ea788a209644. Report an issue: GitHub.

Appendix: source

Thrown at src-rust/crates/core/src/voice.rs:251

                .error_message()
                .unwrap_or_else(|| "Voice unavailable".to_string());
            let _ = event_tx.send(VoiceEvent::Error(msg.clone())).await;
            return Err(anyhow::anyhow!(msg));
        }

        self.is_recording.store(true, Ordering::SeqCst);

        let is_recording = self.is_recording.clone();
        let config = self.config.clone();

        // cpal::Stream is !Send, so we can't use tokio::spawn (which requires Send).
        // Instead, spin up a dedicated OS thread with its own single-threaded tokio
        // runtime so the stream stays local to that thread throughout its lifetime.
        std::thread::spawn(move || {
            let rt = tokio::runtime::Builder::new_current_thread()
                .enable_all()
                .build()
                .expect("voice thread runtime");
            rt.block_on(async move {
                match record_and_transcribe(is_recording, event_tx.clone(), config).await {
                    Ok(()) => {}
                    Err(e) => {
                        let _ = event_tx.send(VoiceEvent::Error(e.to_string())).await;
                    }
                }
            });
        });

        Ok(())
    }

    /// Stop recording.  The transcription request is sent immediately after
    /// the audio capture loop exits.
    pub async fn stop_recording(&mut self) -> anyhow::Result<()> {
        self.is_recording.store(false, Ordering::SeqCst);
        Ok(())

View on GitHub (pinned to b0637c97ec)