Recording showed as active for 9 minutes after capture silently stopped (wireless CarPlay) #43

Open
opened 2026-08-09 15:26:43 -04:00 by agent · 2 comments
Member

A recording continued to display as recording for nine minutes after it stopped capturing
anything
. Observed 2026-08-09 over wireless CarPlay.

This is M1-06's failure mode 2 — "the recording toast appeared, nothing was captured" — under
a route condition M1-06 never exercised.

The evidence

Capture 2026-08-09-143325-p6f6om, pulled off the device and measured:

value
Started 14:33:25
Stopped by hand ~14:51
Wall-clock length ~18 minutes
Audio duration 510.0 s — 8:30
Audio file last written 14:42:06
Transcript last cue ends 00:08:34.680
Transcript file written 14:51:17

14:33:25 + 8:30 = 14:41:55, which is when the audio file stopped growing. Audio and transcript
agree with each other to within four seconds and both stop dead at 8:30. The last nine minutes of
the session produced no audio and no cues
, while the UI continued to show a recording in
progress.

The transcript's own final words are Wes noticing the state: "Oh, I guess I'm still recording.
That's strange... I guess it was still connected to CarPlay."

What was happening

Wireless CarPlay in a Volvo EX30. Drove with the recording running, arrived, parked, and walked
away from the car — so the audio route changed repeatedly as the phone handed off from the car back
to the phone or to headphones. Wes reports the recording visibly pausing and resuming several
times
through this while still presenting as active.

He deliberately left it running after noticing, which is what produced the clean nine-minute window
of nothing.

Why this matters more than its priority suggests

The project already names this as its worst outcome, in #19:

A memo the user believes is being captured and is not is the worst outcome this project has.

By severity this is a p0: silent, total data loss with a UI that actively asserts the opposite.
It is filed p1 only because Wes explicitly deferred chasing it on 2026-08-09, not because the
severity is in doubt. Anyone picking it up should treat it as the former.

What is not yet known

Nothing here has been debugged — this is the observation and the measurement, no more.

  • Whether the audio engine stopped and the session state was never updated, or the engine kept
    running and the writer stopped accepting samples.
  • Whether the repeated pause/resume is the interruption handler cycling, the route-change handler
    re-attaching, or both fighting.
  • Whether transcription stopped because there was no audio, or stopped independently.
  • Whether a route change away from CarPlay is the trigger, or specifically the wireless CarPlay
    handoff, or walking out of Bluetooth range while parked.
  • Whether the same thing happens with wired CarPlay, or with plain Bluetooth to headphones.

The gap this exposes in the M1 test matrix

M1-06's four device tests covered lock-and-walk, a declined call, an AirPods-to-wired swap, and a
backgrounded reconnect. None of them involved CarPlay, and none involved a route disappearing
because the user physically walked away from the audio device. Wes: "I'm not surprised given the
odd ways wireless CarPlay hands off. It'll need to be tested further."

Whatever the fix turns out to be, the device tests for it should include: connect wireless CarPlay,
start recording, drive, park, walk away from the vehicle, and confirm capture either follows the
phone's microphone or stops honestly and says so.

Done when

  • A recording that loses its input either follows the current input or ends and says why.
  • The UI never shows an active recording that is not writing audio. If capture cannot continue,
    the recording bar, the Live Activity and the lock screen control all agree on that.
  • The CarPlay walk-away sequence above is a documented device test that passes.

Found while processing the 8/9 voice memos. Files retained in the vault at
Side Quests/Stash/_captured-audio/ — not attached here, because this repository is public and
they are personal audio.

