Skip to content

Telemetry for 1.1.62: call-audio failures and reconnects, permission prompt, warmup, crash builds - #1781

Merged
claude[bot] merged 27 commits into
mainfrom
claude/telemetry-1162-bofke5
Sep 23, 2026
Merged

claude[bot] merged 27 commits into
mainfrom
claude/telemetry-1162-bofke5

Conversation

@claude

@claude claude Bot commented Sep 23, 2026 •

Copy link
Copy Markdown
Contributor

Requested by Justin · project thread

Before: when the new Core Audio tap couldn't start or reconnect, PostHog and Sentry only said "system_stream_unavailable". Nothing counted sleep/wake reconnects or AirPods format rebuilds. Nothing recorded how people answer the "can't hear the other side of the call" prompt, or whether models were ready at launch. Crash reports could only be matched by version number.

After: every meeting event says which tap step failed and with what code, how many times call audio reconnected and why, and why it ended if it did. Meeting starts say whether the models were already warm and whether mic-only was the user's choice. The permission prompt reports what people picked and what macOS says afterwards. Dictation reports how fast it started and whether it's the first dictation since launch. Launch reports once when the models are warm. Crashes carry build_revision and build_channel.

Why

So we can tell after 1.1.62 ships whether the new capture, sleep, prompt and warmup pieces work for real users.

Product Impact

What changed

This branch is stacked. It merges the in-scope 1.1.62 heads (#1767 2529424 including #1771 86fa3df, #1768 1aec18d, #1773 b51ff88) so the tracking can hook into their code. Merge it last. #1774 was merged earlier and then backed out in 311a96e after Justin left it out of 1.1.62.

Left out on purpose:

How

  • All tap counters live on the tap's own queue and are read through serialized {}. Nothing touches the IOProc.
  • A fresh capture object backs each recording, and start() resets the counts.
  • The mic rebuild count uses the existing device-switch lock and resets with it.
  • Status codes are written neg10877 because the shared category check rejects a leading dash.

How I checked it

Risk Review

  • Privacy: fixed step names, numeric OSStatus codes, fixed end reasons, permission states, count buckets and booleans. No device names, paths or error text. Guarded by SentryEventPolicyTests and AnalyticsEventPolicyTests.
  • No storage or release-flow change

🤖 Generated with Claude Code

https://claude.ai/code/session_01SRK9LR6bS7HMphU19DMDfy


Generated by Claude Code

On 2026-09-23 a dictation started while AirPods were becoming the default
input. The AirPods mic never came up inside the 6s start budget (format
read timeout, bind failure, prewarm timeout) and dictation failed with
"Selected mic unavailable". Nothing ever tried the Mac's own mic.

- On a Bluetooth headset route (AirPods as mic and playback), dictation now
  binds the Mac's built-in mic first, so playback stays out of call mode and
  there's no slow HFP switch. It follows the headset instead when "Use
  Mac-selected microphone" is on (the same setting meetings read) or the
  MacBook lid is closed.
- If the first mic hasn't started 2.5s into the wait loop, dictation
  switches to the other mic once and gives it at least 4s, instead of
  timing out on the stuck mic. The overlay says "Using your Mac's mic" /
  "Using your Bluetooth mic". The switch only lasts for that dictation.
- The mic choice is an AUHAL bind only; dictation still never writes the
  Mac-wide default input.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018WR8eSZBbxLhZFaNENM5yU
Hardware log (AirPods Pro, music playing): every start picked the Mac mic,
then the readiness check saw the input node's output bus stuck at 24k and
reported routeNotSettled four times. After ~1.5s the recovery start
suppressed the built-in fallback and recorded on AirPods, whose mic
release later paused music on stop.

- A local mic with Bluetooth playback no longer waits on a low-rate output
  bus. The raw tap uses the hardware input format and VPIO is deferred on
  that split route, so the bus rate doesn't matter. This is also the
  likely cause of the 2026-09-09 "built-in never became ready" report.
- Remember the mic that last started on the same headset, so a mic the
  wait loop switched to starts directly next time. Recovery landings
  that followed the headset only because the fallback was suppressed
  are not remembered.
- Route analytics now pass the live recording formats, so hfp_suspected
  reflects the running recording instead of always reading false.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018WR8eSZBbxLhZFaNENM5yU
Hardware log after relaunch (AirPods Pro, music playing): the first press
landed 1s after launch. The cold built-in bind missed its 1.2s settle
window. At 2.5s the wait loop switched to the AirPods mic, which put
playback into call mode for the ~10s the dictation ran. The start took
5.4s and garbled the music.

A Mac-mic first choice is now pinned, for recovery starts too. The
wait loop switches only headset -> Mac mic. A slow Mac mic keeps
getting readiness retries on the built-in, and if it never comes up,
the user sees "Built-in mic unavailable" with Try Again rather than
music in call mode. This also closes the known gap where a resume
after borrowing the meeting mic could open the AirPods mic.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018WR8eSZBbxLhZFaNENM5yU
After a relaunch with AirPods as the Mac's input, the first dictation's
cold AUHAL rebind to the built-in mic took 1.4s once and ran out the
7.7s start budget once (press 8.6s after launch, so this wasn't launch
load). Warm starts took 81-84ms.

- TranscriptedAppState runs the permission-gated input prewarm once at
  launch. It only binds the mic and reads formats; nothing records. This
  arms the engine's wake/route observers at launch, the same state the
  app is in after its first dictation today.
- dictation_input_device_selection_failed now logs failure_kind and
  status_code. The error text is redacted from local logs, which hid
  why the cold bind failed.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018WR8eSZBbxLhZFaNENM5yU
- Core Audio tap: remember the last HAL step that refused and its
  OSStatus; report them as system_tap_step / system_tap_status on the
  meeting PostHog events and as Sentry tags on meeting_start_failed.
  The tap has no ScreenCaptureKit fallback, so this is the only
  off-device signal for why call audio did not start or come back.
- dictation_started: add start_latency_bucket (request to recording).
  Both dictation start events: add first_since_launch, so cold starts
  after launch are visible.
- Sentry crashes: tag build_revision and build_channel so a crash can
  be matched to the exact build.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SRK9LR6bS7HMphU19DMDfy
@claude claude Bot assigned r3dbars Sep 23, 2026
@claude
claude Bot requested a review from r3dbars September 23, 2026 18:41
…memory

Fixes three 1.1.62 deep-review findings on this PR:

- S2 (lid closed): a slow AirPods start could switch to the MacBook mic,
  which is cut off in hardware while the lid is shut.
- M12 (no built-in mic): on a Mac with no built-in mic, the overlay said
  "Using your Mac's mic" when it wasn't.
- S3: remembering the switched-to mic pinned every later dictation to
  it, overriding "Use Mac-selected microphone" until the app quit.

The wait loop's switch now asks the engine, which declines (returns
nil) when the lid is closed or a pinned Mac-mic lookup finds no built-in
mic. A declined switch doesn't extend the wait or change the overlay.
The remembered-mic feature is removed. It existed to skip a failing
first choice, and that failure was the stale-bus readiness gate fixed in
613a0df.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018WR8eSZBbxLhZFaNENM5yU
…ng-xpyplf' into claude/telemetry-1162-bofke5
… into claude/telemetry-1162-bofke5

