"'No recording was in progress' — said the app, while a recording was in progress."
The Feature and the Crash
The tap grammar is meant to be effortless: hold to push-to-talk; quick tap then a second tap to toggle hands-free. The first implementation was obvious and wrong: a quick tap cancelled the session, and a second tap started a new hands-free one. Sometimes it worked. Sometimes it died with "No recording was in progress" — an error message that was, at the moment it printed, factually false.
The Race
The first tap fires an async cancel. The second tap starts a fresh session. If the cancel is slow — and it sometimes is, because tearing down audio isn't instant — it arrives after the new session exists, doesn't know it's stale, and tears down the wrong recording. A ghost from a session that's already dead reaches out and kills the living one. Then the code that expected a session finds nothing, and reports the absurdity.
tap 1 -> cancel session A (async, slow...)
tap 2 -> start session B (fast)
... cancel-A finally lands ...
-> tears down B # the ghost killed the wrong session
-> "No recording was in progress"
Two Fixes, Both Necessary
The first fix removes the race entirely: never restart the session. A quick tap no longer cancels — it leaves the session recording through a short decision window (about 0.45s). If a second tap arrives, the same session converts to hands-free in place; if the window expires with no second tap, then it cancels. There is no teardown-then-startup gap for a ghost to land in. The second fix is defense in depth: stamp every audio session with a generation token, and make any cancel check its generation before acting. A cancel from generation 1 arriving at generation 2 is simply a no-op. Fix one removes the race; fix two guarantees that if a race ever reappears, it can't do damage.