docs: advance audio exclusion plan into phase 6

This commit is contained in:
2026-08-21 15:40:12 -04:00
parent 9ba42c4cda
commit 9e52acf9d3
3 changed files with 242 additions and 56 deletions
+88 -18
View File
@@ -1,14 +1,19 @@
# Implementation plan: whole-desktop screen-share audio without self-echo # 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** 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 S1S5: S1 and S2 are merged; **S3a is merged (§1). The two pre-Phase-6 decisions are now **closed** in design v3.8 §6.9: the conservative
locally** in pixelpass (`15d1374`); and **S3b is built and validated in the pixelpass working same-device hardware bridge is built and validated, and the 2 s readiness budget passed
tree and committed locally** (`5d3da8b`). S4/S5 are not complete. **0d and phases 69 are not baseline, inflated-graph and live-churn calibration. The completed **0c step 2** was
built** — nothing merged to date mutates the graph, so the feature has no user-visible behaviour sliced S1S5: S1 and S2 are
yet. Phase 6 is the first mutation. merged; **S3a is merged locally** in pixelpass (`15d1374`); and **S3b is built, validated, and
**Date:** 2026-07-21 (v4); status line refreshed 2026-08-15 committed locally** (`5d3da8b`). **S4, S5, 0d, round 11's bridge changes, and Phase 6's first
**Design of record:** [`screenshare-audio-exclusion-plan.md`](screenshare-audio-exclusion-plan.md) v3.4 (`8768cd2`), converged round 7. pure channel-planning prerequisite are built, validated, and committed locally in pixelpass
(`781defc`).** The Phase 6 mutation and phases 79 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.* **Scope:** *ordering, gates and acceptance criteria only.*
**Reference convention.** `v3.4 §N` = the design doc. `plan §N` = this document. The two **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 (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 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 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 of the DAG: `0b → 6` is satisfied and merged; **0c and 0d are built, validated, and committed
open** (S1 and S2 merged; S3a merged locally in pixelpass; S3b built, validated and committed locally with S4/S5 and round 11 in pixelpass `781defc`**. **Round 11 now closes the two former
locally at `5d3da8b`; S4/S5 incomplete), and **0d has not been built at all**. 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 ⚠️ **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 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; capture sink;
- legacy behaviour byte-identical. - 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 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. 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* > 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 > 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 > 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 > **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 > `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 > ⚠️ **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 > **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 > 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 > 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 > 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 | — | | 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) | — | | 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 | | 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 | | 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 | | 11 | recycled serial/index/link-group after teardown | — | must **not** inherit taint |
| 12 | AEC loaded, then unloaded | four nodes; then `Revoked` | — | | 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 test. It stays as row 1b, **opportunistic and non-gating**, because it cannot be relied on to
be present. be present.
**Any surprise here goes back to the design doc as round 8. Phase 6 does not start until this **Any surprise here goes back to the design doc as a new measured round. Phase 6 did not start
results file exists.** 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 link is `ACTIVE`, same-epoch revalidation immediately before each creation, proxy drop on
ancestry becoming unsafe. 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 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. 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 | | 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 | | 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 | | 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 | | 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 | the two live exit gates: two-host ownership, and the never-yet-run Stop Share SIGINT gate | needs S4 on the graph | | 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 **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.** through libpulse, and one of the review's own prescriptions had to be replaced after measuring.**
@@ -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 ## What changed since run 1
Run 1 failed on two defects, both fixed before this run: 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. teardown in bursts, and a 32 µs recompute drains a burst faster than it forms.
`busy_fraction` is the number to trust. `busy_fraction` is the number to trust.
⚠️ **The readiness budget still has no calibration argument.** 12 ms against ⚠️ **Historical run-2 finding, closed by the 2026-08-21 addendum above:** 12 ms
2000 ms is three orders of magnitude of slack on *this* host with 18 binds; it is against 2000 ms was three orders of magnitude of slack on *this* host with 18
not an argument about a host with a large USB interface, many virtual devices, or binds, but not an argument about enumeration volume or instability. The addendum
a cold cache. Carried forward as open, unchanged. adds a 48-module graph and concurrent create/remove churn and retains the 2 s
budget from that evidence.
--- ---
+93 -34
View File
@@ -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 **Status:** 🟢 **v3.8 — round 11: both pre-Phase-6 design gates are closed.** The graph now
unchanged for the third consecutive measured round.** Round 8 revised the **observation models an unobservable playback-to-capture route as a conservative same-`device.id` hardware
boundary** (§6.7), round 9 revised what stickiness may remember (§6.8), and round 10 deletes edge (§6.9), and the 2 s readiness budget is retained after repeated baseline, inflated-graph,
the pipewire-pulse PID **derivation heuristic** (§6.1.2) after measuring that WirePlumber and live-churn calibration. The targeted live dry-run partition passed. The first pure Phase 6
repeats a `sec_pid` too — which had switched key 4's suppression off permanently. All three channel-planning prerequisite is committed locally in PixelPass `781defc`; the live fan-out
were found by running code, not by reading it, and all three were at the *observation* remains the first graph mutation.
boundary rather than in the design. **Date:** 2026-08-21 (v1: 07-19 · v2: 07-20 · Option C 07-20 · v3.1 r4 · v3.2 r5 · v3.3 r6 ·
**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 · v3.8 r11 2026-08-21)
v3.4 r7 · v3.5 r8 · v3.6 r9 · v3.7 r10)
**Origin:** Joe's suggestion — "whitelist all audio except audio coming from peerspeak." **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`), **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. 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 | | 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 | | 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 | | **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, 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. so the O5 headroom absorbs it without argument.
⚠️ **Owed, from the round-9 review (Codex, P1 "worth checking"): hardware ⚠️ **Round-9 open item — CLOSED in round 11 (§6.9): hardware playback-to-capture
playback-to-capture paths.** A card offering "Stereo Mix" / "Digital Loopback" presents an paths.** A card offering "Stereo Mix" / "Digital Loopback" presents an
ordinary driver name (`snd_hda_intel`), so both its sink and its source classify 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 `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 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 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 either recent round. Round 11 closes it without relying on a driver denylist or control name.
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**.
⚠️ **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 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 obligation-free instant during initial enumeration is silent for its lifetime. The original
this host: readiness at ~3 ms with 19 binds. The margin is three orders of magnitude, which ~3 ms observation on one idle desktop was not enough; round 11 adds repeated starts, an
is an argument, but it is one measurement on one idle desktop. 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 ## 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 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. 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 **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 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 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** | **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_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** | `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 → 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** | the 2 s readiness budget has no calibration argument (Codex) | **OPEN — measurement owed**, §6.8; 12 ms observed on this host with 18 binds (phase-5 run 2) | | **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/<pid>/comm` decide | | **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/<pid>/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** | 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 | | **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:<idx>` · peerspeak playback and child Option C fan-out · explicit `--aec=off|pulse-module:<idx>` · peerspeak playback and child
tagging · exact AEC module validation · graph taint with the owner-key union and the 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 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 ### 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 create failing cleanly · **(r8)** no serial-continuity signal for the AEC validator's
no-coalescing contract. 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 Phases 0a5 and 3r are built; the full phase-5 matrix passed; 0b is merged; and 0c/0d plus
remains before phase 6 unblocks: 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 1. **Continue Phase 6:** implement the owned, non-lingering fan-out link manager and causal
including the live prop-recovery row and an added live gate for the Device-side path. status events through the real host path. This is still the first exclusion-related graph
2. **Revise phase 1** to emit both carriers (§5.1), literals pinned in plan §3. Unblocked mutation.
and next. 2. Carry the revised row-9 partition forward: music-only and a different-device microphone
3. **Re-run the whole phase-5 §5.1 matrix** — no row was completable under the round-8 stay eligible; same-device capture and a tainted-monitor capture are excluded.
defect, so nothing carries over — and **re-measure O5** with bind I/O *and* the round-9 3. Build the next bounded mutation slice from PixelPass checkpoint `781defc`, preserving its
second fixpoint in it. Phase 6 stays blocked until that results file passes. ownership, fail-closed, and exact-channel planning gates.
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.
Still owed beyond that, unchanged: the §9.2 rig upgrade before any exclusion claim is 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 published, and **field-test §12** — nothing in this design has been tested over the real