agent: code-atomiser-fix — cancellable Whisper inference + bounded drain + lock-over-await

Three findings chain: thread::spawn discarded the JoinHandle and gave
no cancellation route into the whisper backend; drain_inference busy-
polled with no timeout; stop_live_transcription_session held the
lifecycle AsyncMutex across the await of a non-cancellable
spawn_blocking handle. Together: a single wedged inference (ggml
deadlock, GPU stall) bricked every future start/stop until app restart.

Fix: each inference task carries an Arc<AtomicBool> abort_flag; the
flag is wired into whisper-rs::FullParams::set_abort_callback_safe so
the spawned blocking thread checks it and exits cleanly. drain_inference
is bounded by a deadline (3 x chunk_duration, min 2s); on expiry the
flag is set, the receiver is dropped, and a typed Error::InferenceTimeout
status is surfaced. Drop for InferenceTask asserts the abort flag so
any '?'-propagation or panic unwind closes the cancellation route
without relying on the explicit drain path. stop_live_transcription_session
restructured so the lifecycle guard is dropped BEFORE the JoinHandle
is awaited; start_live_transcription_session releases the guard
explicitly after installing the RunningLiveSession.

Unit test added: dropping_inference_task_sets_abort_flag covers the
Race-A regression directly. A real Race-B drain-timeout test would
require a wedged whisper-rs, which is hard to fixture; that path is
covered by the SAFETY comment and cargo build + manual smoke.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-13 14:38:22 +01:00
parent 3f4e5cc9a4
commit 5725836f40
5 changed files with 245 additions and 17 deletions

View File

@@ -22,6 +22,14 @@ pub enum Error {
#[error("transcription failed: {0}")]
TranscriptionFailed(String),
/// Inference exceeded the bounded wait imposed by the caller (live
/// session `drain_inference`). The spawned worker has had its abort
/// flag set so whisper-rs will return early on its next
/// abort-callback poll; the lock-up itself is *not* recovered by
/// this error — but the live-session lifecycle can now progress.
#[error("inference timed out after {timeout_ms}ms")]
InferenceTimeout { timeout_ms: u64 },
#[error("audio decode failed: {0}")]
AudioDecodeFailed(String),