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.
recording / locked recording
  -> finalizing
  -> awaiting response / awaiting tools
  -> playing
  -> terminal(reason)
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

ComponentOwnsMust not own
VoiceTurnReducerLegal transitions, route, deadlines, terminal reason, semantic UI projectionI/O, singletons, network, audio buffers
VoiceTurnCoordinatorReducer execution, timers, effects, diagnostics, bounded timelineProvider protocol parsing or audio processing
PushToTalkManagerShortcut and capture adapters, bounded audio sidecar, existing STT dispatchA second route or phase state machine
RealtimeHubControllerWarm session transport, normalized provider callbacks, tool adapterFloating-bar presentation or terminal policy
VoiceOutputCoordinatorOne turn-scoped audible lease across every PTT laneAudio synthesis or playback
FloatingBarVoicePlaybackServiceSynthesis and playbackPTT ownership or direct bar mutation
PTTBarPresenterApplying the reducer projection to floating-bar stateTurn policy
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. Barge-in replacement awaits that immutable resolution before recording the interrupted exchange, refreshing the kernel seed, and opening the new provider session. Agent delegation follows the same 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.

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.