The mic wasn't silent. The health check was impatient.
The decision. Warm-up and stall are decided in one pure seam with a 3-second grace, and the redundant self-heal timer is gone.
Some recordings were cancelling themselves about five seconds after the start beep. The obvious read was a silent microphone. The app has dead-mic detection, and the symptom matched, so I chased that for longer than I’d like to admit.
It wasn’t the mic. The half-second pre-capture health check ran before the audio unit had delivered a single frame. On a cold AUHAL start, zero frames at the first tick is normal warm-up. The check called it a stall, restarted the engine, got another zero-frame tick, and kept looping until the 5-second watchdog cancelled the whole session. The impatience was the bug.
So warm-up versus stall is now one question answered in one place: otoCore/Audio/CaptureLiveness, a pure function with a 3-second grace window before zero frames count as a stall. The 2-second self-heal timer in RecordingSessionController is deleted, because two watchers with different opinions was the actual problem. The watchdog re-arms on AUHAL start, and dead-mic detection still works: a mic that never delivers a buffer still trips it.
// otoCore/Audio/CaptureLiveness. The whole decision, testable without hardware.
enum CaptureLiveness {
static let grace: TimeInterval = 3
static func verdict(framesSeen: Int, sinceStart: TimeInterval) -> Verdict {
if framesSeen > 0 { return .live }
return sinceStart < grace ? .warmingUp : .stalled
}
}
The fix went through the factory line as run 65a9cc70: 36 checks, 58 of 58 tests, plus a new capture-liveness suite that pins the grace window. One thing the line couldn’t do on its own: integrate tripped over the gitignored oto.xcodeproj in the builder’s claims, so I merged by hand. That’s now a known edge of the pipeline rather than of the app.
Since 2026-08-19, zero self-cancelled recordings in daily use on two Macs.