{"record":{"id":"2c9ff09cee67a584","repo":"zeroclaw-labs/zeroclaw","slug":"voicewake-audio-stream-ended-unexpectedly","errorCode":null,"errorMessage":"VoiceWake: audio stream ended unexpectedly","messagePattern":"VoiceWake: audio stream ended unexpectedly","errorType":"exception","errorClass":"anyhow::Error","httpStatus":null,"severity":"error","filePath":"crates/zeroclaw-channels/src/voice_wake.rs","lineNumber":477,"sourceCode":"                                    .with_outcome(::zeroclaw_log::EventOutcome::Unknown)\n                                    .with_attrs(::serde_json::json!({\"error\": format!(\"{}\", e)})),\n                                    \"VoiceWake: transcription error for utterance\"\n                                );\n                            }\n                        }\n\n                        state = WakeState::Listening;\n                        capture_buf.clear();\n                    }\n                }\n                WakeState::Processing => {\n                    // Should not receive chunks while processing, but just buffer them.\n                    // State transitions happen above synchronously after transcription.\n                }\n            }\n        }\n\n        bail!(\"VoiceWake: audio stream ended unexpectedly\");\n    }\n}\n\n// ── Audio utilities ────────────────────────────────────────────\n\n/// Compute RMS (root-mean-square) energy of an audio chunk.\npub fn compute_rms_energy(samples: &[f32]) -> f32 {\n    if samples.is_empty() {\n        return 0.0;\n    }\n    let sum_sq: f32 = samples.iter().map(|s| s * s).sum();\n    (sum_sq / samples.len() as f32).sqrt()\n}\n\n/// Encode raw f32 PCM samples as a WAV byte buffer (16-bit PCM).\n/// This produces a minimal valid WAV file that Whisper-compatible APIs accept.\npub fn encode_wav_from_f32(samples: &[f32], sample_rate: u32, channels: u16) -> Vec<u8> {\n    let bits_per_sample: u16 = 16;","sourceCodeStart":459,"sourceCodeEnd":495,"githubUrl":"https://github.com/zeroclaw-labs/zeroclaw/blob/88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc/crates/zeroclaw-channels/src/voice_wake.rs#L459-L495","documentation":"VoiceWakeChannel::listen runs a state machine over chunks received from an mpsc channel fed by the cpal input-stream callback; the loop only exits when audio_rx.recv() yields None, i.e. every sender was dropped. Since the stream is intentionally leaked, this bail means the capture callback stopped sending — the microphone stream terminated underneath the channel (device removed, host error, or the stream was dropped by the OS/driver). It is a liveness error: the wake-word listener is dead until listen() is restarted.","triggerScenarios":"listen() is running and (1) a USB microphone or Bluetooth headset is unplugged/disconnected mid-session; (2) the OS suspends or reclaims the audio device (system sleep/resume, another app takes exclusive control); (3) an ALSA/JACK error kills the stream and the callback's sender is dropped; (4) the daemon's audio thread panics. The error appears asynchronously, long after startup.","commonSituations":"Long-running bot on a laptop: closing the lid or unplugging the USB mic kills the listener; Bluetooth headset battery dying; PulseAudio/PipeWire restarting; headless box losing the default input after an OS update.","solutions":["Check physical/default device state: is the mic still connected and still the system default input?","Restart the daemon (or re-invoke listen) — startup re-acquires default_input_device and rebuilds the stream.","Use a stable, always-present input device (built-in mic or virtual device) instead of hot-pluggable hardware for an always-on bot.","Inspect preceding WARN 'VoiceWake: audio stream error' log entries — they carry the cpal error that preceded the stream death."],"exampleFix":"// before — one-shot listen dies with the device\nvoice_wake.listen(tx).await?; // someday: \"audio stream ended unexpectedly\"\n\n// after — supervise and rebuild the stream on failure\nloop {\n    if let Err(e) = voice_wake.listen(tx.clone()).await {\n        ::zeroclaw_log::record!(WARN, ::zeroclaw_log::Event::new(module_path!(), ::zeroclaw_log::Action::Note)\n            .with_attrs(::serde_json::json!({\"error\": format!(\"{e}\")})),\n            \"voice_wake listener stopped; restarting in 5s\");\n    }\n    tokio::time::sleep(std::time::Duration::from_secs(5)).await;\n}","handlingStrategy":"retry","validationCode":"use cpal::traits::{DeviceTrait, HostTrait};\n// Verify an input device still exists before (re)starting the listener.\nlet device_available = cpal::default_host().default_input_device()\n    .and_then(|d| d.default_input_config().ok().is_some());\nanyhow::ensure!(device_available, \"no usable default input device; skipping voice_wake restart\");","typeGuard":null,"tryCatchPattern":"match voice_wake.listen(tx.clone()).await {\n    Ok(()) => {}\n    Err(e) if e.to_string().contains(\"audio stream ended unexpectedly\") => {\n        // Device-level failure: wait, re-probe the default input device, then\n        // call listen() again. Back off between restarts to avoid hot-looping\n        // on a permanently missing device.\n    }\n    Err(e) => return Err(e),\n}","preventionTips":["Supervise listen() in a restart loop with backoff instead of treating it as run-forever.","Run always-on bots with a non-hot-pluggable default input (built-in mic or persistent virtual device).","Alert on the preceding 'VoiceWake: audio stream error' WARN logs — they usually precede stream death.","Disable the voice_wake channel on hosts without a microphone so startup fails predictably."],"tags":["voice-wake","audio","device-disconnect","liveness"],"backgroundTag":"audio-device-disconnected","analyzedSha":"88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc","analyzedAt":"2026-08-23T01:07:41.857Z","schemaVersion":2},"datasetVersion":"2026-08-23T08:06:27.607Z"}