# Conflicts:
#	Sources/Meeting/MeetingSessionController.swift
- 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 claude Bot changed the title Telemetry for 1.1.62: why call audio fails, dictation start speed, build on crashes Telemetry for 1.1.62: call-audio failures and reconnects, AirPods, permission prompt, warmup, crash builds Sep 23, 2026
Hardware (combined build a8036363, AirPods as default input and output,
music playing): every app launch made a short audible blip in the
AirPods. A fresh AVAudioEngine input node binds the macOS default input
before any device can be pinned, and on AirPods that briefly flips
playback toward call mode. This is the same mechanism as the meeting-mic
sleep garble.

The launch prebind now checks the default input off the main actor.
When it's Bluetooth, the prebind is skipped and
dictation_launch_prebind_skipped is logged. Those users pay the cold
bind on their first dictation, which is where the touch happened before
the prebind existed. Built-in and USB defaults keep the warm first
press.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018WR8eSZBbxLhZFaNENM5yU
A press 1.96s after launch raced the launch prebind for the audio engine
queue and fell into the slow start path (4.9s, ending on a stale 24k bus).
The launch bind is bounded, so a press that lands while it runs now waits
for it (up to 3s) and then starts on the warm engine instead of starting a
second cold bind. Recovery attempts skip the join.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018WR8eSZBbxLhZFaNENM5yU
Hardware (build 94cc4fa1, AirPods as default input and output, music
playing): with the launch bind skipped, the first dictation after launch
took the cold path. The music garbled, the overlay showed a mic switch
after 1-2s, and AirPods playback then cut out. That is much worse than
the short launch bump, so the launch bind runs on Bluetooth defaults
again. A press that lands during it still joins it (ac4d96a).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018WR8eSZBbxLhZFaNENM5yU
Hardware (build 94cc4fa1, AirPods as default input and output): both cold
presses failed with binding_not_settled about 2.5s in. The AUHAL rebind
from AirPods to the Mac mic outran the 1.2s settle window. The start then
spent ~5s in retries while the AirPods garbled and cut out. The one
successful cold bind took ~2s.

The launch prebind now gets a 3s settle window when it pins the Mac mic
away from a Bluetooth default input. A press keeps the 1.2s window its
readiness refresh is sized for, and a press that lands during the launch
bind waits up to 4.5s for it to finish.

