Repository navigation
Start meetings on AirPods without crashing or failing when they switch to call mode - #1767
Merged
Merged
Conversation
…rmat AirPods can drop from 48 kHz to their 24 kHz call profile between graph validation and installTap, because opening their mic triggers the switch. installTap then raises an Objective-C exception that Swift cannot catch, so the app crashes at recording start. Re-check the input format under the graph lock right before both tap installs (start and device recovery) and turn a mismatch into a normal failed attempt instead. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015njvTLqC9Fao3TMUNouKka
11 of 13 tasks
On hardware the tap-install guard turned the crash into a failed start on nearly every first AirPods start: the graph validates at 48 kHz, opening the AirPods mic flips them to their 24 kHz call profile, and the start then refused to install the tap. Before the mic file is created, re-read the input format and, if it moved, discard the unstarted graph and build a fresh one on the settled route (at most twice). The guard before installTap stays as the last safety net. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015njvTLqC9Fao3TMUNouKka
On Justin's Mac a meeting that survived two lid-closes cleanly still put up the "System audio interrupted" prompt after each wake, and saved as degraded for device switches. The planned wake reconnect announced itself exactly like a stall, and each wake counted twice (mic + system) toward the three-switch degraded rule. The tap's wake reconnect now publishes a new .systemWake recovery event (arms the write hold like .deviceSwitch, but is not counted) and no "reconnecting" message. The user still hears about it if the reconnect fails or the tap stalls afterwards. The mic restart after wake is also kept out of the device-switch count. Every sleep is still recorded as a gap, so the capture quality still notes the interruption. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015njvTLqC9Fao3TMUNouKka
An AirPods call-profile flip can also land during a mid-meeting mic recovery (after a wake or a reconnect). The tap-install guard caught it, but the recovery then stopped the meeting. Run the same settle-and-rebuild step on the recovery graph before the recovery segment is sized. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015njvTLqC9Fao3TMUNouKka
…ake' into claude/quiet-wake-reconnect # Conflicts: # Sources/TranscriptedCore/Audio/CoreAudioSystemAudioCapture.swift # Tests/TranscriptedCoreTests/AudioTests/CoreAudioSystemAudioCaptureTests.swift
Both add a small policy enum at the same spot in AudioDeviceRecovery.swift; keep both. Lets #1767 merge cleanly after #1771. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015njvTLqC9Fao3TMUNouKka
With AirPods as the output, a tap and aggregate kept attached across sleep came back delivering only digital zeros while a video played, and AirPods playback stayed garbled until they were reconnected. Buffers kept arriving, so the stall check never fired and nothing reconnected. - Sleep now releases the tap and aggregate (keeping the queued tail), and the wake reconnect builds a fresh tap on the output the Mac woke with. If no wake notice comes, it rebuilds after 30s of awake time. - For 5 minutes after a wake, a tap delivering only zeros for 3s while another process is running audio output is rebuilt, up to 3 times. Real signal ends the watch; zeros on a quiet Mac never trigger it. - Every reconnect, the sleep release and capture failures are now logged, so the next hardware run shows what happened. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015njvTLqC9Fao3TMUNouKka
9 of 11 tasks
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015njvTLqC9Fao3TMUNouKka
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015njvTLqC9Fao3TMUNouKka
- Wake: leave the meeting mic alone when it is still delivering. Every rebuild creates a fresh engine that first binds to the macOS default input; with AirPods as the default that flipped them into call mode and garbled playback after wake (confirmed on hardware). - A callback landing after the tap's format listener fired no longer latches an overflow, and drain checks format change first, so a rate change (e.g. a call app opening the AirPods mic) reconnects instead of ending system audio. An overflow while the tap already reports a new format also reconnects. - A failed tap rebuild after wake or a route change retries (1, 2, 4, 8 s) instead of ending system audio on the first error. - A lid closed again before the last wake's recovery ran: that stale recovery is skipped, the tap stays released, and the new sleep keeps its mic hold. The mic sleep hold now ends after 30 s of awake time. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015njvTLqC9Fao3TMUNouKka
claude Bot
pushed a commit
that referenced
this pull request
Sep 23, 2026
- Call-audio tap (#1762/#1771): per-recording reconnect counts by cause (wake, format change, silent after wake, stall), rebuild retries, sleeps, silent-after-wake give-ups and why system audio ended. Bucketed on meeting events and allowlisted as Sentry tags. - #1767: mic_format_rebuilds_bucket when AirPods flip format at start. - #1768: meeting_system_audio_prompt_answered (outcome, tcc_status), plus mic_only_by_choice and system_permission_check on meeting_recording_started. - #1773: launch_models_warmed once per launch; models_warm on meeting_recording_started. - #1774: headset_mic_switched on dictation events. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SRK9LR6bS7HMphU19DMDfy
claude Bot
pushed a commit
that referenced
this pull request
Sep 23, 2026
Justin chose to leave the AirPods dictation change out of 1.1.62 (decision card, 2026-09-23 19:45Z). Reverses #1774's diff (main..d469c0e) on this branch only and drops its headset_mic_switched tracking from the dictation events, allowlists, test, and docs. #1767/#1771, #1768 and #1773 stay. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SRK9LR6bS7HMphU19DMDfy
claude Bot
pushed a commit
that referenced
this pull request
Sep 23, 2026
Keeps the in-place mic restart ahead of #1767's settled-format rebuild in device recovery, and gives the in-place graph the new voiceProcessingEnabled field (always false there). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NZkqPnF73GpVNv9N5LYX9b
This was referenced Sep 23, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Before: starting a meeting with AirPods as the mic could crash the app. Opening the AirPods mic flips them from 48 kHz into their 24 kHz call mode. If that flip lands after we check the mic format but before we attach to the mic, macOS throws an error Swift can't catch, and the app dies. The first version of this PR stopped the crash, but on Justin's Mac the first AirPods start then failed every time with "The microphone route did not become ready". A second start worked because the AirPods were already in call mode.
After: the start checks the mic format again just before creating the mic file. If the AirPods have switched, it throws away the unstarted mic setup and builds a fresh one on the new format, up to twice, then records normally. The check right before attaching to the mic stays as a last safety net and fails cleanly instead of crashing. The mid-meeting mic recovery path has the same safety net.
The crash is also in 1.1.61 (same attach call), so it isn't new in 1.1.62. It was found and retested during Justin's local test run on 2026-09-23 (logs: graph at 48000, guard saw 24000 about 6 ms later).
How:
settleMeetingInputGraphFormatre-reads the input node's format, using the same voice-processing state the graph was validated with (now carried onPreparedMeetingInputGraph). On a mismatch it discards the unstarted graph, waits 0.3 s, and callsmakeReadyMeetingInputGraphagain with the meeting's mic selection kept.startAudioCapturecalls it before the mic file is created and adopts the rebuilt graph.ensureMicTapFormatStillMatchesstill runs under the graph lock right before bothinstallTapcalls, so a flip that lands in the last few milliseconds becomes a normal failed attempt.Why
The app crashed at meeting start with AirPods, and after the first fix every first AirPods start failed. Both were seen on hardware.
Product Impact
meetingsmeeting reliabilityWhat changed
AudioDeviceRecovery.swift: addedMicTapFormatPolicy,ensureMicTapFormatStillMatches(called before the recovery tap install) andsettleMeetingInputGraphFormat.AudioFileManager.swift: the start now settles the route before creating the mic file, and checks again before the tap install.Audio.swift:PreparedMeetingInputGraphcarriesvoiceProcessingEnabled.AudioInitializationTests.testMicTapIsNotInstalledAfterAirPodsSwitchToTheirCallFormat, plustestMeetingStartSettlesTheMicRouteBeforeCreatingTheMicFile, a source-order check (settle, then mic file, then guard, then tap install).How I checked it
scripts/dev/agent-preflight.shpython3 scripts/dev/check-build-source-lists.pybash build.sh --no-open(no Swift toolchain in this session; CI builds it)bash run-tests.sh(CI)bash run-integration-smoke.sh(CI)swift test(CI)Risk Review
Notes
The rebuild runs after system audio capture has started. If the rebuild throws, the existing start-failure path stops everything, same as an
engine.start()failure today. It adds 0.3 s to a start only when the format actually moved.Agent handoff
COORD_DONE: BRIEF | this PR | rebuild the mic graph on the settled route at start + guard before both tap installs | none | none | preflight + source lists; CI pending | Justin retries a first AirPods start🤖 Generated with Claude Code
https://claude.ai/code/session_015njvTLqC9Fao3TMUNouKka