A recording continued to display as recording for **nine minutes after it stopped capturing anything**. Observed 2026-08-09 over wireless CarPlay. This is M1-06's failure mode 2 — *"the recording toast appeared, **nothing was captured**"* — under a route condition M1-06 never exercised. ## The evidence Capture `2026-08-09-143325-p6f6om`, pulled off the device and measured: | | value | |---|---| | Started | 14:33:25 | | Stopped by hand | ~14:51 | | Wall-clock length | ~18 minutes | | **Audio duration** | **510.0 s — 8:30** | | Audio file last written | 14:42:06 | | Transcript last cue ends | 00:08:34.680 | | Transcript file written | 14:51:17 | 14:33:25 + 8:30 = 14:41:55, which is when the audio file stopped growing. Audio and transcript agree with each other to within four seconds and both stop dead at 8:30. **The last nine minutes of the session produced no audio and no cues**, while the UI continued to show a recording in progress. The transcript's own final words are Wes noticing the state: *"Oh, I guess I'm still recording. That's strange... I guess it was still connected to CarPlay."* ## What was happening Wireless CarPlay in a Volvo EX30. Drove with the recording running, arrived, parked, and walked away from the car — so the audio route changed repeatedly as the phone handed off from the car back to the phone or to headphones. Wes reports the recording **visibly pausing and resuming several times** through this while still presenting as active. He deliberately left it running after noticing, which is what produced the clean nine-minute window of nothing. ## Why this matters more than its priority suggests The project already names this as its worst outcome, in #19: > A memo the user believes is being captured and is not is the worst outcome this project has. By severity this is a `p0`: silent, total data loss with a UI that actively asserts the opposite. It is filed `p1` **only because Wes explicitly deferred chasing it on 2026-08-09**, not because the severity is in doubt. Anyone picking it up should treat it as the former. ## What is not yet known Nothing here has been debugged — this is the observation and the measurement, no more. - Whether the audio engine stopped and the session state was never updated, or the engine kept running and the writer stopped accepting samples. - Whether the repeated pause/resume is the interruption handler cycling, the route-change handler re-attaching, or both fighting. - Whether transcription stopped because there was no audio, or stopped independently. - Whether a route change **away** from CarPlay is the trigger, or specifically the wireless CarPlay handoff, or walking out of Bluetooth range while parked. - Whether the same thing happens with wired CarPlay, or with plain Bluetooth to headphones. ## The gap this exposes in the M1 test matrix M1-06's four device tests covered lock-and-walk, a declined call, an AirPods-to-wired swap, and a backgrounded reconnect. **None of them involved CarPlay**, and none involved a route disappearing because the user physically walked away from the audio device. Wes: *"I'm not surprised given the odd ways wireless CarPlay hands off. It'll need to be tested further."* Whatever the fix turns out to be, the device tests for it should include: connect wireless CarPlay, start recording, drive, park, walk away from the vehicle, and confirm capture either follows the phone's microphone or stops honestly and says so. ## Done when - [ ] A recording that loses its input either **follows the current input** or **ends and says why**. - [ ] The UI never shows an active recording that is not writing audio. If capture cannot continue, the recording bar, the Live Activity and the lock screen control all agree on that. - [ ] The CarPlay walk-away sequence above is a documented device test that passes. --- *Found while processing the 8/9 voice memos. Files retained in the vault at `Side Quests/Stash/_captured-audio/` — not attached here, because this repository is public and they are personal audio.*
Author
Member

Wes's recollection points at the mechanism, and it holds up in the code

2026-08-11, Wes: "I think what happened was related to whatever logic is running when it shows that the recording is paused waiting on a mic. That's the only time I've seen that and that was shown around the time when the memo died."

That state is AudioRecorder.attemptRestart(into:allowSessionRestart:), in the catch at AudioRecorder.swift:703-714:

isInterrupted = true
errorMessage = "Stash lost the microphone and is waiting for it to come back. "
    + "Everything recorded so far is saved. \(RecordingError.describe(error))"

Nothing is waiting. That is the defect.

Why nothing is waiting

scheduleRestart(into:) (AudioRecorder.swift:609) drives a fixed ladder:

private static let restartDelays: [Duration] = [
    .zero, .milliseconds(150), .milliseconds(400), .seconds(1), .seconds(2), .seconds(4)
]

