diff --git a/docs/screenshare-audio-exclusion-impl-plan.md b/docs/screenshare-audio-exclusion-impl-plan.md index d0a48c8..784748e 100644 --- a/docs/screenshare-audio-exclusion-impl-plan.md +++ b/docs/screenshare-audio-exclusion-impl-plan.md @@ -1,14 +1,19 @@ # Implementation plan: whole-desktop screen-share audio without self-echo -**Status:** 🟢 **v4 — three review rounds applied.** *Progress as of 2026-08-15:* phases 0a, 0b, +**Status:** 🟢 **v4 — three review rounds applied.** *Progress as of 2026-08-21:* phases 0a, 0b, 0c step 1, 1, 2, 3, 3r, 4 and 5 are merged, and the **phase-5 major gate PASSED on 2026-07-26** -(§1). The current front is **0c step 2**, sliced S1–S5: S1 and S2 are merged; **S3a is merged -locally** in pixelpass (`15d1374`); and **S3b is built and validated in the pixelpass working -tree and committed locally** (`5d3da8b`). S4/S5 are not complete. **0d and phases 6–9 are not -built** — nothing merged to date mutates the graph, so the feature has no user-visible behaviour -yet. Phase 6 is the first mutation. -**Date:** 2026-07-21 (v4); status line refreshed 2026-08-15 -**Design of record:** [`screenshare-audio-exclusion-plan.md`](screenshare-audio-exclusion-plan.md) v3.4 (`8768cd2`), converged round 7. +(§1). The two pre-Phase-6 decisions are now **closed** in design v3.8 §6.9: the conservative +same-device hardware bridge is built and validated, and the 2 s readiness budget passed +baseline, inflated-graph and live-churn calibration. The completed **0c step 2** was +sliced S1–S5: S1 and S2 are +merged; **S3a is merged locally** in pixelpass (`15d1374`); and **S3b is built, validated, and +committed locally** (`5d3da8b`). **S4, S5, 0d, round 11's bridge changes, and Phase 6's first +pure channel-planning prerequisite are built, validated, and committed locally in pixelpass +(`781defc`).** The Phase 6 mutation and phases 7–9 are not built — nothing released to date +changes the feature's user-visible behaviour. **Phase 6 is now the current front**; its live +fan-out remains the first audio-exclusion graph mutation. +**Date:** 2026-07-21 (v4); status line refreshed 2026-08-21 +**Design of record:** [`screenshare-audio-exclusion-plan.md`](screenshare-audio-exclusion-plan.md) v3.8, round 11. **Scope:** *ordering, gates and acceptance criteria only.* **Reference convention.** `v3.4 §N` = the design doc. `plan §N` = this document. The two @@ -111,9 +116,10 @@ Record: [`screenshare-audio-exclusion-phase5-results.md`](screenshare-audio-excl (audit build pixelpass `main` @ `91c4ded`, release profile). Phase 1 was the hard prerequisite of the re-run for both carriers (plan §3) and was satisfied — rows 4 and 5 passed at the real tagging sites. **Phase 6 is no longer blocked by this gate.** What still blocks it is the rest -of the DAG: `0b → 6` is satisfied and merged, but `0c → 0d → 6` is not — **0c step 2 remains -open** (S1 and S2 merged; S3a merged locally in pixelpass; S3b built, validated and committed -locally at `5d3da8b`; S4/S5 incomplete), and **0d has not been built at all**. +of the DAG: `0b → 6` is satisfied and merged; **0c and 0d are built, validated, and committed +locally with S4/S5 and round 11 in pixelpass `781defc`**. **Round 11 now closes the two former +design §6.8 blockers** with the v3.8 §6.9 hardware bridge and readiness calibration; Phase 6 +is unblocked and its first pure planning prerequisite has landed. ⚠️ **Superseded, kept for the trail — status 2026-07-25 (evening): "3r is BUILT AND MERGED; the re-run has not happened yet."** The phase-5 gate failed on its first live run and put 3r into @@ -313,6 +319,18 @@ through four phases of active work around them. Guardrails go up before the scaf capture sink; - legacy behaviour byte-identical. +> ✅ **BUILT, VALIDATED, AND COMMITTED locally in pixelpass `781defc`, 2026-08-21.** The real hidden +> `--internal-desktop-excluding` host input resolves to a typed `CapturePlan`; only +> `LegacyDesktop` can construct `DefaultMonitor`, while `DesktopExcluding` owns a bare +> connection-owned sink whose type has no legacy loopback API. The complete 16-row mode matrix +> passes, both conflict inputs reject before graph mutation, and the exact legacy GStreamer +> audio tail is unchanged. The serialized live graph assertion passed with neither an incoming +> PipeWire link nor a Pulse module feeding the new sink; all four prior ownership/cleanup live +> regressions also passed and left no PixelPass audio residue. Broad result: pixelpass **310 +> passed, 9 ignored**, fresh `--doctor` all green; peerspeak screen-share units **41 passed, 1 +> ignored**, plus the real Stop Share/SIGINT compatibility gate passed. This remains an internal +> mode with no fan-out and no public selector; Phase 6 is still the first fan-out mutation. + A constructible-but-not-yet-public variant is acceptable for the interval between 0d and Phase 6 provided it is unit-tested and reachable by the hidden trigger. @@ -441,7 +459,8 @@ and 4 are unaffected, and the phase-5 audit machinery is already correct. > mutation-verified (a `device_props` ambiguity test that checked for one live *Device* > rather than one live *global*, and `device.api` corroborating by presence). Two findings > left open as design items, both pre-existing — hardware playback-to-capture paths and the -> readiness-budget calibration, both recorded in design §6.8. +> readiness-budget calibration. **Both are now closed by design v3.8 §6.9 / the pre-Phase-6 +> round-11 gate below.** > > **Added beyond the spec: a second live gate for the Device-side path.** Row 1's > `session_device` assertion is satisfied by a union, and WirePlumber 0.5.15 copies @@ -539,7 +558,9 @@ observable in Phase 5 before they gate anything real. > ⚠️ **Phase 6 is NOT unblocked by this file alone.** F11-1 was the other gate and is now > **closed** (2026-07-26, pixelpass `c78eb2d`: key 4 bounds an owner only when the node's > Client resolves; measured cost on the live graph, zero — see the results file). Phases -> 0b/0c/0d and the "Stereo Mix" design call still precede phase 6. +> 0b/0c/0d are satisfied and committed locally with S4/S5 in pixelpass `781defc`. +> **Round 11 (2026-08-21) closed the remaining hardware playback-to-capture and readiness +> gates; Phase 6 is now unblocked for development.** > > Two things to keep when re-running: **every partition row must run with `AEC=off`** (a > configured-but-unvalidated AEC shuts the fan-out gate and empties the eligible half of every @@ -580,7 +601,7 @@ exclude-everything implementation fails the eligible half of every row. | 6 | peerspeak **notification** sound | that node, reason = tag | — | | 7 | a **second** pixelpass host's capture sink, **plus a controlled forwarder reading that sink's monitor** | the forwarder's **named output serial** (cycle prevention, v3.4 §6.2) | — | | 8 | EasyEffects running | combined output leg | EasyEffects stopped ⇒ ordinary streams | -| 9 | Firefox: music only / mic on untainted source / capturing a tainted monitor | the third only (v3.4 §6.1.1) | the first two | +| 9 | Firefox: music only / mic on a **different Device** / mic on the **same Device receiving tainted playback** / capturing a tainted monitor | the third and fourth (`tainted-owner-bridge`) | the first two | | 10 | sticky taint: tainted input leg removed, output leg lives | still excluded | after full owner teardown + restart | | 11 | recycled serial/index/link-group after teardown | — | must **not** inherit taint | | 12 | AEC loaded, then unloaded | four nodes; then `Revoked` | — | @@ -610,8 +631,32 @@ assumptions, whereas Sunshine is an uncontrived third-party forwarder nobody des test. It stays as row 1b, **opportunistic and non-gating**, because it cannot be relied on to be present. -**Any surprise here goes back to the design doc as round 8. Phase 6 does not start until this -results file exists.** +**Any surprise here goes back to the design doc as a new measured round. Phase 6 did not start +until this results file existed; round 11's targeted addendum now pins the same-device rule.** + +### 5.4 Pre-Phase-6 round-11 closure — ✅ PASSED 2026-08-21 + +Design v3.8 §6.9 and the addendum in +[`screenshare-audio-exclusion-phase5-results.md`](screenshare-audio-exclusion-phase5-results.md) +are the durable evidence. PixelPass retains snapshot-local `device.id` on positively classified +session-device nodes and adds a conservative `Sink → Source` taint edge only within that Device. +No ALSA control-name guess is part of the runtime policy. + +Exit gates: + +- 83 focused taint tests pass, including same-device exclusion, different-device eligibility, + and the accepted same-device-microphone over-exclusion. +- 65 pure observer tests and all three serialized live PipeWire observer tests pass; a live + passive device retains the `device.id` consumed by the engine. +- Full non-GUI suite: 313 passed, 0 failed, 9 ignored. The three live observer tests were then + run explicitly and passed. +- Targeted live audit exact partition: tagged ALC897 playback excluded the same-ALC897 + capture/re-emitter as `tainted-owner-bridge`; the Arctis-source control remained eligible. +- Readiness: baseline 30 starts p95/max 10/11 ms; 48-module graph 30 starts 113/114 ms; + 20 starts during 250 create/remove cycles 6/8 ms, zero timeouts. Keep the 2 s budget. + +All temporary modules were unloaded by their exact module ids, configured audio defaults were +unchanged, and root filesystem free space was 17 GiB after tests and Clippy. --- @@ -624,6 +669,16 @@ retained for the life of the share, per-port link sets, "captured" only when **e link is `ACTIVE`, same-epoch revalidation immediately before each creation, proxy drop on ancestry becoming unsafe. +> **Status 2026-08-21 — Phase 6 started, mutation not yet enabled.** The first bounded slice +> found and closed a prerequisite the old model did not carry: Port snapshots now retain +> `audio.channel`, and a pure planner produces exact link specs by channel rather than global-id +> or enumeration order. Five matrix tests pin FL/FR mapping, MONO fan-out, unknown/surround +> refusal, excluded-candidate refusal, and capture-sink replacement. Full non-GUI PixelPass: +> 318 passed, 0 failed, 9 ignored; the serialized live observer boundary test and strict Clippy +> pass. The next slice is the same-observer-callback mutation edge that creates and retains the +> planned non-lingering proxies; no fan-out link is created by the current checkpoint, committed +> locally in pixelpass `781defc`. + Failure ⇒ report the stream unsupported. **Never** fall back to the default monitor — and after 0d that fallback is unconstructible in this mode, by either path. @@ -1040,8 +1095,23 @@ light (the `abort()` orphan race, the unbounded join, peerspeak advertising a de | S1 | repair planner (`919d5bd` + `9145b2a`) | done; awaiting re-review, then merge | | S2 | peerspeak host-fault path: always-on notice channel, EOF synthesis, session-scoped fault, clear `is_sharing` + presence ticket, `ScreenShareStopped` then error | fixes a defect **today** — a dead share stays advertised — and is independent of the actor | | S3 | pixelpass ledger transactions + ambiguous-load reconciliation + child containment + pre-armed watchdog + poison state machine + supervisor health arm | fixes the `abort()` orphan race **today**; no libpipewire work | -| S4 | the `AudioGraphOwner` actor itself, the readiness handshake, and both measured budgets | the only slice that needs new PipeWire mechanism | -| S5 | the two live exit gates: two-host ownership, and the never-yet-run Stop Share SIGINT gate | needs S4 on the graph | +| S4 | **built, validated, and committed locally in pixelpass `781defc` (2026-08-20):** the `AudioGraphOwner` actor itself, the readiness handshake, and both measured budgets | the only slice that needs new PipeWire mechanism | +| S5 | **built, validated, and committed locally in pixelpass `781defc` (2026-08-21):** the two live exit gates: two-host ownership/repair and Stop Share SIGINT | needs S4 on the graph | + +**S5 live evidence (2026-08-21).** Pixelpass's ignored +`live_two_host_sigkill_and_repair_preserve_the_survivor` gate starts two independent routing +owners, observes a distinct native sink and ownership-tagged loopback for each, SIGKILLs the +first, and proves only its sink disappears. `--repair` then removes exactly the dead host's +loopback while explicitly leaving the second live host alone. The survivor exits through a real +SIGINT with its active graph teardown measured at 40 ms, inside peerspeak's 2 s grace, and leaves +no sink or module residue. Peerspeak's separate +`stop_share_ends_the_real_host_via_sigint_within_the_grace` end-to-end gate also passed against +the freshly built pixelpass binary, proving Stop Share drives that signal path before fallback. +The pre-gate cold review also found and fixed an S4 unwind regression: the actor's emergency +`Stop` path had quit without restoring still-owned `target.object` values. `Stop` now performs +the same ownership-checked restoration and waits for a PipeWire core round-trip before closing +the connection, so constructor cancellation or unwind cannot knowingly strand an app on the +disappearing sink. **Round 18 (2026-07-26 night) — two more repair review rounds. `--repair` now reads and unloads through libpulse, and one of the review's own prescriptions had to be replaced after measuring.** diff --git a/docs/screenshare-audio-exclusion-phase5-results.md b/docs/screenshare-audio-exclusion-phase5-results.md index 149913d..b844e82 100644 --- a/docs/screenshare-audio-exclusion-phase5-results.md +++ b/docs/screenshare-audio-exclusion-phase5-results.md @@ -17,6 +17,62 @@ was closed with this data on 2026-07-26 (see "What still blocks phase 6"). --- +## Addendum — pre-Phase-6 hardware/readiness gate (2026-08-21) + +This addendum does not rewrite the historical 2026-07-26 matrix. Design v3.8 §6.9 adds one +conservative edge the old engine did not have: tainted playback into a positively classified +hardware sink taints passive capture nodes carrying the same snapshot-local `device.id`. It +also closes the readiness-budget calibration that round 9 left open. + +### Hardware path — targeted live exact partition + +The audit started first and reached readiness. Controlled modules then created three named +candidates: + +| candidate | expected | observed | +| --- | --- | --- | +| tagged playback into ALC897 | excluded root | `peerspeak-owned` | +| capture/re-emitter reading the ALC897 source | excluded through hidden same-device hop | `tainted-owner-bridge` | +| identical capture/re-emitter reading the Arctis source | eligible; different Device | eligible, no reason | + +The settled record was `graph_ready=true`, epoch `complete`. This revises row 9 for every +future full matrix: music-only and a microphone on a **different Device** remain eligible; +a microphone on the **same Device receiving tainted playback** and a tainted-monitor capture +are excluded. This is deliberate fail-closed over-exclusion because a private hardware or +firmware loopback is not observable as a PipeWire Link. + +The host's ALC897 had no `Stereo Mix` capture-source item: `Input Source` offered Rear Mic, +Front Mic and Line. Its separate `Loopback Mixing` control was disabled. Runtime safety does +not depend on either spelling; USB/vendor loopbacks need the same rule. + +### Readiness calibration — retain the 2 s sticky deadline + +Each measurement used a fresh observer process and its emitted monotonic `at_ms` readiness +timestamp: + +| arm | runs | min | p50 | p95 | max | timed out | +| --- | ---: | ---: | ---: | ---: | ---: | ---: | +| ambient live graph | 30 | 4 ms | 5 ms | 10 ms | 11 ms | 0 | +| 24 temporary null sinks + 24 loopbacks | 30 | 109 ms | 111 ms | 113 ms | 114 ms | 0 | +| 250 null-sink create/remove cycles concurrent with starts | 20 | 4 ms | 5 ms | 6 ms | 8 ms | 0 | + +The inflated maximum is 17.5x below the 2 s deadline. All temporary modules were unloaded by +the exact ids returned from `pactl`; no test modules remained, and the configured default +sink/source were unchanged. + +### Regression gates + +- focused taint engine: 83 passed; +- observer: 65 pure passed, then all 3 serialized live PipeWire tests passed; +- complete non-GUI PixelPass suite: 313 passed, 0 failed, 9 ignored; +- disk before the first build: 20 GiB free; after tests and Clippy: 17 GiB free. + +**Verdict:** both former pre-Phase-6 design blockers are closed. S4/S5/0d, round 11, and the +first pure Phase 6 channel-planning prerequisite are committed locally in PixelPass `781defc`; +the first fan-out mutation remains the current work. + +--- + ## What changed since run 1 Run 1 failed on two defects, both fixed before this run: @@ -276,10 +332,11 @@ orders of magnitude of remaining headroom, not one. teardown in bursts, and a 32 µs recompute drains a burst faster than it forms. `busy_fraction` is the number to trust. -⚠️ **The readiness budget still has no calibration argument.** 1–2 ms against -2000 ms is three orders of magnitude of slack on *this* host with 18 binds; it is -not an argument about a host with a large USB interface, many virtual devices, or -a cold cache. Carried forward as open, unchanged. +⚠️ **Historical run-2 finding, closed by the 2026-08-21 addendum above:** 1–2 ms +against 2000 ms was three orders of magnitude of slack on *this* host with 18 +binds, but not an argument about enumeration volume or instability. The addendum +adds a 48-module graph and concurrent create/remove churn and retains the 2 s +budget from that evidence. --- diff --git a/docs/screenshare-audio-exclusion-plan.md b/docs/screenshare-audio-exclusion-plan.md index 7d863ba..34b8c34 100644 --- a/docs/screenshare-audio-exclusion-plan.md +++ b/docs/screenshare-audio-exclusion-plan.md @@ -1,14 +1,13 @@ -# Design v3: whole-desktop screen-share audio without self-echo +# Design v3.8: whole-desktop screen-share audio without self-echo -**Status:** 🟢 **v3.7 — round 10: the §5.1 matrix PASSED in full and the architecture is -unchanged for the third consecutive measured round.** Round 8 revised the **observation -boundary** (§6.7), round 9 revised what stickiness may remember (§6.8), and round 10 deletes -the pipewire-pulse PID **derivation heuristic** (§6.1.2) after measuring that WirePlumber -repeats a `sec_pid` too — which had switched key 4's suppression off permanently. All three -were found by running code, not by reading it, and all three were at the *observation* -boundary rather than in the design. -**Date:** 2026-07-26 (v1: 07-19 · v2: 07-20 · Option C 07-20 · v3.1 r4 · v3.2 r5 · v3.3 r6 · -v3.4 r7 · v3.5 r8 · v3.6 r9 · v3.7 r10) +**Status:** 🟢 **v3.8 — round 11: both pre-Phase-6 design gates are closed.** The graph now +models an unobservable playback-to-capture route as a conservative same-`device.id` hardware +edge (§6.9), and the 2 s readiness budget is retained after repeated baseline, inflated-graph, +and live-churn calibration. The targeted live dry-run partition passed. The first pure Phase 6 +channel-planning prerequisite is committed locally in PixelPass `781defc`; the live fan-out +remains the first graph mutation. +**Date:** 2026-08-21 (v1: 07-19 · v2: 07-20 · Option C 07-20 · v3.1 r4 · v3.2 r5 · v3.3 r6 · +v3.4 r7 · v3.5 r8 · v3.6 r9 · v3.7 r10 · v3.8 r11 2026-08-21) **Origin:** Joe's suggestion — "whitelist all audio except audio coming from peerspeak." **Scope:** a new capture mode in pixelpass (`src/host/pipeline.rs`, `src/host/audio.rs`), playback tagging + AEC-identity export + teardown-ordering invariants in peerspeak. @@ -29,6 +28,7 @@ v1/v2 remain in git history at `88ad5a0` and `10203e1`. | fan-out spike | `~/Documents/handoff-docs/Claude/peerspeak/fanout-spike-results-2026-07-20.md` + Codex rounds 3/4 | **Option C adopted**, ratified | | AEC identity gate | `~/Documents/handoff-docs/Claude/peerspeak/aec-playback-leg-identity-2026-07-20.md` | **🟢 gate passed**, both models agree after 2 adversarial rounds | | **phase 5 dry-run gate (r8)** | `docs/screenshare-audio-exclusion-phase5-results.md` | **🚦 GATE FAILED** — the observation boundary is wrong (§6.7); architecture unaffected | +| **pre-Phase-6 closure (r11)** | `docs/screenshare-audio-exclusion-phase5-results.md` addendum | **PASSED** — same-device bridge, different-device negative control, and readiness calibration (§6.9) | --- @@ -907,21 +907,74 @@ through a teardown, and it is unaffected by this change. Cost: two fixpoints per graph event. Measured 80 µs worst case against a 47 Hz event rate, so the O5 headroom absorbs it without argument. -⚠️ **Owed, from the round-9 review (Codex, P1 "worth checking"): hardware -playback-to-capture paths.** A card offering "Stereo Mix" / "Digital Loopback" presents an +⚠️ **Round-9 open item — CLOSED in round 11 (§6.9): hardware playback-to-capture +paths.** A card offering "Stereo Mix" / "Digital Loopback" presents an ordinary driver name (`snd_hda_intel`), so both its sink and its source classify `session_device` — and audio written to the sink reappears on the source through a hop the Link graph cannot see. This is the `snd_aloop` hazard (§6.1.1, phase-3 review finding 2) in a form the driver denylist cannot detect. It is **not new in round 9** and not introduced by -either recent round; distinguishing it needs ALSA control inspection, a new I/O surface and -therefore a design decision. Until then a card with that path enabled can carry the call -from sink to source untainted, and a capture app reading it can re-emit: **echo**. +either recent round. Round 11 closes it without relying on a driver denylist or control name. -⚠️ **Also owed: a calibration argument for the readiness budget.** The observer times out +⚠️ **Round-9 open item — CLOSED in round 11 (§6.9): calibration of the readiness +budget.** The observer times out after 2 s and `TimedOut` is sticky by design, so a process that never sees one -obligation-free instant during initial enumeration is silent for its lifetime. Measured on -this host: readiness at ~3 ms with 19 binds. The margin is three orders of magnitude, which -is an argument, but it is one measurement on one idle desktop. +obligation-free instant during initial enumeration is silent for its lifetime. The original +~3 ms observation on one idle desktop was not enough; round 11 adds repeated starts, an +inflated graph, and concurrent graph churn. + +### 6.9 🟢 Same-device hardware bridge + readiness calibration (round 11, MEASURED) + +**The hardware rule.** For every positively classified passive hardware terminal, retain the +Node's snapshot-local `device.id`. The taint walk adds a directed synthetic edge from an +`Audio/Sink` (or output side of `Audio/Duplex`) to every `Audio/Source` (or input side of +`Audio/Duplex`) carrying the **same** `device.id`: + +``` +tainted stream -> hardware sink ~[private mixer / firmware]~> same-device source -> reader +``` + +Both predicates are load-bearing. `session_device=true` limits the rule to the observer's +positive passive-hardware allowlist; `device.id` limits it to one physical Device instead of +fusing every card exported by WirePlumber. The id never enters sticky identity and never +survives its snapshot. + +**Why this is unconditional rather than an ALSA-control probe.** Measured 2026-08-21: the +ALC897 exposes `Loopback Mixing` (disabled) and two `Input Source` controls containing Rear +Mic, Front Mic and Line, but no `Stereo Mix`. Linux HDA treats analog loopback monitoring and +the optional `Stereo Mix` capture source as distinct mechanisms (kernel +[`hda_generic.c`](https://code.googlesource.com/linux/torvalds/linux/+/master/sound/pci/hda/hda_generic.c) +and [HDA control documentation](https://cdn.kernel.org/doc/html/latest/sound/hd-audio/controls.html)). +More importantly, ALSA/HDA +control spelling cannot prove the absence of USB, vendor-DSP or firmware loopback paths. A +control-name allowlist would therefore be precise on this card and unsound as a portable +absence proof. The graph rule closes every such hidden same-device hop without a new runtime +ALSA dependency. + +**Accepted cost.** An app capturing a microphone from the same Device that is receiving +tainted playback is excluded even when that particular microphone path is clean. A source on +a different Device remains eligible. This is deliberate fail-closed over-exclusion, pinned by +pure exact-partition tests and a live dry-run negative control. + +**Live partition, 2026-08-21.** Tagged playback was routed to the ALC897 sink. A controlled +reader/re-emitter on the ALC897 source was excluded `tainted-owner-bridge`; the identical +reader/re-emitter on the Arctis source remained eligible. The audit was graph-ready, all +temporary modules were unloaded by exact module id, and the configured default sink/source +were unchanged. + +**Readiness budget — keep 2 s for v1.** Fresh observer startup measurements on the same live +desktop, using the observer's own monotonic `at_ms` clock: + +| arm | runs | p50 | p95 | max | timeout | +| --- | ---: | ---: | ---: | ---: | ---: | +| ambient graph | 30 | 5 ms | 10 ms | 11 ms | 0 | +| inflated graph: 24 null sinks + 24 loopbacks | 30 | 111 ms | 113 ms | 114 ms | 0 | +| 250 create/remove cycles concurrent with fresh starts | 20 | 5 ms | 6 ms | 8 ms | 0 | + +The deliberately inflated maximum leaves 17.5x headroom to the sticky 2 s deadline. This is +not a universal latency promise; it is a calibration argument that exercises enumeration +volume and graph instability, rather than extrapolating from one idle start. Revisit the +budget if a supported target measures startup p95 above 500 ms or produces a real timeout; +do not weaken `TimedOut`'s fail-closed/sticky semantics to hide one. ## 7. Lifecycle and teardown invariants @@ -1227,6 +1280,11 @@ requirement to assert what must remain **eligible**: §6.1.2's pulse-PID derivat Three rows passed with recorded substitutions (8 EasyEffects, 9 Firefox's own mic/monitor paths, 13 a real `Audio/Duplex` device) and the third-party samples stay owed. +**Round-11 supersession (2026-08-21):** the row above is the round-10 snapshot, not current +status. F11-1 and phases 0b/0c/0d are satisfied, and §6.9 closes the hardware-path decision +plus readiness calibration. S4/S5/0d, round 11, and Phase 6's first pure channel planner are +committed locally in PixelPass `781defc`; the first live fan-out mutation remains pending. + **Round 9 (2026-07-25).** Phase 3r shipped §6.7 and the audit was re-run immediately; it found a *second* measured defect within minutes — a permanent sticky taint on a hardware sink (§6.8). Both rounds share a shape worth naming: **the architecture was @@ -1286,8 +1344,8 @@ How the blockers closed: | **9** | **a fail-closed unresolved mark became permanent sticky taint (measured, phase 5 re-run)** | **fixed** — §6.8 evidence-only sticky pass | | **9** | `device_props` tested for one live *Device* rather than one live *global* on the id (Codex, certain) | **fixed** in phase 3r — stale `session_device` on a contested id is an echo path | | **9** | `device.api` corroborated by presence, so `v4l2` under an ALSA factory passed (Codex) | **fixed** in phase 3r — the API must equal the allowlist's own | -| **9** | hardware playback-to-capture ("Stereo Mix") defeats the `session_device` classifier (Codex, P1 worth checking) | **OPEN — design decision owed**, §6.8; pre-existing, needs ALSA control inspection | -| **9** | the 2 s readiness budget has no calibration argument (Codex) | **OPEN — measurement owed**, §6.8; 1–2 ms observed on this host with 18 binds (phase-5 run 2) | +| **9 → 11** | hardware playback-to-capture ("Stereo Mix") defeats the `session_device` classifier (Codex, P1 worth checking) | **CLOSED — §6.9.** Conservative `Sink → Source` edge for passive terminals sharing `device.id`; pure and targeted live exact partitions pass. No runtime control-name guess. | +| **9 → 11** | the 2 s readiness budget has no calibration argument (Codex) | **CLOSED — §6.9.** 30 baseline starts, 30 starts with 48 temporary modules, and 20 starts during 250 create/remove cycles; inflated max 114 ms, zero timeouts. Keep 2 s. | | **10** | **the pulse-PID derivation required a *single* repeated `sec_pid`; WirePlumber repeats one too, so it returned `None` permanently and key 4's suppression never fired (measured, phase-5 run 2)** | **fixed** — §6.1.2 round-10 box: probe every distinct `sec_pid`, let `/proc//comm` decide | | **10** | the audit's `sticky` flag means "is in the remembered set", so it is true for nearly every tainted node and does not answer "excluded only because remembered" | **OPEN — reporting only**; the evidence-only pass §6.8 already computes what is needed | | **10** | a bridge's named key is lost when a leg reappears under a new serial (sticky `reason_for` falls back to keyless, and `raise` will not replace a same-rank reason) | **OPEN — reporting only**; verdict unaffected | @@ -1297,7 +1355,8 @@ How the blockers closed: Option C fan-out · explicit `--aec=off|pulse-module:` · peerspeak playback and child tagging · exact AEC module validation · graph taint with the owner-key union and the pipewire-pulse PID exception · sticky taint · readiness epoch · fail-closed unresolved -ancestry · owned non-lingering links · §10 items 1, 4 and 5 landed first. +ancestry · conservative same-device hardware bridge · owned non-lingering links · §10 items +1, 4 and 5 landed first. ### Deliberately OUT of v1 @@ -1311,20 +1370,20 @@ binding, so `port.exclusive` is never observed and the §6.2 row it guards relie create failing cleanly · **(r8)** no serial-continuity signal for the AEC validator's no-coalescing contract. -### Next step (round 9) +### Current front after round 11 -Phases 0a, 2, 3, 3r, 4 and 5 are built; §6.7 and §6.8 are implemented and merged. What -remains before phase 6 unblocks: +Phases 0a–5 and 3r are built; the full phase-5 matrix passed; 0b is merged; and 0c/0d plus +the S4/S5 ownership work are built, validated, and committed locally. Round 11 closes the last +two pre-Phase-6 design decisions with pure, observer, live PipeWire and targeted dry-run +evidence. Phase 6's channel-aware pure planner is also committed in PixelPass `781defc`. -1. ~~**Revise phase 3** to §6.7~~ — **done**, phase 3r merged, four-part gate passed - including the live prop-recovery row and an added live gate for the Device-side path. -2. **Revise phase 1** to emit both carriers (§5.1), literals pinned in plan §3. Unblocked - and next. -3. **Re-run the whole phase-5 §5.1 matrix** — no row was completable under the round-8 - defect, so nothing carries over — and **re-measure O5** with bind I/O *and* the round-9 - second fixpoint in it. Phase 6 stays blocked until that results file passes. -4. Decide the two items §6.8 leaves open: hardware playback-to-capture paths (a real echo - path, needs a design call) and the readiness-budget calibration. +1. **Continue Phase 6:** implement the owned, non-lingering fan-out link manager and causal + status events through the real host path. This is still the first exclusion-related graph + mutation. +2. Carry the revised row-9 partition forward: music-only and a different-device microphone + stay eligible; same-device capture and a tainted-monitor capture are excluded. +3. Build the next bounded mutation slice from PixelPass checkpoint `781defc`, preserving its + ownership, fail-closed, and exact-channel planning gates. Still owed beyond that, unchanged: the §9.2 rig upgrade before any exclusion claim is published, and **field-test §12** — nothing in this design has been tested over the real