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
**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 S1S5: 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 69 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 S1S5: 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 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.*
**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.**