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:
@@ -100,11 +100,21 @@ if it differs, failing closed.
|
||||
1 (r8 carriers) ──────────────────────────────────────► 5 (re-run)
|
||||
```
|
||||
|
||||
⚠️ **Status 2026-07-25: the phase-5 gate FAILED on its first live run** and put 3r into the
|
||||
DAG. Phase 5's machinery is built and correct — it is the audit that found the defect — so
|
||||
"5 (re-run)" is a *re-run of the matrix*, not a rebuild. **Phase 6 does not start** until a
|
||||
passing results file exists. Phases 2 and 4 need no change; the defect is entirely in 3.
|
||||
**Phase 1 is now a hard prerequisite of the re-run for both carriers** (plan §3).
|
||||
⚠️ **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 the DAG; 3r's own four-part
|
||||
gate now passes, including the live prop-recovery row on this host. Phase 5's machinery is
|
||||
built and correct — it is the audit that found the defect, twice — so "5 (re-run)" is a
|
||||
*re-run of the matrix*, not a rebuild. **Phase 6 still does not start** until a passing
|
||||
results file exists. **Phase 1 is a hard prerequisite of the re-run for both carriers**
|
||||
(plan §3).
|
||||
|
||||
⚠️ **A smoke run of the audit against the fixed observer immediately found a second defect
|
||||
(design v3.6 §6.8): a fail-closed `unresolved-ancestry` mark was being promoted to permanent
|
||||
sticky taint.** Fixed in the taint engine (evidence-only sticky pass, 3 new tests,
|
||||
mutation-verified) and merged. Decisions were unaffected — all 57 phase-2 tests passed
|
||||
untouched — so this is a change to what stickiness *remembers*, not to what it *decides*.
|
||||
Note the pattern for the re-run: the matrix rows assert exact partitions, and a stale sticky
|
||||
entry from enumeration would have contaminated every one of them.
|
||||
|
||||
- **0b strictly precedes 6.** v2/v3 drew 0b with no continuing edge. Phase 6 is the first phase
|
||||
that creates objects whose lifetime is tied to pixelpass being alive, so the teardown-ordering
|
||||
@@ -356,11 +366,26 @@ removals are ignored, the fallback is dead code, and readiness releases early:
|
||||
| **PID derivation matrix** (round-2): consistent valid PID · inconsistent PIDs · missing client property · `/proc` entry missing · `comm` mismatch · PID reuse — **every failure makes owner-bridge key 4 unusable** | the pure engine can be correct on a wrong context; this is where the context is built |
|
||||
| **live**: create and destroy a controlled node/link topology; diff Nodes, **Ports**, Links and Clients before/during/after | the adapter tracks a *changing* graph, not a static one |
|
||||
|
||||
### Phase 3 revision (round 8) — bind every Node and Device 🔴 blocks the phase-5 re-run
|
||||
### Phase 3 revision (round 8) — bind every Node and Device ✅ BUILT AND MERGED 2026-07-25
|
||||
v3.5 §6.7. Phase 3 shipped reading node properties off the registry `global` event, where
|
||||
**eight of them are never announced**. This is the fix. Scope is the observer only — phases 2
|
||||
and 4 are unaffected, and the phase-5 audit machinery is already correct.
|
||||
|
||||
> **🟢 Done.** Pure core + adapter, split-seam with mutual review as in phase 3 (mine and
|
||||
> Codex's respectively, each reviewing the other). All four gate rows below pass, the live
|
||||
> row on this host. Codex's review of the core found no certain P1; two findings taken and
|
||||
> 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.
|
||||
>
|
||||
> **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
|
||||
> `device.api`/`alsa.driver_name` onto ALSA nodes on this host — so row 1 passes through the
|
||||
> node fallback and would keep passing if the Device bind delivered nothing at all, leaving
|
||||
> §6.7 decision 4 ungated on the development machine. Verified by mutation: breaking the
|
||||
> Device-side driver read fails the new test while row 1 still passes.
|
||||
|
||||
**Requirements.**
|
||||
|
||||
1. **Bind every `Node` global**, unconditionally, no `media.class` filter. Retain the proxy
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user