diff --git a/crates/headroom-core/src/pw/registry.rs b/crates/headroom-core/src/pw/registry.rs index ecc19e2..32631e8 100644 --- a/crates/headroom-core/src/pw/registry.rs +++ b/crates/headroom-core/src/pw/registry.rs @@ -496,6 +496,17 @@ impl RoutingState { /// `sinks_by_name`. If the captured name matches the active /// `preferred_real_sink`, populate `real_sink.node_id` so `status` /// reports it and downstream ops can route by id. + /// + /// **Cold-boot fallback (F4).** If we haven't captured a real-sink + /// name yet — either because the metadata listener's initial + /// replay hasn't fired, or because `default.audio.sink` was + /// already `headroom-processed` from a prior daemon run that + /// didn't clean up — adopt the first non-processed Audio/Sink we + /// see as the real sink. This avoids the failure mode Codex + /// flagged where `real_sink.name = None` indefinitely and bypass + /// routes log "no real sink known" forever. A subsequent + /// `default.audio.sink` event will refine the choice via + /// `adopt_new_real_sink` if the user/WP picks a different one. fn try_capture_real_sink(&mut self, global: &GlobalObject<&DictRef>) { let Some(props) = &global.props else { return }; let dict: &DictRef = props; @@ -510,7 +521,15 @@ impl RoutingState { } self.sinks_by_name.insert(name.to_owned(), global.id); let mut s = self.daemon.lock(); - if s.real_sink.name.as_deref() == Some(name) && s.real_sink.node_id != Some(global.id) { + if s.real_sink.name.is_none() { + tracing::info!( + node_id = global.id, + name, + "no preferred_real_sink yet; adopting first available Audio/Sink as fallback" + ); + s.real_sink.name = Some(name.to_owned()); + s.real_sink.node_id = Some(global.id); + } else if s.real_sink.name.as_deref() == Some(name) && s.real_sink.node_id != Some(global.id) { tracing::info!( node_id = global.id, name,