host/observer: phase 3 I/O adapter (read-only registry observer)

Codex authored the adapter half of the phase-3 split; I reviewed it and
applied one robustness fix (below). Additive/read-only — does not yet replace
the existing audio.rs router (that migration is a later integration phase).

adapter.rs: a dedicated libpipewire main-loop thread translating registry
globals into RegEvents and publishing the latest Projection via
RegistryObserverHandle::latest(). core.sync(0)/done is matched one-shot →
ServerSynced; a 250 ms loop timer emits Tick for the fail-closed readiness
timeout; pulse-PID candidates are probed from /proc/<pid>/comm only when the
candidate changes; Links missing endpoint props are bound (LinkInfoRef, weak
back-ref to avoid the listener cycle) and resolved via LinkEndpointsResolved.
mod.rs: `pub mod adapter;`. audio.rs: parse_object_serial → pub(crate) so the
adapter reuses the strict 64-bit parser.

Review fix: record_global was called unconditionally per global (including
unknown object types and dropped globals), which could desync the bound-link
FIFO from the model's live_ids and leak a Link proxy on a recycled id. Now
folded into `add()` so a slot is recorded only when an Added event is applied
— the two id queues are provably lockstep.

Exit gate complete: 5 pure rows + the live topology-diff row (row 6) — the
#[ignore] adapter test PASSES on this host against the live daemon
(module-null-sink + module-loopback observed appearing and disappearing).
cargo test --bins 117 + 1 live green, clippy clean, no fmt sweep.

Co-Authored-By: Codex (gpt-5.6-sol) <codex@openai.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-24 13:06:36 -04:00
co-authored by Codex Claude Opus 4.8
parent 8206864a43
commit 66a0dd54df
3 changed files with 602 additions and 1 deletions
+1 -1
View File
@@ -615,7 +615,7 @@ fn run_router(
/// (empty, signed, whitespace-padded, non-numeric, overflowing) is a
/// property we do not understand and must not guess at. Leading zeroes
/// are accepted — they are unambiguous and parse to the same value.
fn parse_object_serial(raw: &str) -> Option<u64> {
pub(crate) fn parse_object_serial(raw: &str) -> Option<u64> {
if raw.is_empty() || !raw.bytes().all(|b| b.is_ascii_digit()) {
return None;
}