Six attempts, 7.55 seconds end to end. When the last one fails the Task returns, defer ends the repair assertion, and the function is done. There is no Timer, no periodic re-attempt and no watchdog anywhere in AudioRecorder.swift — recovery past that point is purely event-driven, and the only events that can restart capture are:

  • .routeChanged / .engineConfigurationChanged / .interruptionEnded — another route event has to arrive
  • .becameActive — the phone has to be unlocked

Meanwhile isRecording stays true, the file stays open, and syncLiveActivity() has pushed isInterrupted: true to a Live Activity that is still presenting an active recording.

Which is exactly the observed sequence

Park the car, walk away with the phone in a pocket:

  1. CarPlay drops → one route change.
  2. Ladder runs. All six attempts fail inside 7.55 s. With the screen locked they were always going to: only the last attempt is allowed RecordingSession.activate(), and iOS refuses that from the background with .cannotStartRecording (561145187) — documented in the comment at attemptRestart lines 666-675.
  3. Task exits. The message says the app is waiting. It isn't.
  4. No further route change ever arrives. The phone is never unlocked, so .becameActive never fires.
  5. Capture is dead; every surface keeps asserting it is alive until Wes hits stop.

Audio stopping dead at 8:30 and never resuming, while the session ran to ~18 minutes wall clock, is precisely what that produces on disk. It also explains the "visibly pausing and resuming several times" earlier in the drive — those were ladders that succeeded, during handoffs where a usable route arrived within the 7.55 s window. The last one had no route to find.

This is not a CarPlay bug

Downgrading the hardware dependency. CarPlay is a convenient way to generate a route change you then physically walk away from — it is not the cause. Any input that disappears without a replacement arriving inside 7.55 s reaches the same state: Bluetooth headphones going out of range, a wired mic unplugged with the phone locked.

That means this reproduces at a desk without the Volvo:

  • Start a recording on Bluetooth headphones, lock the phone, walk out of range, wait ten minutes. Expect: audio stops, UI keeps claiming it is recording.
  • Or, in a unit test, force attemptRestart to fail on all six delays and assert on what the recorder reports afterwards.

Removing needs-hardware accordingly. The CarPlay walk-away test in the acceptance criteria stays, as confirmation rather than as the way in.

What the fix has to do

  1. Actually wait. After the ladder exhausts, keep re-attempting on a slow cadence rather than stopping. The current copy describes behaviour the code does not have; either the code learns to wait or the copy stops promising it.
  2. Bound the wait. Past a deadline, end the recording rather than holding an open file that will never receive another sample.
  3. End honestly, and persist why. Write the reason beside the memo so the detail screen can answer the question later. This is the same mechanism #56 needs — that issue is a transcription failure whose reason is computed into in-memory status and discarded, and this is a capture failure with the same disease. One persisted failure-reason sidecar serves both.
  4. One source of truth for capture state. The recording bar, the Live Activity and the lock screen control must not be able to report an active recording while no audio is reaching the file.

The Done when list in the description is unchanged; this comment is the mechanism behind it.

Still unknown

Whether the app surviving nine idle minutes with the screen locked is expected — the repair assertion is ended in the defer when the ladder exits, so the process should have been suspendable from that point, and it was still alive at 14:51 to write the transcript. Not load-bearing for the fix, but it is unexplained.

