v1.2.0
2026-09⛔ The class vocabulary for "where is this caller’s audio coming from" briefly existed twice — once in this package and once in the backend. Two copies of a vocabulary in two languages cannot be kept in step by intention; they drift, and this drift would have been silent, because the only symptom was a flag on a telemetry field nobody reads until they are already debugging a call. So classification moved to the backend and this package deleted its copy. The client now sends what only a client can see — the device labels — and the server decides what they mean. Improving the classification is one edit, in one language, that applies to every surface at once, and it can be re-run over calls that are already recorded.
A native app is the exception, because it genuinely knows more than a label: iOS names its audio port and Android names its communication device. A client may therefore state a route explicitly, and the server prefers it when it recognises it — while keeping an unrecognised name verbatim, because a route we have never heard of is the only signal that our list is missing one.
The device-route state machine: permission, hot-swapping a microphone without dropping the call, and two rules that are easy to get wrong in opposite directions — keep the old microphone until the replacement is usable (stopping first turns every headset swap into a gap of silence, and a permanent one if the new device fails to open), and never publish two at once. Mute survives a route change, because re-acquiring a track is exactly the moment that state is silently lost.
Microphone selection never trusts a remembered device id indefinitely. A device chosen last week can be unplugged today, and asking for it exactly throws — which a caller experiences as "the microphone is broken". A remembered id is used only if it is still present, and the fallback says it was a fallback, because a silent substitution is how somebody ends up on their laptop speaker with no idea why.
⛔ Removes exports from the ./voice subpath published the same day. Strictly a breaking change, shipped as a minor deliberately: nothing depended on it yet, and the stable root export is untouched, so a major would have wrongly signalled an API-client break to the people who do use that. Once anything imports ./voice, a removal goes to a major.