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
- 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`.
- Test voice startup early with a minimal current_thread runtime probe to surface driver issues at boot.
- Ensure tokio's `rt`, `time`, and `net` features are enabled consistently across the workspace (mixed tokio versions can cause driver conflicts).
- 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
- Check platform/seccomp support for epoll/eventfd before enabling voice
- Keep tokio versions unified across the workspace to avoid driver conflicts
- Send VoiceEvent::Error instead of panicking on the dedicated thread
- Probe runtime construction at startup when voice is configured on
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
- failed to build reqwest client
- generationConfig must be an object
- Failed to build reqwest client
- (?:https?|ftp)://\S+|www\.\S+
- valid OAuth authorize base URL
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)