| 1 | # Meet slice 3 — the record |
| 2 | |
| 3 | Branch `slice/3-relay`, `0690a7a..67f2292` (started on main `2215f26`). |
| 4 | Spec: `docs/superpowers/specs/2026-09-07-meet-design.md`, revision 9 |
| 5 | (§15.13 and §15.14 carry this slice's two wire/behaviour rulings). |
| 6 | |
| 7 | Slice 3 built the platform relay: `CARLOS_RELAY` parsing and minting |
| 8 | against the platform's own vector, `GET /m/{sqid}/ice`, and the |
| 9 | browser client's renewal before the credential's deadline — six tasks, |
| 10 | each with its own review and fix rounds, kept in |
| 11 | `.superpowers/sdd/2026-09-10-meet-slice-3-relay/progress.md` and the |
| 12 | `task-N-report.md` files. Like slice 2's, that ledger is gitignored and |
| 13 | does not survive the worktree; this file is its substance. |
| 14 | |
| 15 | --- |
| 16 | |
| 17 | ## 1. Rulings and decisions |
| 18 | |
| 19 | From the ledger's pre-flight scan and the tasks that followed it: |
| 20 | |
| 21 | - **`/ice` carries the peer as `?peer=`**, the way the stream's own |
| 22 | resume already does, not a body. **Ruling.** A native client needs |
| 23 | no body on a GET, and `/ice` has nothing else a body would carry. |
| 24 | Cost if wrong: none observable. Written into spec §15.13. |
| 25 | - **A member's `/ice` re-derives standing and, when it is gone, ends |
| 26 | the participation** — mutating, exactly like `/signal`'s own |
| 27 | §15.12 correction — while a guest never consults standing at all |
| 28 | (ruling 14). Cost if wrong: one extra standing read per `/ice` call, |
| 29 | which is rare — once per two and a half hours per call, on the |
| 30 | client's own schedule. Written into spec §15.13. |
| 31 | - **The client's fetch-failure retry stays at one minute.** The gate |
| 32 | proves the warning appears and the call stays connected on a failed |
| 33 | fetch; it does not prove the one-minute recovery, which would cost |
| 34 | the gate roughly seventy seconds per scenario for no more assurance. |
| 35 | - **The relay is enabled on the app after the edge deploy**, verified |
| 36 | by `carlos relay status` and the instance's own startup line; no test |
| 37 | in this branch touches a real relay (§13, and §4 below). |
| 38 | - **The renewal schedule is the earlier of a ratio estimate and a |
| 39 | clock estimate**, never the server's `renewAt` read as a bare |
| 40 | instant: `(expiresAt − renewAt) × 5` survives clock skew because it |
| 41 | is built from two of the server's own instants, while |
| 42 | `renewAt − fetchedAt` catches the truncation the ratio reading can |
| 43 | mishandle at short lives. Found during Task 4's fix round 2, after |
| 44 | round 1's schedule amplified the server's own second-truncation at |
| 45 | short test lives; written into spec §15.14. |
| 46 | - **`memberStandingGone` returns `(bool, error)`**, not the brief's bare |
| 47 | `error` — `hub.Authorize` mirrors `Relay`'s own |
| 48 | `gone, err := …; if err != nil { return err }; if gone { return |
| 49 | ErrNotYours }` shape. An implementation decision the spec has no |
| 50 | opinion about (Task 3). |
| 51 | - **The development header's TTL test uses 12s/10s, not the brief's 2s |
| 52 | example** — at two seconds, `life*5/6`'s fractional remainder can |
| 53 | round to the same whole second as `life` once both are truncated by |
| 54 | `.Unix()`, intermittently making `renewAt == expiresAt`. Any TTL that |
| 55 | is a multiple of six seconds keeps the margin clear of that rounding |
| 56 | (Task 3; the same fact governs the browser gate's own TTL choice, |
| 57 | §4 below). |
| 58 | - **`icerenewal` uses an 18-second TTL, not the brief's 12s** — twelve |
| 59 | leaves two seconds between the renewal starting and the deadline that |
| 60 | prints the expired sentence, which this scenario asserts stays away; |
| 61 | eighteen leaves three, and the run measures the margin directly so a |
| 62 | change to either end is caught (Task 5). `icefail` keeps 12s, where |
| 63 | the deadline arriving *is* the point. |
| 64 | - **Recovery from a failed `/ice` fetch is left unproved in the browser |
| 65 | gate**, exactly as the ledger's own ruling allows: `ICE_RETRY` is a |
| 66 | fixed minute by design, and watching it clear costs roughly seventy |
| 67 | seconds for no more assurance than the warning appearing already |
| 68 | gives. |
| 69 | |
| 70 | The rest of each task's own decisions — the suffix validation regexes, |
| 71 | the JSON golden-shape pins, `atomic.Value`'s nil-but-typed slice, the |
| 72 | `--host-resolver-rules` Chromium flag that keeps the platform's relay |
| 73 | hostname off the network entirely — are in the task reports themselves |
| 74 | and are not repeated here; §2 gives each task's disposition, not its |
| 75 | full diff. |
| 76 | |
| 77 | --- |
| 78 | |
| 79 | ## 2. Per-task review dispositions |
| 80 | |
| 81 | | Task | Built | Review | Fix rounds | Final | |
| 82 | |---|---|---|---|---| |
| 83 | | 1 — `internal/calls/relay` | `Parse`, `Region`, `Mint`, `Credential`, `ICEServer`; the platform vector test | Needs fixes: HIGH — `Parse` echoed the raw region field in its errors; MEDIUM — no redacting `String()` on `Region`; MEDIUM — the no-echo test didn't prove its own name; six LOW items (`[]`/`null` unification, an empty URL reaching `RTCIceServer`, an undocumented expiry precondition, unpinned no-mutation/JSON-shape/suffix-regex/base64-alphabet/TURN-case-insensitivity behaviour) | Round 1 `991352e` (combined with Task 2): all ten fixed. Re-review: three test-only pins remained, folded into Task 3's fix round. | Complete; pins verified in Task 3's fix round (`b5c29b3`, items 9–12). | |
| 84 | | 2 — `Config.Relay`, `resolve`, the atomic floor read | `app.relay atomic.Value`, `relayRegions()`/`relayConfigured()`, `TestHandle.SetRelay`, the room-page `Floor` value wired to it | Needs fixes (production code judged right): the Task-1-inherited echo at `config.go:146/148`; a test that printed the key on failure; the with-relay build path and its one INFO line untested; the floor sentence's "relay configured ⇒ absent" direction untested (a hardcoded `Floor` was passing `make ci`); the refusal test satisfied by `relay.Parse`'s text rather than `config.go`'s own phrase; a false doc comment on the since-deleted `RelayConfigured` field; `SetRelay` unproven before Task 3 became its first caller; malformed cases exercising only the outer error | Round 1 `62853ed` (combined with Task 1): all eight fixed, including deleting the dead field once `grep` confirmed nothing else read it. | Complete. | |
| 85 | | 3 — `GET /m/{sqid}/ice`, `hub.Authorize` | The wire shape, the §2.5 authority check, the development TTL header | Needs fixes: MEDIUM — `/ice` read the store before authority and was unlimited (no `LiveForSession` gate, no limiter); MEDIUM — the logging test passed even with a credential logged on success; MEDIUM — the dev header had no ceiling; several test gaps (ended-meeting refusal, `validPeerID`/`validSqid` load-bearing against a newline, byte-equality against a real `/signal` 404, a malformed sqid); five LOW items (the 500 arm's `path`, an overstated doc comment, log level, an unlogged meeting refusal, a duplicated test helper, an unpinned JSON entry); the guest half of the standing test not discriminating | Round 1 `b5c29b3`: all fixed except four items carried forward — see §3. Re-review: clean. | Complete. Carried: the sqid guard on `/ice` (unproved); the shared-bucket pin; the log-test comment; the family-wide `path` nit. `N4` (`Retry-After` on `/ice` 429) was folded into Task 4's fix round instead of carried. | |
| 86 | | 4 — `room.js`, the ICE list and renewal | `fetchIce`, `scheduleRenewal`, `renewAll`/`renewOne`, `proveRenewed`, the must-succeed-by watchdog, the `data-relay` floor sentence | Needs fixes: HIGH — the watchdog's exemption compared against the wrong stamp and was unreachable; HIGH — a stale selected pair could answer for a fresh one in Chromium; MEDIUM — manual Retry erased the note and did nothing on a failed fetch; MEDIUM — the warning showed even on the floor, with no relay to fail; MEDIUM — the schedule trusted the client's clock, not a duration; six LOW items | Round 1 `9fed86c`: nine of eleven items fixed outright, two (the 429 loop, the schedule) added or partly addressed. Re-review after round 1: two half-closed — MEDIUM A, the ratio/clock amplification of the server's second-truncation at short lives (a 2s life could silently never renew; a 10s life landed the deadline 2s late); LOW B, `failedSince` stamped once per outage rather than per attempt, letting a visibility catch-up jump the retry queue — plus three nits. | Round 2 `67f2292`: A and B fixed (earlier-of-ratio-and-clock scheduling per spec §15.14; `lastTryAt` stamped every attempt); the nits fixed; and, not asked for but needed, the stale-roster-after-await defect Task 5 traced (see below) and an `adoptServers` guard against restarting a negotiation already in flight. Still owed: a Go tripwire pinning `iceRenewAfter == iceLife*5/6` against the client's own ratio arithmetic drifting from it silently, and the residual gate flake's diagnostics — see §3. | |
| 87 | | 5 — the browser gate: `ice`, `icerenewal`, `icefail` | The three scenarios, `state().ice`/`state().renewals`, the credential-never-in-console check, the Chromium flag keeping the relay hostname off the network | No separate review pass: the task's own result was 23 passed, 3 skipped, with the skip's cause fully diagnosed and reproduced rather than papered over — `applyRoster` reconciled the roster it captured *before* awaiting `/ice`, so a `peer` event landing during that fetch was closed by the stale roster and its §4.6 counters dropped, discarding the answer that followed as an already-seen replay. The same defect, reached through a different door, explained slice 2's `staletarget` flake. | Fixed as part of Task 4's fix round 2 (`67f2292`), which also fixed a second door onto the same symptom (a superseded roster applied at all) and reordered `accept`'s generation check ahead of its cursor check. | Complete; the three scenarios un-skipped, `make browser` 26/26. | |
| 88 | | 6 — this record and spec §15.13/§15.14 | Documentation only | — | — | This commit. | |
| 89 | |
| 90 | --- |
| 91 | |
| 92 | ## 3. Carried items, and where they go |
| 93 | |
| 94 | - **The sqid guard on `/ice` is unproved.** Task 3's own peer-newline |
| 95 | test (`TestIceNeverLogsAMalformedPeerVerbatim`) has no sqid |
| 96 | equivalent: nothing proves an oversized or newline-bearing sqid never |
| 97 | reaches the limiter key or the log the way an oversized peer is |
| 98 | proven not to. **Destination: the next fix wave**, alongside whatever |
| 99 | review of the whole branch runs next — mirror the peer test with a |
| 100 | sqid-shaped input. |
| 101 | - **`/signal` and `/ice` sharing one rate-limiter bucket is |
| 102 | implemented but not pinned.** `TestIceRateLimitsOneSessionsBurst` |
| 103 | proves `/ice` alone is rate-limited; nothing spends the budget on one |
| 104 | endpoint and shows the other refused because of it. **Destination: |
| 105 | the next fix wave** — a test that exhausts the bucket through |
| 106 | `/signal` and then gets 429 from `/ice` in the same session and room, |
| 107 | or the reverse. |
| 108 | - **The log-test comment says authority where the arm is unbound.** |
| 109 | `ice.go`'s refusal logging distinguishes an `outcome=unbound` refusal |
| 110 | (no live participation at all, checked first, before the store or the |
| 111 | limiter) from an `outcome=authority` refusal (`hub.Authorize`'s own |
| 112 | four sentinel errors, checked last); a comment near the log-content |
| 113 | test names the wrong one. **Destination: the next fix wave** — a |
| 114 | one-line comment fix, found while writing this record and not worth |
| 115 | a separate commit on its own. |
| 116 | - **The family-wide `path` nit.** Every no-session-subject 500 across |
| 117 | the handler family (`auth.go`, `stream.go`, `signal.go`, `ice.go`, |
| 118 | `handlers_room.go`'s four arms) logs `r.URL.Path` on an arm that |
| 119 | should be unreachable if the framework's own session middleware is |
| 120 | doing its job — an unbounded, attacker-shaped value on a path that |
| 121 | exists only for a bug elsewhere. Not `/ice`-specific; carried under |
| 122 | its own name because fixing it in `/ice` alone would leave six other |
| 123 | call sites inconsistent. **Destination: a family-wide pass**, not |
| 124 | this slice's fix wave — touching all seven call sites at once is a |
| 125 | different-shaped commit from anything slice 3 built. |
| 126 | - **The Go tripwire `iceRenewAfter == iceLife*5/6`.** Task 4's fix |
| 127 | round 2 moved the client's own schedule to the earlier of a ratio and |
| 128 | a clock estimate (spec §15.14) specifically so the client's |
| 129 | arithmetic stays correct even if the server's numbers drift — but |
| 130 | nothing on the Go side pins that `ice.go`'s `iceRenewAfter` constant |
| 131 | is still five sixths of `iceLife`, which is the assumption the |
| 132 | ratio's own "renewal at five sixths of the life" shape depends on. |
| 133 | Named as owed in Task 4's own fix-round-2 report. **Done** in fix |
| 134 | round 3: `TestIceRenewAtIsFiveSixthsOfTheLife` in |
| 135 | `internal/callstest/ice_test.go` (not `internal/calls` — the constants |
| 136 | are unexported, so the tripwire reads the ratio off the wire, which |
| 137 | is the form the client actually depends on). Verified against a |
| 138 | deliberately moved constant. |
| 139 | - **A margin too small to schedule on. Decided, not carried.** A |
| 140 | development-only `X-Meet-Test-Ice-TTL` under about six seconds leaves |
| 141 | a renew-to-expiry margin below a second, and the client's own |
| 142 | one-second floor under the renewal delay would then turn that list |
| 143 | into a fetch roughly once a second for as long as the tab stayed open |
| 144 | — a client that can be made to hammer its own server, even if only a |
| 145 | developer can ask it to. The choice was between clamping the client |
| 146 | and recording a gate rule that no scenario may use such a TTL; the |
| 147 | Codex fix wave **clamped the client**, because a rule that lives only |
| 148 | in a document is a rule the next scenario breaks. `schedulable()` is |
| 149 | false below a one-second margin: nothing is timed, and |
| 150 | `state().ice.schedulable` says so rather than the client looking idle |
| 151 | for no reason. Node-tested (W3-12). |
| 152 | - **The residual gate flake, `connectionState: "new"`.** Roughly one |
| 153 | full `make browser` run in three loses one scenario — a different one |
| 154 | each time — always at the first pairing, always with the same |
| 155 | signature on both browsers: descriptions exchanged and `stable`, but |
| 156 | `connectionState: "new"` — no ICE transport ever left the new state, |
| 157 | meaning gathering and checking never started. This is not the |
| 158 | stale-roster defect (whose signature was `peers: []`); it matches the |
| 159 | Makefile's own documented environment symptom, hitting one scenario |
| 160 | at a time rather than every scenario in the run. Three things were |
| 161 | tried against it in Task 4's fix round 2 and none moved it: the |
| 162 | `accept`-ordering fix (kept regardless — it closes a real hole), a |
| 163 | negotiation nudge for a lost `negotiationneeded` event (reverted — no |
| 164 | evidence it was the cause), and re-running affected scenarios in |
| 165 | isolation, where all of them pass every time. |
| 166 | |
| 167 | The reviewer's reading of that signature, which the fix-round-3 |
| 168 | diagnostic was written to settle: `stable` with `connectionState: |
| 169 | "new"` does not mean "negotiated and could not gather" — it is also |
| 170 | what a connection that *never negotiated at all* looks like, because |
| 171 | a peer connection is born `stable` and `new`. The decisive value is |
| 172 | `localDescription`: null says nothing was ever offered, so nothing |
| 173 | was ever gathered, and the fault is upstream of ICE entirely. Fix |
| 174 | round 3 prints it. `diagnose()` in `call.mjs` now carries a |
| 175 | `pcState` array for every connection the page still holds — |
| 176 | `signaling`, `connection`, `ice`, `gathering`, `local`, `remote`, |
| 177 | `senders`, `transceivers` as `direction:kind`, and `tracks` as |
| 178 | `kind:readyState` — collected through the construct trap that scenario |
| 179 | already used for its own `getStats` walk, now installed on every |
| 180 | context. It changes no assertion. |
| 181 | |
| 182 | **What the fix-round-3 diagnostic then showed** (whole-branch review |
| 183 | W3-7, from a lost `playfallback` run and confirmed by a lost |
| 184 | `staletarget` one): negotiation is *not* where it stops. Both sides |
| 185 | report a local offer and a remote answer applied, `signalingState` |
| 186 | stable, two `sendrecv` transceivers, and live tracks on the senders — |
| 187 | and `iceConnectionState: "new"` all the same, with `iceGatheringState` |
| 188 | `complete` on one side and still `gathering` on the other. So the |
| 189 | fault is inside ICE, after a negotiation that finished properly, and |
| 190 | the reading in the paragraph above — "nothing was ever offered" — is |
| 191 | wrong for these runs. The Codex fix wave adds the next four numbers, |
| 192 | per peer, to `state()` and to `diagnose()`: candidates gathered |
| 193 | (`onicecandidate`), candidates posted (`send` of type `candidate`), |
| 194 | candidates received, candidates added (`addIceCandidate`), and the |
| 195 | pending buffer's length. Counts only — an address is not something |
| 196 | the client publishes. Between them they say which of the four ways |
| 197 | ICE can stall this is, in one losing run. |
| 198 | |
| 199 | Round 3 also closed the likeliest client-side cause the *earlier* |
| 200 | reading admitted: `createPC` registered the connection before attaching its |
| 201 | listeners (so a `negotiationneeded` fired by `addTrack` could be lost |
| 202 | outright, leaving exactly a `stable`/`new` connection that never |
| 203 | offers), and its recvonly fallback was all-or-nothing, so a browser |
| 204 | that came down `startLocal`'s ladder to audio alone offered no video |
| 205 | m-line and the peer's tile could never get a frame. Whether either was |
| 206 | the flake is unproved; both were real, and W3-7's reading now says |
| 207 | neither was it. |
| 208 | |
| 209 | **Settled, in the Codex fix wave itself.** The counts were read from |
| 210 | three losing runs — `ice`, `icerenewal` and `staletarget`, on separate |
| 211 | gate runs — and all three say the same thing: |
| 212 | |
| 213 | | side | gathered | delivered | received | added | pending | gatheringState | |
| 214 | | --- | --- | --- | --- | --- | --- | --- | |
| 215 | | A | 4 | 4 | 0 | 0 | 0 | complete | |
| 216 | | B | 0 | 0 | 4 | 4 | 0 | **gathering** | |
| 217 | |
| 218 | (`delivered` was `posted` when these were captured; Codex round 2 |
| 219 | moved the count to where the hub accepts a candidate rather than |
| 220 | where one is handed to a queue, and the readings are unchanged.) |
| 221 | |
| 222 | One browser gathers **no ICE candidate at all** and never leaves |
| 223 | `gathering`. The other gathers, posts every one it gathers, and the |
| 224 | first adds every one it receives. There is nothing for the client to |
| 225 | do differently: it sends what the browser gives it and applies what |
| 226 | arrives. The stall is inside one Chromium's own candidate gathering, |
| 227 | which is the Makefile's documented environment symptom — "Chromium |
| 228 | gathers zero ICE candidates and every call fails with frames stuck at |
| 229 | 0" — reaching one context at a time instead of all of them. |
| 230 | |
| 231 | It correlates with load: every losing run of this wave took 560-590 s |
| 232 | against a healthy 318-420 s, one hit `go test`'s own ten-minute |
| 233 | timeout, and the wave ran while a whole-branch reviewer was working |
| 234 | the same tree. **No further client change is owed.** What is owed, if |
| 235 | anyone wants it gone: run the gate where nothing else is running, and |
| 236 | give `assertPair` a failure message that names `gathered: 0` for what |
| 237 | it is, so the next person spends a minute on it rather than a |
| 238 | fix round. |
| 239 | |
| 240 | --- |
| 241 | |
| 242 | ## 4. Deploying this slice |
| 243 | |
| 244 | Enabling the relay is not free the way slice 2's ordinary deploys are. |
| 245 | **`carlos relay enable --app meet` re-renders the app's config and |
| 246 | restarts the instance**, which drops every live call exactly as an |
| 247 | ordinary deploy does (spec §15.11 — the hub is per process and dies |
| 248 | with it). So: **enable the relay in the same deploy window as the edge |
| 249 | deploy**, not as a separate step days later that catches a room full of |
| 250 | people by surprise a second time. |
| 251 | |
| 252 | Verification is two commands, not one: `carlos relay status --app |
| 253 | meet` confirms the platform's own side, and the instance's own startup |
| 254 | line — `meet: relay configured regions=N` — confirms the app actually |
| 255 | parsed what the platform handed it, at the region count expected. Both |
| 256 | matter; the platform reporting a relay attached and the app logging |
| 257 | zero regions would be the config having reached the process malformed, |
| 258 | caught only outside development (§15.13's startup refusal) or silently |
| 259 | accepted as no relay if the value were simply empty. |
| 260 | |
| 261 | **No test on this branch touches a real relay.** Every gate — `relay`'s |
| 262 | own vector test, the config tests, `/ice`'s tests, and the three |
| 263 | browser scenarios — proves the contract against fixtures and a fake |
| 264 | `/ice`. The platform relay itself is verified only by the two commands |
| 265 | above, after the deploy that enables it. |
| 266 | |
| 267 | --- |
| 268 | |
| 269 | ## 5. The gates, and how to run them |
| 270 | |
| 271 | | Gate | Command | Notes | |
| 272 | |---|---|---| |
| 273 | | The one gate | `CGO_ENABLED=0 make ci` | vet, gofmt, tests, migration check — unchanged from slice 2. | |
| 274 | | Race | `CGO_ENABLED=1 go test -race ./internal/...` | Run repeatedly through this slice's fix rounds; green throughout. | |
| 275 | | Browser | `make browser` | 26 scenarios, three of them (`ice`, `icerenewal`, `icefail`) new this slice. **Needs the sandbox off** — inside the Claude Code bash sandbox Chromium gathers zero ICE candidates and every call fails, as slice 2's record already noted. **A development TTL header must be a multiple of six seconds** — `X-Meet-Test-Ice-TTL`'s life is split `renewAt = life*5/6`, `.Unix()`-truncated on both ends, and only a multiple of six keeps the margin clear of that truncation (a twelve-second life reads exactly 10 and 12; a ten-second life reads 8 and 10, where the same arithmetic renews at the very moment the credential dies). **The run takes about 5.5 minutes** — 317.95 s measured over ten full runs in Task 4's fix round 2, five of the ten and the last two clean at that mark, against 287 s in Task 5's own run with the three ICE scenarios skipped. | |
| 276 | |
| 277 | `make browser` is still deliberately outside `make ci`, for the reason |
| 278 | slice 2's record already gives. The residual flake in §3 above is why a |
| 279 | red run here is worth a second run before it is worth debugging, exactly |
| 280 | as slice 2's `rosterpair` flake was. |
| 281 | |
| 282 | ## Codex round 1 |
| 283 | |
| 284 | `codex exec` read the branch at `6b316cc` and asked for changes: no P1, |
| 285 | five P2 and two P3. The whole-branch reviewer read the same tree |
| 286 | concurrently and numbered its own findings W3-1..W3-12, of which |
| 287 | W3-1..W3-6 are the same defects. One commit answered all of them. |
| 288 | |
| 289 | | Finding | What it was | Disposition | Test that would have caught it | |
| 290 | | --- | --- | --- | --- | |
| 291 | | S3-1 / W3-1 (P2) | `expireUnrenewed` exempted any connection with `renewed=true`, and nothing ever cleared that flag: one successful renewal exempted a connection from every later deadline, so a second credential could expire in silence | Fixed. The watchdog reads `entry.listAt` — the stamp of the list a connection is actually on, which moves only on proof — and exempts only a connection on a list newer than the expiring one | `a renewal in an earlier cycle does not exempt a connection from a later one` | |
| 292 | | S3-2 / W3-2 (P2) | `retryRenewal` fetched for the whole call and renewed only its own tile, while resetting the shared timers: every other connection stayed on a credential about to die | Fixed. `renewOutdated(expiring)` hands a list that has just arrived to every connection still on an older one, and both the scheduled renewal and a tile's Retry go through it | `a Retry on one tile renews every connection the fetch re-credentialed` | |
| 293 | | S3-3 / W3-3 (P2) | With no baseline fragment — including after a rejected `getStats` — any succeeded selected pair counted as proof, and `''` passed a `!== null` check; a connection could be stamped onto a credential it had never gathered on | Fixed. No baseline is no proof, and `''` is not evidence. The poller takes its own baseline from the first reading after the restart and then requires a different one; if none can be read the restart stays unproven | `nothing recorded before the restart is not proof of anything`, `a connection that never moves is unproven, whatever it started with` | |
| 294 | | S3-4 / W3-4 (P2) | A successful fetch followed by one failed restart near the old deadline reported "expired" after one attempt of three, with a replacement credential in hand | Fixed. `renewOne` no longer takes a deadline: with a fresh credential it spends all three restarts and its verdict is always `stuck`. `expired` belongs to the watchdog, the only place that knows the other case | `a fresh credential buys three restarts and the verdict is stuck, never expired`, and the `icestuck` scenario | |
| 295 | | S3-5 / W3-5, W3-6 (P2) | The gate asserted the client's cached list length, not that any connection received it: `ice` would pass with `iceServers: []` in every constructor, and `icerenewal` with `setConfiguration` deleted | Fixed. The connection trap records every configuration installed — count, credentialed count, and an FNV-1a digest, never a credential — and the two scenarios assert that a real connection got the two-entry list and that the renewal installed a *different* one. The §13 successful-fetch/failed-restart ending gets its own scenario, `icestuck` | `ice`, `icerenewal`, `icestuck` | |
| 296 | | S3-6 (P3) | The startup-logging test matched substrings, so duplicate lines, a WARN, or an extra decoded attribute would all have passed | Fixed. A recording `slog.Handler` keeps whole records; the test asserts exactly one INFO record with exactly one attribute, `regions=1` | `TestBuildLogsOneINFOLineWithTheRegionCountAndNotTheKey`, verified against a WARN and against an extra attribute | |
| 297 | | S3-7 (P3) | The burst test exercised `/ice` alone, so a limiter of its own would have passed it; the malformed-room case passed with `validSqid` removed, because the liveness gate refuses that room a step later | Fixed. One test exhausts the budget entirely through `POST /signal` and then asks `/ice`; another sends an oversized room id and asserts the log names it `invalid` and that no bucket was minted | `TestIceSpendsTheSignalEndpointsOwnBucket`, `TestIceNeverLogsAMalformedRoomOrMintsABucketForOne`, both verified against the guards removed | |
| 298 | | W3-7 | The residual flake's `pcState` showed negotiation complete and ICE at `new`, so the fault is in ICE | Instrumented, then **settled** — see the flake entry above. Per-peer candidate counts (gathered, posted, received, added, pending) in `state()` and `diagnose()`; three losing runs then showed one browser gathering zero candidates | no assertion changed | |
| 299 | | W3-12 | A sub-six-second development TTL leaves a margin under a second, which the delay floor would turn into a 1 Hz fetch loop | Fixed, not carried: `schedulable()` declines below a one-second margin and `state().ice.schedulable` says so | `a margin under a second is not a schedule` | |
| 300 | |
| 301 | Commit: `6b316cc..c3e2318`. |
| 302 | |
| 303 | ## Codex round 2 |
| 304 | |
| 305 | `codex exec` read the branch again at `d62e93b`. S3-3, S3-4 and S3-7 |
| 306 | closed; the other four were half-closed, each for a reason that had |
| 307 | survived under the fix rather than because the fix was wrong. Five new |
| 308 | findings, one commit. |
| 309 | |
| 310 | | Finding | What it was | Disposition | Test | |
| 311 | | --- | --- | --- | --- | |
| 312 | | S3-8 (P2) | S3-1's other half: the watchdog compared generations correctly and then called `markTrouble`, which returned early on `record.renewed`. A connection that renewed once was never told its *current* credential had expired — and the round-1 test passed a spy for the marking function, so it proved nothing about the path that swallowed it | Fixed. `markTrouble(id, kind, upTo)` compares `entry.listAt` with the list the verdict is about; the boolean is gone from the decision entirely. The watchdog test now runs the real marking functions and reads the sentence `tileNote` would render | `a renewal in an earlier cycle does not exempt a connection from a later one`, `a connection already proven onto a newer list is left alone by the deadline` | |
| 313 | | S3-9 (P2) | S3-2's other half: a restart installs list B, another tile's Retry fetches C, `renewOutdated` skips the running restart — and B's proof then stamped the connection with C, a credential it had never been given, whose schedule then governed it | Fixed. Each attempt captures the list it installs and stamps only that generation on proof; when a newer list arrived during the attempt, `followUp` renews onto it once the attempt settles. The follow-up is bounded by the list that run had to work with, so a connection that simply cannot renew does not restart itself for ever | `a proof stamps the list it installed, not one that arrived while it was proving`, `a Retry reaches a connection whose own restart was already running` | |
| 314 | | S3-10 (P3) | The trap recorded `setConfiguration` before calling it, so a throw counted as an installation, and the counts were global, so one connection's second update could stand in for another left on the empty list | Fixed. Recorded after the real call returns, keyed by the connection's index; the assertions fold the log to each *live* connection's last configuration | `ice`, `icerenewal` | |
| 315 | | S3-11 (P3) | The recording handler put attributes in a map (collapsing duplicate keys) and gave every derived handler its own store, so a logger reached through `WithAttrs` wrote where nobody read and its bound attributes were invisible | Fixed. One shared store, bound attributes and groups replayed onto each record, and the assertion reads an ordered slice with a typed value | `TestBuildLogsOneINFOLineWithTheRegionCountAndNotTheKey`, plus `TestTheRecordingHandlerSeesWhatALoggerCarries` — a test of the test | |
| 316 | | S3-12 (P3) | `posted` counted candidates before the stale-target return and before the queue, so it counted ones that never reached the wire | Fixed. Renamed `delivered` and counted where the hub answers 200 | the flake captures above, unchanged in shape | |
| 317 | |
| 318 | Two test claims were tightened rather than added: the three-restarts |
| 319 | test passes an old deadline that the current code ignores, so restoring |
| 320 | the removed optional-deadline branch fails it; and the Retry test gains |
| 321 | a connection whose own restart is already running, with the extra |
| 322 | assertion that it *restarted again* rather than merely acquiring the |
| 323 | new stamp — without which the S3-9 bug satisfied it. |
| 324 | |
| 325 | One behaviour changed that no finding asked for, and it is worth |
| 326 | naming: `renewals[peer].attempts` accumulates across runs now instead of |
| 327 | starting again at 1 with each one. A follow-up renewal reset it to 1 |
| 328 | within milliseconds of the first verdict, which made `icestuck`'s |
| 329 | "it spent the whole budget" assertion race, and a count that starts |
| 330 | again says nothing a gate can wait on. |
| 331 | |
| 332 | Commit: `d62e93b..15b2f38`. |
| 333 | |
| 334 | ## Codex round 3 |
| 335 | |
| 336 | Read at `9bb0994`. S3-8, S3-10, S3-11 and S3-12 closed; S3-9 half-closed |
| 337 | through one remaining door, S3-13. One P3 on a test claim. One commit. |
| 338 | |
| 339 | | Finding | What it was | Disposition | Test | |
| 340 | | --- | --- | --- | --- | |
| 341 | | S3-13 (P2) | The last door onto S3-9: `installedAt` was recorded before `setConfiguration`, and a throw from it was swallowed. The restart that followed re-gathered on the *previous* servers, and a new fragment then read as proof and stamped the connection onto a list it had never taken — `listAt` moved, `renewed` went true, no trouble was shown | Fixed. The stamp moves only after `setConfiguration` returns. A refused configuration restarts nothing, proves nothing and stamps nothing: it asks for a fresh generation, whose rebuilt connection takes `ice.servers` at construction and keeps §4.6's counters, and the run ends `stuck` | `a configuration the browser refuses is not a credential the connection has` | |
| 342 | | S3-14 (P3) | `icestuck` asserted `attempts >= 3` against a lifetime counter, which cannot say how many restarts *this* verdict followed — a later run raises it while the first verdict is still on the screen | Fixed. `renewals[peer].lastRun = {attempts, verdict}` is captured with the verdict itself; the scenario asserts that run spent three and ended `stuck`, and keeps the cumulative count as a second, weaker line | `a run records the budget it spent, alongside the lifetime count`, `a proven run says so, and says what it cost`, and `icestuck` | |
| 343 | |
| 344 | `markRenewalStuck` is gone: every verdict now goes through one function |
| 345 | that records what the run spent and then says it on the tile, so the |
| 346 | two cannot drift. |
| 347 | |
| 348 | Commit: `9bb0994..8448bee`. |
| 349 | |