docs: round 9 — uncertainty is not history (design v3.6)

Phase 3r landed §6.7 and the phase-5 audit was re-run against it immediately.
It found a second measured defect within minutes: a real hardware sink
carrying `unresolved-ancestry` permanently, from one link observed while its
output node was still unbound during enumeration. Round 8 made that
systematic rather than rare, because every node is now withheld until its
bind resolves.

New §6.8: sticky taint is a claim about history, and uncertainty is not
history. Retiring by reason code would not be enough — an unresolved node
propagates `TaintedUpstream`, which is indistinguishable from real
contamination once recorded — so the split is by provenance: the engine runs
its fixpoint twice, and only the evidence-only pass may feed sticky state.
Decisions are unchanged and still fail closed.

Also recorded in §6.8, both from Codex's round-9 review and both pre-existing:
hardware playback-to-capture paths ("Stereo Mix") defeat the `session_device`
classifier in a way the driver denylist cannot detect — a real echo path
needing a design call — and the 2 s readiness budget has no calibration
argument beyond one measurement on one idle desktop.

Impl plan: phase 3r marked built and merged with its gate results, including
the extra Device-side live gate and why row 1 alone could not cover it.
This commit is contained in:
2026-07-25 18:51:21 -04:00
parent 1cd19b355f
commit 6773a3882b
2 changed files with 128 additions and 20 deletions
+97 -14
View File
@@ -1,10 +1,11 @@
# Design v3: whole-desktop screen-share audio without self-echo
**Status:** 🟠 **v3.5 — round 8, reopened by a MEASURED implementation finding.** v3.4's
architecture is unchanged and still converged; what changed is the **observation boundary**
(new §6.7), which v3.4 got wrong in a way that made the built engine non-functional.
**Status:** 🟠 **v3.6 — round 9, opened by a second MEASURED finding, this time from a live
audit run of the *fixed* observer.** v3.4's architecture is still unchanged and converged.
Round 8 revised the **observation boundary** (§6.7); round 9 revises what stickiness is
allowed to remember (new §6.8). Both were found by running code, not by reading it.
**Date:** 2026-07-25 (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.4 r7 · v3.5 r8 · v3.6 r9)
**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.
@@ -801,6 +802,64 @@ produced correct answers), and the Link/Client observation path.
⚠️ **O5 must be re-measured.** The phase-5 numbers (max 15 µs recompute, `busy_fraction`
0.0004) were taken on the graph this defect produces and do not include per-node bind I/O.
### 6.8 🔴 Uncertainty is not history — what stickiness may remember (round 9, MEASURED)
**Found by the phase-5 audit on the live graph within minutes of §6.7's fix landing**, which
is the strongest available argument for the dry-run phase existing at all.
**The observation.** With phase 3r running, a real hardware sink
(`alsa_output.usb-…Arctis_1_Wireless…`) carried `unresolved-ancestry` **permanently** — the
mark survived the readiness epoch, 21 recomputes and deliberate module churn. Traced to a
single event during enumeration: a link was observed while its output node was still
unbound, so §6.1.4's fail-closed rule correctly raised `UnresolvedAncestry` on the input
side. That mark was then written into sticky state, and §6.1.3 retires a sticky entry only
when **every member object is absent** — which a live sound card never is.
**Why round 8 made it systematic rather than rare.** Under §6.7 every node is withheld until
its bind resolves, so *any* link observed across that gap raises unresolved ancestry. It
fires at startup, every startup, on whichever node happens to lose the race.
**The rule (round 9).**
> **Sticky taint is a claim about history, and uncertainty is not history.** A node tainted
> only because the graph could not be seen has had nothing observed about it. Decisions
> still fail closed on it — that is unchanged and non-negotiable — but nothing about it may
> be *remembered* once the uncertainty is gone.
**Retiring by reason code is not sufficient, and this is the load-bearing part.** Uncertainty
launders itself: an unresolved node propagates `TaintedUpstream` to everything downstream,
and that reason is indistinguishable from real contamination once recorded. The split must
be by **provenance**, so the engine runs its fixpoint twice per recompute:
| pass | uncertainty roots | consumed by |
| --- | --- | --- |
| fail-closed | raised (`UnresolvedAncestry`, `UnresolvedOwner`) | **every decision** — unchanged from v3.5 |
| evidence-only | never raised, so nothing derived from one exists | **sticky state, and only sticky state** |
Evidence-based taint — `peerspeak.owned`, the AEC identity, pixelpass-owned objects, a
foreign echo canceller, and anything propagated from them — keeps §6.1.3's absence rule
exactly as written. That is what stops an app buffering the call and laundering itself
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
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**.
⚠️ **Also owed: a calibration argument for 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.
## 7. Lifecycle and teardown invariants
### 7.1 ⚠️ The invariant
@@ -1084,7 +1143,23 @@ Both reviewers agree on all seven. Recorded as decided; reopen only with new evi
- **D7 — no materially simpler design exists** that still meets Joe's ask. The available
simplification is to *narrow v1 scope*, not to change architecture. ✅
## 14. Readiness — 🟠 v3.5 (round 8): architecture converged, observation boundary REVISED.
## 14. Readiness — 🟠 v3.6 (round 9): architecture converged; observation boundary and sticky provenance REVISED.
**Round 9 (2026-07-25, same day).** 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
right and the instrumentation was wrong**, and only running the code against a live daemon
found either.
| | verdict |
| --- | --- |
| Architecture — Option C, taint as a graph property, owner-key union, sticky taint, AEC identity state machine | **unchanged, twice vindicated** |
| §6.8 | **new** — sticky state is built from an evidence-only pass; decisions still fail closed |
| §6.1.3 | **narrowed** — the absence rule now governs evidence-based taint only |
| Phase 3r | **built and merged**, four-part gate passed incl. live |
| Phase 5 | **machinery unchanged and correct** — it has now caught two real defects on first contact with the live graph |
**Round 8 was not a review round.** It was opened by the phase-5 dry-run audit failing its
gate on the first live run: the engine built to v3.4 was measured **non-functional** — it
@@ -1126,6 +1201,11 @@ How the blockers closed:
| 7 | "resolves" = first-present would still leak | **fixed** — §6.1.2 wording trap + test |
| 7 | stickiness on recyclable ids | **fixed** — §6.1.3 lifetime-aware owner components |
| **8** | **node props are not on the registry global (measured, phase 5)** | **fixed** — §6.7 bind-every-node rule; §5.1 second carrier |
| **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; ~3 ms observed on this host |
### v1 scope — agreed
@@ -1146,17 +1226,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 8)
### Next step (round 9)
The sequenced implementation plan exists (`screenshare-audio-exclusion-impl-plan.md`) and
phases 0a, 2, 3, 4 and 5 are built. Round 8's work is:
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:
1. **Revise phase 3** to §6.7: bind every Node and Device, live prop tracking, one readiness
obligation per unbound node. Plan §4 "Phase 3 revision (round 8)".
2. **Revise phase 1** to emit both carriers (§5.1), literals pinned in plan §3.
3. **Re-run the whole phase-5 §5.1 matrix** — no row was completable under the defect — and
**re-measure O5** with bind I/O in it. Phase 6 stays blocked until that results file
passes.
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.
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