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)
|
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
|
⚠️ **Status 2026-07-25 (evening): 3r is BUILT AND MERGED; the re-run has not happened yet.**
|
||||||
DAG. Phase 5's machinery is built and correct — it is the audit that found the defect — so
|
The phase-5 gate failed on its first live run and put 3r into the DAG; 3r's own four-part
|
||||||
"5 (re-run)" is a *re-run of the matrix*, not a rebuild. **Phase 6 does not start** until a
|
gate now passes, including the live prop-recovery row on this host. Phase 5's machinery is
|
||||||
passing results file exists. Phases 2 and 4 need no change; the defect is entirely in 3.
|
built and correct — it is the audit that found the defect, twice — so "5 (re-run)" is a
|
||||||
**Phase 1 is now a hard prerequisite of the re-run for both carriers** (plan §3).
|
*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
|
- **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
|
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 |
|
| **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 |
|
| **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
|
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
|
**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.
|
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.**
|
**Requirements.**
|
||||||
|
|
||||||
1. **Bind every `Node` global**, unconditionally, no `media.class` filter. Retain the proxy
|
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
|
# Design v3: whole-desktop screen-share audio without self-echo
|
||||||
|
|
||||||
**Status:** 🟠 **v3.5 — round 8, reopened by a MEASURED implementation finding.** v3.4's
|
**Status:** 🟠 **v3.6 — round 9, opened by a second MEASURED finding, this time from a live
|
||||||
architecture is unchanged and still converged; what changed is the **observation boundary**
|
audit run of the *fixed* observer.** v3.4's architecture is still unchanged and converged.
|
||||||
(new §6.7), which v3.4 got wrong in a way that made the built engine non-functional.
|
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 ·
|
**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."
|
**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.
|
||||||
@@ -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`
|
⚠️ **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.
|
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. Lifecycle and teardown invariants
|
||||||
|
|
||||||
### 7.1 ⚠️ The invariant
|
### 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
|
- **D7 — no materially simpler design exists** that still meets Joe's ask. The available
|
||||||
simplification is to *narrow v1 scope*, not to change architecture. ✅
|
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
|
**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
|
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 | "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 |
|
| 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 |
|
| **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
|
### 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
|
create failing cleanly · **(r8)** no serial-continuity signal for the AEC validator's
|
||||||
no-coalescing contract.
|
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, 3r, 4 and 5 are built; §6.7 and §6.8 are implemented and merged. What
|
||||||
phases 0a, 2, 3, 4 and 5 are built. Round 8's work is:
|
remains before phase 6 unblocks:
|
||||||
|
|
||||||
1. **Revise phase 3** to §6.7: bind every Node and Device, live prop tracking, one readiness
|
1. ~~**Revise phase 3** to §6.7~~ — **done**, phase 3r merged, four-part gate passed
|
||||||
obligation per unbound node. Plan §4 "Phase 3 revision (round 8)".
|
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.
|
2. **Revise phase 1** to emit both carriers (§5.1), literals pinned in plan §3. Unblocked
|
||||||
3. **Re-run the whole phase-5 §5.1 matrix** — no row was completable under the defect — and
|
and next.
|
||||||
**re-measure O5** with bind I/O in it. Phase 6 stays blocked until that results file
|
3. **Re-run the whole phase-5 §5.1 matrix** — no row was completable under the round-8
|
||||||
passes.
|
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
|
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
|
||||||
|
|||||||
Reference in New Issue
Block a user