docs: §15.16's two lists, and the reproduction kept for upstream
Codex S4-16 (P3), and the round-3 record. §15.16 called the yield unconditional when its allowance can bypass it; it now says so, and says the wait itself runs to its end once entered. Its "four things not its business" ran two different lists together — slow stuttering was in it, which the watch expressly repairs — so the stall criteria and the exemptions are now separate: what counts (decode rising, track live and unmuted, element not paused, clock advancing less than a quarter of a second, three polls in a row), what the repair is, and what is exempt (receiving nothing, ended or muted, paused for autoplay). And the recovery claim is qualified by what the gate now actually measures — growth in every one of three windows of a second and a half — with what is still not established said plainly: why a fresh MediaStream unblocks the element is unknown. The record gains §8, and with it the thing this slice learned that nobody else has: the identity, decode and render reproduction, written out as material for a Chromium report. Two peers sharing at once with one side's offers held past the yield; the rollback counted rather than assumed; and then a receiver that decodes hundreds of frames while the element holds exactly that receiver's own live, unmuted track and paints none of them. It locates the defect — downstream of decode, upstream of paint — in a reproduction that runs headless in under a minute. It does not explain it, and the section says so.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CzvnuKhLzudvq7jZ7SvVt2
2 files changed,
+127
−28
docs/superpowers/reviews/2026-09-10-slice-4-record.md+80 −0docs/superpowers/specs/2026-09-07-meet-design.md+47 −28
diff --git a/docs/superpowers/reviews/2026-09-10-slice-4-record.md b/docs/superpowers/reviews/2026-09-10-slice-4-record.md| index 6549b3b..2ba81df 100644 |
| --- a/docs/superpowers/reviews/2026-09-10-slice-4-record.md |
| +++ b/docs/superpowers/reviews/2026-09-10-slice-4-record.md |
| @@ -556,3 +556,83 @@ times**, 484 s and 485 s, with `laterolls` in it and `skipped` empty — |
| the first time this branch's gate has carried no skipped scenario since |
| Task 5 put `mutualshare` in it. 206 node tests, `make ci`, `-race` and |
| `go vet -tags browser` green. |
| + |
| +--- |
| + |
| +## 8. Codex round 3 |
| + |
| +Judged at `a4d1ec1`. S4-7, S4-9, S4-10 and S4-11 closed; S4-8 and S4-6 |
| +half-closed, each with the half that was missing named as a new |
| +finding; three more beside them. All five are closed on this branch. |
| + |
| +| Finding | Disposition | Commit | Test | |
| +|---|---|---|---| |
| +| **S4-12 (P2)** — the retry timer spent the ask it was meant to protect: it fired mid-exchange, `negotiate` cleared the flag, slept, woke to a connection still not stable and walked away (pending false, timers none, offers none) | Fixed. An attempt that settles nothing owes the ask back: `politeYield` answers with the reason rather than a bare no, a `busy` wake and an offer that never went out both hand `renegotiate` back, and the timer defers instead of consuming, bounded by `NEGOTIATE_DEFERS` | `ad13608` | Five, both halves mutation-verified; the harness runs the timer callback now, which is what the round-2 tests never did | |
| +| **S4-13 (P2)** — the render repair handed the element a fresh stream and left the role writing to the old one, so a microphone arriving or being replaced afterwards went where nothing was showing: authoritative [video, new mic], element [video, old mic] | Fixed. `freshStream` puts the new object back where the role keeps it, which restores the rule the file has about tiles: the element holds the role's stream, and every track adopted later reaches it | `5a0be1e` | Three replay tests, mutation-verified against the element-only version | |
| +| **S4-15 (P2)** — a screen replaced on its own mid ends afterwards, and its `ended` listener checked only the connection, so the dead screen took the live replacement's tile with it | Fixed. The share is dropped only if the ending track is still the one being shown | `5a0be1e` | One test, mutation-verified; the screen that *is* showing still ends its tile | |
| +| **S4-14 (P2)** — `sustained` was two early-return waits: 0, 1, 2, 3 then frozen for ever passed with a twenty-second argument, and streams were walked one after another | Fixed. `observe` requires growth in every one of three windows of 1.5 s, `watchMedia` reads every stream on both pages in one pass, and each caller names the keys it expects rather than asking the page what it is showing | `85e0a61` | Ten node tests in a new `media.test.mjs`, Codex's burst-then-freeze reproduction among them | |
| +| **S4-16 (P3)** — §15.16 called the yield unconditional, listed slow stuttering as exempt when the watch repairs it, and claimed a recovery resting on those windows | Fixed in the spec: the yield is conditional on its allowance, stall criteria and exemptions are two separate lists, and the recovery claim is now "bounded and timed", with what is still not established said plainly | this commit | — | |
| + |
| +**What the new windows found on their first run**, which is the whole |
| +argument for them: `laterolls` went red with `28->28->28->28` on one |
| +camera. Two of that was mine — the scenario's own pre-check was "the |
| +counter is above zero", which a counter left over from before the |
| +collision answers by itself, and the settle check ran before the |
| +recovery rather than after it, so a pair mid-rebuild read as a pair |
| +that never settled. Fixed in the same commit: growth from a fresh |
| +baseline for the *arrival* of recovery, then settling, then the |
| +windows. It passes with the stall really happening first — rendered |
| +frames `[25,25,25,25,25,25]`, recovery at 26 → 28, and |
| +`273->296->320->351` through the windows. |
| + |
| +### The reproduction, kept for an upstream report |
| + |
| +This is the material for a Chromium bug, and it is written down here |
| +because it is the thing this slice learned that nobody else has. |
| + |
| +**Setup.** Two peers on one connection, both sharing a screen at the |
| +same moment, with one side's own `offer` requests held back four times |
| +`POLITE_YIELD` (1200 ms) so the remote offer lands after the yield and |
| +the polite side rolls back. The gate's `laterolls` scenario does |
| +exactly this and counts the rollback (`setRemoteDescription` of an |
| +offer while in `have-local-offer`) rather than assuming it. |
| + |
| +**What is observed on the *receiving* side afterwards**, from |
| +`meet.media()`, which compares the objects inside the page because |
| +identity does not survive an `evaluate()`: |
| + |
| +``` |
| +cameraMid 1 |
| +wanted ce46729b-…aef7 (the track onRemoteTrack recorded) |
| +bound ce46729b-…aef7 (the track the tile is bound to) |
| +element ["ce46729b-…aef7:live"] (the tracks in video.srcObject) |
| +receivers mid 1, sendrecv, live, muted:false, isCamera:true |
| +agrees true |
| +currentTime 1.452 (and it does not move again) |
| +``` |
| + |
| +with `getStats` on the same connection reporting `framesDecoded` rising |
| +throughout for that mid, and the far side's `framesEncoded` rising too. |
| +So: the sender encodes, the receiver decodes, the element holds exactly |
| +the receiver's own live, unmuted track — and paints nothing. Everything |
| +upstream of the element is right and the picture is stopped downstream |
| +of it. |
| + |
| +**The workaround that has an effect**, and it is a workaround: taking |
| +the element's `srcObject` away and giving it a *new* `MediaStream` over |
| +the same tracks. Re-assigning the same object does not do it — that was |
| +measured, and it bought a frame or two. Rendered-frame totals every |
| +500 ms, before and after the watch that does this: `[25,26,26,26,26,26]` |
| +and `[25,25,25,25,25,26]`; a later run `[25,25,25,25,25,25]` then |
| +recovery. |
| + |
| +**What is not established.** Why a new stream object unblocks the |
| +element; whether the decoder pipeline is rebuilt or merely re-attached; |
| +and whether the rollback is the cause or only the trigger. Nothing here |
| +identifies the Chromium defect — it locates it, downstream of decode |
| +and upstream of paint, with a reproduction that runs in a headless |
| +Chromium in under a minute. |
| + |
| +**Destination: file it upstream**, with this scenario and these |
| +readings. Until it is understood, §15.16 claims a bounded, timed |
| +recovery and nothing more. |
| \ No newline at end of file |
diff --git a/docs/superpowers/specs/2026-09-07-meet-design.md b/docs/superpowers/specs/2026-09-07-meet-design.md| index 4c778f6..8f34a37 100644 |
| --- a/docs/superpowers/specs/2026-09-07-meet-design.md |
| +++ b/docs/superpowers/specs/2026-09-07-meet-design.md |
| @@ -1705,8 +1705,11 @@ stopping, or being dropped. |
| **Decision.** Before making an offer, the polite side of a pair |
| (§5.1's own tie-break, the lower peer id) waits 300 ms |
| -(`POLITE_YIELD`). The wait is unconditional; what is read after it is |
| -the connection. If a remote offer arrived meanwhile, or a whole remote |
| +(`POLITE_YIELD`) — while it has an allowance left to spend, which is |
| +the paragraph after this one. A side with none offers straight away and |
| +takes the ordinary collision path, so the wait is a rule about the |
| +usual case rather than about every offer. Once entered, the wait itself |
| +runs to its end; what is read after it is the connection. If a remote offer arrived meanwhile, or a whole remote |
| exchange arrived and completed, no offer of this side's own goes out |
| and the negotiation belongs to whatever that left behind; otherwise the |
| offer goes out as it always did. |
| @@ -1742,27 +1745,33 @@ because a reset that did not take is not a reason to ask again every |
| two seconds. |
| **And a tile that decodes and does not paint is handed its media |
| -back.** One link further along, and the one nothing else notices. The |
| -same `getStats` walk reads `framesDecoded` per tile; three polls in a |
| -row where the decoder is delivering, the tile's track is live and |
| -unmuted, and the element's own clock has not moved is a rendering |
| -stall. The repair is a fresh `MediaStream` over the same tracks put |
| -back on the element and played again; two of those, then a reset, then |
| -it stops asking. |
| - |
| -Four things are deliberately not its business, and each of them cost a |
| -red gate run to learn. A tile **receiving nothing** is idle, not |
| -stalled, which is what reading the decode count alongside the clock is |
| -for. A track that has **ended or gone muted** is the sender's doing. An |
| -element the autoplay policy **paused** is the person's: §5 offers a |
| -Play control, and this watch — and every rebinding of a tile — leaves |
| -it alone while that control is showing, because calling `play()` behind |
| -it hides a question nobody has answered. And an element painting **one |
| -frame every few seconds** is stalling with a tic rather than playing: |
| -a second of video is a second of clock and a frame is thirty |
| -milliseconds, so the clock must advance by `RENDER_MOVED` (0.25 s) in a |
| -poll to count as health. Reading any advance at all as health let such |
| -a tile reset the budget for ever and never earn its reset. |
| +back.** One link further along, and the one nothing else notices. |
| + |
| +*What counts as a stall*, on the same one-second `getStats` walk, and |
| +all of it together: `framesDecoded` for that tile rising, the tile's |
| +track live and unmuted, the element not paused, and the element's own |
| +clock advancing by less than `RENDER_MOVED` (0.25 s) in the poll — |
| +three polls in a row of that. A second of video is a second of clock |
| +and a frame is thirty milliseconds, so a tile painting one frame every |
| +few seconds meets this criterion and **is repaired**: it is a stall |
| +with a tic, not health. Reading any advance at all as health let such a |
| +tile reset the budget for ever and never earn its reset. |
| + |
| +*The repair* is a fresh `MediaStream` over the same tracks put back on |
| +the element and played again — fresh, and the role's own, so every |
| +track adopted afterwards still reaches the element. Two of those, then |
| +a reset, then it stops asking, until qualifying clock movement gives |
| +the allowance back. |
| + |
| +*What is exempt*, which is a different list and not a softer version of |
| +the same one. A tile **receiving nothing** is idle: there is nothing to |
| +paint, and the decode count is read alongside the clock precisely to |
| +tell that apart from a stall. A track that has **ended or gone muted** |
| +is the sender's doing and the sender's to fix. An element the autoplay |
| +policy **paused** is the person's: §5 offers a Play control, and this |
| +watch — and every rebinding of a tile — leaves it alone while that |
| +control is showing, because calling `play()` behind it hides a question |
| +nobody has answered. Each of those three cost a red gate run to learn. |
| **Three separate facts, recorded separately**, because they were run |
| together once and the section contradicted itself. |
| @@ -1815,11 +1824,21 @@ Rendered frames every 500 ms read `[25,26,26,26,26,26]` — stopped, and |
| stopped for as long as anything watched — before the render watch, and |
| `[25,25,25,25,25,26]` after it: frozen for the three polls it takes to |
| notice, then moving again. A later run of the same scenario stopped |
| -dead at `[25,25,25,25,25,25]` and came back 21 → 26 across two |
| -successive intervals once the watch escalated past the stutter rule. |
| -Two of these paragraphs are recovery *after* a failure that is the |
| -browser's, not this app's; none of them is a claim that the rollback |
| -has stopped costing anything. |
| +dead at `[25,25,25,25,25,25]` and came back once the watch escalated |
| +past the stutter rule. |
| + |
| +**What that evidence is worth.** The recovery is now required to *last* |
| +rather than to happen: every media check in `share`, `mutualshare` and |
| +`laterolls` observes each expected stream over three windows of a |
| +second and a half and fails any stream that stops in any one of them, |
| +which the two-early-returns check it replaced did not — counters |
| +reading 0, 1, 2, 3 and then frozen for ever satisfied that one. What is |
| +still not established is *why* a fresh `MediaStream` unblocks the |
| +element: it is a browser workaround with a measured effect, not a |
| +diagnosis, and it does not show that the decoder pipeline is rebuilt. |
| +The identity, decode and render reproduction is kept in the slice |
| +record for an upstream report; until Chromium is understood, this |
| +section claims a bounded, timed recovery and no more. |
| **If a deployment reports a face freezing after two people share at |
| once**, the order to check is: `POLITE_YIELD` against the pair's actual |