oficina / meet Public

Clone
git clone https://amadan.net/oficina/meet

Plain git — no account needed to clone.

Download

Download this file

1# Meet slice 3 — the record
2
3Branch `slice/3-relay`, `0690a7a..67f2292` (started on main `2215f26`).
4Spec: `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
7Slice 3 built the platform relay: `CARLOS_RELAY` parsing and minting
8against the platform's own vector, `GET /m/{sqid}/ice`, and the
9browser client's renewal before the credential's deadline — six tasks,
10each 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
13does not survive the worktree; this file is its substance.
14
15---
16
17## 1. Rulings and decisions
18
19From 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
70The rest of each task's own decisions — the suffix validation regexes,
71the JSON golden-shape pins, `atomic.Value`'s nil-but-typed slice, the
72`--host-resolver-rules` Chromium flag that keeps the platform's relay
73hostname off the network entirely — are in the task reports themselves
74and are not repeated here; §2 gives each task's disposition, not its
75full 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
244Enabling 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
246restarts the instance**, which drops every live call exactly as an
247ordinary deploy does (spec §15.11 — the hub is per process and dies
248with it). So: **enable the relay in the same deploy window as the edge
249deploy**, not as a separate step days later that catches a room full of
250people by surprise a second time.
251
252Verification is two commands, not one: `carlos relay status --app
253meet` confirms the platform's own side, and the instance's own startup
254line — `meet: relay configured regions=N` — confirms the app actually
255parsed what the platform handed it, at the region count expected. Both
256matter; the platform reporting a relay attached and the app logging
257zero regions would be the config having reached the process malformed,
258caught only outside development (§15.13's startup refusal) or silently
259accepted as no relay if the value were simply empty.
260
261**No test on this branch touches a real relay.** Every gate — `relay`'s
262own vector test, the config tests, `/ice`'s tests, and the three
263browser scenarios — proves the contract against fixtures and a fake
264`/ice`. The platform relay itself is verified only by the two commands
265above, 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
278slice 2's record already gives. The residual flake in §3 above is why a
279red run here is worth a second run before it is worth debugging, exactly
280as 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,
285five P2 and two P3. The whole-branch reviewer read the same tree
286concurrently and numbered its own findings W3-1..W3-12, of which
287W3-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
301Commit: `6b316cc..c3e2318`.
302
303## Codex round 2
304
305`codex exec` read the branch again at `d62e93b`. S3-3, S3-4 and S3-7
306closed; the other four were half-closed, each for a reason that had
307survived under the fix rather than because the fix was wrong. Five new
308findings, 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
318Two test claims were tightened rather than added: the three-restarts
319test passes an old deadline that the current code ignores, so restoring
320the removed optional-deadline branch fails it; and the Retry test gains
321a connection whose own restart is already running, with the extra
322assertion that it *restarted again* rather than merely acquiring the
323new stamp — without which the S3-9 bug satisfied it.
324
325One behaviour changed that no finding asked for, and it is worth
326naming: `renewals[peer].attempts` accumulates across runs now instead of
327starting again at 1 with each one. A follow-up renewal reset it to 1
328within milliseconds of the first verdict, which made `icestuck`'s
329"it spent the whole budget" assertion race, and a count that starts
330again says nothing a gate can wait on.
331
332Commit: `d62e93b..15b2f38`.
333
334## Codex round 3
335
336Read at `9bb0994`. S3-8, S3-10, S3-11 and S3-12 closed; S3-9 half-closed
337through 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
345that records what the run spent and then says it on the tile, so the
346two cannot drift.
347
348Commit: `9bb0994..8448bee`.
349