## Wes's recollection points at the mechanism, and it holds up in the code 2026-08-11, Wes: *"I think what happened was related to whatever logic is running when it shows that the recording is paused waiting on a mic. That's the only time I've seen that and that was shown around the time when the memo died."* That state is `AudioRecorder.attemptRestart(into:allowSessionRestart:)`, in the `catch` at `AudioRecorder.swift:703-714`: ```swift isInterrupted = true errorMessage = "Stash lost the microphone and is waiting for it to come back. " + "Everything recorded so far is saved. \(RecordingError.describe(error))" ``` **Nothing is waiting.** That is the defect. ## Why nothing is waiting `scheduleRestart(into:)` (`AudioRecorder.swift:609`) drives a fixed ladder: ```swift private static let restartDelays: [Duration] = [ .zero, .milliseconds(150), .milliseconds(400), .seconds(1), .seconds(2), .seconds(4) ] ``` Six attempts, **7.55 seconds end to end**. When the last one fails the `Task` returns, `defer` ends the repair assertion, and the function is done. There is no `Timer`, no periodic re-attempt and no watchdog anywhere in `AudioRecorder.swift` — recovery past that point is **purely event-driven**, and the only events that can restart capture are: - `.routeChanged` / `.engineConfigurationChanged` / `.interruptionEnded` — another route event has to arrive - `.becameActive` — the phone has to be unlocked Meanwhile `isRecording` stays `true`, the file stays open, and `syncLiveActivity()` has pushed `isInterrupted: true` to a Live Activity that is still presenting an active recording. ## Which is exactly the observed sequence Park the car, walk away with the phone in a pocket: 1. CarPlay drops → **one** route change. 2. Ladder runs. All six attempts fail inside 7.55 s. With the screen locked they were always going to: only the last attempt is allowed `RecordingSession.activate()`, and iOS refuses that from the background with `.cannotStartRecording` (561145187) — documented in the comment at `attemptRestart` lines 666-675. 3. Task exits. The message says the app is waiting. It isn't. 4. No further route change ever arrives. The phone is never unlocked, so `.becameActive` never fires. 5. Capture is dead; every surface keeps asserting it is alive until Wes hits stop. Audio stopping dead at **8:30** and never resuming, while the session ran to ~18 minutes wall clock, is precisely what that produces on disk. It also explains the *"visibly pausing and resuming several times"* earlier in the drive — those were ladders that **succeeded**, during handoffs where a usable route arrived within the 7.55 s window. The last one had no route to find. ## This is not a CarPlay bug Downgrading the hardware dependency. CarPlay is a convenient way to generate a route change you then physically walk away from — it is not the cause. **Any** input that disappears without a replacement arriving inside 7.55 s reaches the same state: Bluetooth headphones going out of range, a wired mic unplugged with the phone locked. That means this **reproduces at a desk without the Volvo**: - Start a recording on Bluetooth headphones, lock the phone, walk out of range, wait ten minutes. Expect: audio stops, UI keeps claiming it is recording. - Or, in a unit test, force `attemptRestart` to fail on all six delays and assert on what the recorder reports afterwards. Removing `needs-hardware` accordingly. The CarPlay walk-away test in the acceptance criteria stays, as confirmation rather than as the way in. ## What the fix has to do 1. **Actually wait.** After the ladder exhausts, keep re-attempting on a slow cadence rather than stopping. The current copy describes behaviour the code does not have; either the code learns to wait or the copy stops promising it. 2. **Bound the wait.** Past a deadline, end the recording rather than holding an open file that will never receive another sample. 3. **End honestly, and persist why.** Write the reason beside the memo so the detail screen can answer the question later. This is the same mechanism #56 needs — that issue is a transcription failure whose reason is computed into in-memory `status` and discarded, and this is a capture failure with the same disease. **One persisted failure-reason sidecar serves both.** 4. **One source of truth for capture state.** The recording bar, the Live Activity and the lock screen control must not be able to report an active recording while no audio is reaching the file. The `Done when` list in the description is unchanged; this comment is the mechanism behind it. ## Still unknown Whether the app surviving nine idle minutes with the screen locked is expected — the repair assertion is ended in the `defer` when the ladder exits, so the process should have been suspendable from that point, and it was still alive at 14:51 to write the transcript. Not load-bearing for the fix, but it is unexplained.
Author
Member

Raised to p0, 2026-08-11. The description already argued the severity — silent, total data loss with the UI asserting the opposite — and said it sat at p1 only because chasing it was deferred on 2026-08-09. That deferral is over.

Raised to `p0`, 2026-08-11. The description already argued the severity — silent, total data loss with the UI asserting the opposite — and said it sat at `p1` only because chasing it was deferred on 2026-08-09. That deferral is over.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
Stash/stash-ios#43
No description provided.