Skip to main content
The macOS push-to-talk path has one semantic lifecycle owner: VoiceTurnCoordinator and its pure VoiceTurnReducer. Capture, provider, tool, and playback components remain I/O adapters. They report typed events and execute reducer effects; they do not independently decide whether a turn is listening, waiting, playing, or finished.

Turn lifecycle

Every turn has a stable VoiceTurnID. Async boundaries add the narrowest identity needed to reject late work: VoiceCaptureID, VoiceSessionID, VoiceResponseID, VoiceToolCallID, and VoiceLeaseID. Realtime provider callbacks are delivered in transport order inside a RealtimeHubEventIdentity envelope. The controller rejects the envelope before mutating transcript, playback, tools, persistence, or terminal state unless both its turn and response identities still own the active reducer turn.
Hub warm wait, transcription, provider response, pending tools, deferred commit, barge-in replacement, playback drain, capture start, lock decision, and transient hints all have reducer-owned deadlines. Timer callbacks carry both the turn ID and deadline kind, so a timer from an older turn cannot mutate the current one. Terminal transitions are idempotent and use a typed reason. The coordinator emits cleanup effects once, cancels all turn deadlines, records low-cardinality diagnostics, and retains a bounded event timeline for reconstructing failures without logging user text.

Ownership boundaries

The hot CoreAudio path does not send every PCM chunk through the reducer. Capture callbacks cross a serialized, turn-tokened ingress boundary before touching turn state or provider input; finalization closes that token before commit, so an admitted tail chunk cannot leak into the next turn. Audio remains in bounded sidecar storage and transport adapters; only lifecycle milestones cross the reducer boundary.

Audible output invariant

Native realtime audio, selected-voice fallback, deterministic agent acknowledgement, filler, and system-voice fallback use one VoiceOutputLease. A stale turn cannot acquire a lease, a competing real lane is denied, and only the exact lease identity can release playback. Filler is provisional: the playback engine stops it before releasing its lease, then the real answer acquires the lane. Provider completion and local playback drain are separate events: a turn remains active until both the provider is done and queued audio has drained. Gemini audio is also gated by its manual activity window. Audio and commit requests that arrive before activityStart remain buffered inside RealtimeHubSession; they flush in order only after the input window opens. Abandoning a turn clears that buffer so pre-window audio cannot leak into the next turn. Provider-specific barge-in is an atomic handoff. Reducer terminalization preserves an active hub runtime long enough for OpenAI to cancel its in-session response or Gemini to create a fresh replacement session. Preparation, seed refresh, permission, authentication, transcription, and persistence work capture immutable turn ownership and revalidate it after every suspension. Continuity persistence resolves the user transcript at the write boundary. A provider transcript is preferred when it is present and compatible with configured voice languages; otherwise the already-running local full-buffer decode gets a bounded wait. Writes are serialized, optimistically projected into chat, and retried with the same idempotency key. If bounded foreground delivery does not receive turn_recorded, a durable local outbox replays the turn in the background and contributes it to the next provider seed without blocking voice capture. Each entry carries the immutable owner and surface captured when it was enqueued. Seed merging is scoped to that same surface and reconciles stable idempotency keys already present in the kernel, while distinct keys remain distinct even when two utterances have identical text. This makes the kernel-owned main-chat projection, provider seed, and visible transcript converge even when the agent runtime or app restarts during a rapid PTT burst. Barge-in replacement awaits that immutable resolution and acknowledgement before refreshing the kernel seed and opening the new provider session. OpenAI can keep its session because input-item and response IDs map callbacks to an Omi turn. Gemini input-transcription events have no equivalent item ID, so every completed Gemini turn creates a fresh-session boundary before the next PTT; an event that is ambiguous across that boundary is dropped. Audio captured while a replacement connects is capped at 120 seconds of 16 kHz PCM. Agent delegation follows the same ownership rule: resolution must still own the tool turn immediately before the canonical spawn side effect, and only a successful spawn may produce the success acknowledgement and durable handoff. Permission checks and requests are direct desktop actions on both main chat and PTT. check_permission_status and request_permission execute in the signed-in app process through the shared Swift handler; they must not create a child agent. The PTT adapter treats an accidental permission-focused spawn_agent call as a direct request_permission request so a malformed model call cannot create a non-functional worker.

UI and error signals

VoiceTurnUIProjection is the semantic source for listening, lock, transcript, thinking, response-waiting, response-active, and terminal hints. PTTBarPresenter is the only writer for those floating-bar fields. User-facing failures stay visible briefly as actionable hints; developer signals remain available through typed terminal diagnostics, stale-event and invalid-transition counts, and the bounded event timeline.

Testing contract

Reducer tests use deterministic events and fake-clock deadlines. Cover the happy path plus races that can change ownership: late capture completion, old session or response callbacks, warm-wait fallback, deferred and replacement commits, pending tools, provider-finish versus playback-drain ordering, competing leases, barge-in, cleanup from every phase, duplicate terminal events, and randomized stale-event sequences. Integration tests verify the coordinator’s real effects and presentation adapter. Provider and playback tests verify normalized callbacks and lease enforcement. Keep tests hermetic: no live provider, network, sleep, or ordering dependency in CI. The tier-2 ptt-lifecycle harness flow drives the real ptt_start and ptt_stop bridge actions, then checks the typed phase and anomaly counters. A file belongs in that flow’s covers list only when those steps exercise its behavior; typed chat-only flows do not claim PTT reducer coverage.