An unsettled bind has no OS status code, so selection_failed now logs
settle_timeout_ms and settle_wait_ms instead.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018WR8eSZBbxLhZFaNENM5yU
When a cold bind pinning the Mac mic away from AirPods didn't settle in
time, the wait path kept disturbing it. Every readiness refresh and
prewarm retry reissued setDeviceID, which restarts a slow Bluetooth
transition. After five refreshes, forced recovery swapped in a fresh
AVAudioEngine, whose input node touched the AirPods mic again. On
2026-09-23 that is when the music garbled and then cut out.

- ParakeetAUHALBindingIntent.hasPendingSwitch: a confirmed switch to the
  same mic on the same engine, issued within 4s, is polled instead of
  reissued.
- forceInputReadinessRecovery keeps the engine while such a pin is
  unsettled (forced_recovery_skipped_bluetooth_default), for up to 4s
  after the first unsettled result. Blocked queues and timeouts still
  replace the engine.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018WR8eSZBbxLhZFaNENM5yU
Hardware (rev 995bbbc1, AirPods as default input): the launch prebind
failed with "prewarm_snapshot timed out after 1500ms". The first input
node read plus the Mac-mic pin on a pristine engine outran the ordinary
engine-work timeout, which fires before the settle window even starts.
A timeout leaves the engine queue busy and ends in retries or a fresh
engine, and each fresh engine touches the AirPods mic again.

The launch prebind now gets 3.5s for that first snapshot when it pins a
mic away from a Bluetooth default input. Presses keep 1.5s. A press
that lands during the launch bind waits up to 5s for it.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018WR8eSZBbxLhZFaNENM5yU
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SRK9LR6bS7HMphU19DMDfy
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 claude Bot changed the title Telemetry for 1.1.62: call-audio failures and reconnects, AirPods, permission prompt, warmup, crash builds Telemetry for 1.1.62: call-audio failures and reconnects, permission prompt, warmup, crash builds Sep 23, 2026
AnalyticsEventPolicyTests checks the doc against analytics-events.psv.

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
…c-only-meeting-xpyplf

Resolves the MeetingSessionController.swift conflict the way #1781's
merge 6cc4dfe did: keep resolveSystemAudioAccessFromSystem from this PR
and take catchUpModelsInBackgroundIfNeeded from #1773.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EuTuFYbp6CM2NGiHz58ZUG
@claude
claude Bot marked this pull request as ready for review September 23, 2026 20:32
@claude

claude Bot commented Sep 23, 2026

Copy link
Copy Markdown
Contributor Author

spm-tests on 2527acb is red, but the cause isn't a test failure. XCTest's stall detector aborted the run (signal 6) inside CoreAudioSystemAudioCaptureTests.testAttemptFinishSubscriberCanCancelAcrossBackendQueueWithoutDeadlock, about 20s into its 3s wait. Every suite that sorts after it was skipped. The build was clean, and all 221 tests that ran passed.

I don't think this PR causes it:

  • The same test stalled the same way at 18:16Z on Release Transcripted 1.1.62: version, appcast and cask #1756 (head d730471, which is main plus Info.plist), before any of this PR's code existed.
  • This PR's changes on that path only set plain vars that are already on the capture queue, such as tapDiagnostics.endReason in fail(). No lock or queue.sync was added. serialized {} is re-entrant, and the IOProc isn't touched.

No fix for this stall exists yet, and the root cause isn't established. This session can't re-run jobs (the API returns 403), so a maintainer re-run of spm-tests is the next step. I'll keep watching the PR.


Generated by Claude Code

@claude
claude Bot merged commit ab5ef38 into main Sep 23, 2026
11 of 13 checks passed
@claude
claude Bot deleted the claude/telemetry-1162-bofke5 branch September 23, 2026 21:31
claude Bot pushed a commit that referenced this pull request Sep 23, 2026
Keeps this branch's overflow reconnect (its last-resort fail() now
carries #1781's buffer_overflow reason) and teaches #1781's reconnect
counter about the overflow and noFirstBuffer triggers: overflow gets
its own on-device count, a reconnect with no first buffer counts as a
stall reconnect.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NZkqPnF73GpVNv9N5LYX9b
claude Bot pushed a commit that referenced this pull request Sep 23, 2026
Resolve the Core CLAUDE.md conflict (keep both notes) and point #1781's
tap-failure diagnostics at the recording's own tap, so a mic-only meeting
doesn't report the previous meeting's tap failure.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wd6LPpeoYvtp2aMj7wVQLU
claude Bot pushed a commit that referenced this pull request Sep 23, 2026
Resolved the diagnostics snapshot and its shape test by keeping main's new
fields and adding the pinned-mic backend fields beside them. The analytics
psv files are a real 3-way merge (union merge would duplicate event lines).
Pinned dictation interruptions other than wake now report at error level
with mic_backend.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VM1D8NinbTDe7T1oZ3i4ZU
claude Bot pushed a commit that referenced this pull request Sep 24, 2026
…p ci]

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013GAxhzGyuaTxNTv8tpj9j2
claude Bot pushed a commit that referenced this pull request Sep 24, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants