fix(audio): log dropped capture errors instead of silently discarding

The cpal stream-error closure used `let _ = err_tx.try_send(...)` against a
bounded sync_channel(16). If the live session's listener stalled or the
frontend disconnected, runtime stream errors were silently dropped — the
diagnostic bundle showed nothing for a session that mysteriously stopped
working.

- Bump the error channel capacity 16 → 32 (matches AUDIO_CHANNEL_CAPACITY).
- On try_send failure, log to stderr with the device name + a per-session
  drop counter so the symptom is visible in the diagnostic bundle even
  when the typed event never reached the frontend.
- Plumb a new `dropped_errors: Arc<AtomicU64>` through `build_input_stream`
  alongside the existing `dropped_chunks`, mirroring the same pattern.

(kon-audio doesn't build in the audit sandbox: it links against ALSA
which the sandbox lacks. CI cross-platform compiles it.)

https://claude.ai/code/session_0189xUb6ie6t9qHkzatGZ9Rb
This commit is contained in:
Claude
2026-04-25 09:37:51 +00:00
parent 581a098508
commit a7fdc96e7b

View File

@@ -370,11 +370,15 @@ fn open_and_validate(
let (tx, rx) = mpsc::sync_channel::<AudioChunk>(AUDIO_CHANNEL_CAPACITY);
let requeue_tx = tx.clone();
let dropped_chunks = Arc::new(AtomicU64::new(0));
// Bounded channel for runtime stream errors. Capacity 16 = plenty for
// the rare error case; if it ever fills, we drop newer errors silently
// because they would be redundant noise in a stream that is already
// failing. (Codex review 2026/04/17 M2)
let (err_tx, err_rx) = mpsc::sync_channel::<CaptureRuntimeError>(16);
// Bounded channel for runtime stream errors. Capacity 32 = plenty for
// the rare error case; if it ever fills, drops are reported via stderr
// and counted in `dropped_errors` so the symptom is visible in the
// diagnostic bundle even when the listener has gone away. Errors
// beyond the cap are by definition redundant noise in a stream that
// is already failing. (Codex review 2026/04/17 M2; capacity bump and
// drop logging added 2026/04/25 audit pass.)
let (err_tx, err_rx) = mpsc::sync_channel::<CaptureRuntimeError>(32);
let dropped_errors = Arc::new(AtomicU64::new(0));
let stream = match format {
SampleFormat::F32 => build_input_stream::<f32>(
@@ -385,6 +389,7 @@ fn open_and_validate(
tx,
dropped_chunks.clone(),
err_tx.clone(),
dropped_errors.clone(),
name.to_string(),
),
SampleFormat::I16 => build_input_stream::<i16>(
@@ -395,6 +400,7 @@ fn open_and_validate(
tx,
dropped_chunks.clone(),
err_tx.clone(),
dropped_errors.clone(),
name.to_string(),
),
SampleFormat::U16 => build_input_stream::<u16>(
@@ -405,6 +411,7 @@ fn open_and_validate(
tx,
dropped_chunks.clone(),
err_tx.clone(),
dropped_errors.clone(),
name.to_string(),
),
other => {
@@ -503,6 +510,7 @@ fn build_input_stream<T>(
tx: mpsc::SyncSender<AudioChunk>,
dropped_chunks: Arc<AtomicU64>,
err_tx: mpsc::SyncSender<CaptureRuntimeError>,
dropped_errors: Arc<AtomicU64>,
device_name: String,
) -> std::result::Result<cpal::Stream, cpal::BuildStreamError>
where
@@ -532,10 +540,24 @@ where
// frontend can show a toast. Also keep the eprintln for ops
// logs. (Codex review 2026/04/17 M2)
eprintln!("[kon-audio] capture error: {err}");
let _ = err_tx.try_send(CaptureRuntimeError {
if err_tx
.try_send(CaptureRuntimeError {
device_name: err_device_name.clone(),
message: err.to_string(),
});
})
.is_err()
{
// Channel full — listener has stalled or detached. Note
// it in stderr and the dropped-errors counter so the
// diagnostic bundle still shows the symptom even if the
// frontend never received the typed event.
let prior = dropped_errors.fetch_add(1, Ordering::Relaxed);
eprintln!(
"[kon-audio] capture error channel full; dropped error #{} for device '{}'",
prior + 1,
err_device_name,
);
}
},
None,
)