oficina / meet Public

spec: codex round 3 findings and Meet revision 4

Round 3: six R2 findings closed, five half, one worse, ten new (three
P1), request changes. Revision 4: resume output branched on
admission; pending_joins bound to the originating link and written for
members too, consumed atomically with an link re-check; 2.5, one
authorization row per endpoint, member kind re-derived per request,
admit and promotion check the target's entry authority under the lock;
the member-reserve claim withdrawn and the edge-wide sign-in denial of
service stated; replay ring with last per sender, gap and pair reset;
renewal completion proven by the restart generation's ufrag on a
succeeded selected pair; replaced terminal for the displaced tab; 7.1
and the 429 claim corrected.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Paul Campbell pushed by paul@keymail.dev 79787b82e2ee675fabd2e144af0e03b06ff0cc97 parent 1bd7161
2 files changed, +338 −121
  • docs/superpowers/specs/2026-09-07-meet-design.md +242 −121
  • docs/superpowers/specs/CODEX-FINDINGS-MEET-R3.md +96 −0
diff --git a/docs/superpowers/specs/2026-09-07-meet-design.md b/docs/superpowers/specs/2026-09-07-meet-design.md
index 8a093d4..160f45d 100644
--- a/docs/superpowers/specs/2026-09-07-meet-design.md
+++ b/docs/superpowers/specs/2026-09-07-meet-design.md
@@ -1,9 +1,8 @@
# 🤖 Meet — design
Design, 2026-09-07. Direction approved by Paul in conversation; this is the
-written form. **Revision 3**, answering the twelve findings of the
-second Codex review (`CODEX-FINDINGS-MEET-R2.md`) on top of revision 2's
-answers to the first (`CODEX-FINDINGS-MEET.md`); §14 has both tables. Drafted by the Meet
+written form. **Revision 4**, answering the ten findings of the third
+Codex review (`CODEX-FINDINGS-MEET-R3.md`); §14 has all three rounds. Drafted by the Meet
session (Fable 5.1) from a brainstorm with Paul on 2026-09-06 and
2026-09-07. Every ruling in §1 attributed to Paul was given in that
conversation or on the discussion it cites.
@@ -60,7 +59,10 @@ text, any SDP fragment, any candidate address and any link token. A
link token appears once in an HTTP response body and nowhere else.
What the claim does not cover, said plainly: signalling passes through
-Meet's process in plaintext for the instant it is relayed, and a
+Meet's process in plaintext for the instant it is relayed, plus a
+replay ring of at most thirty-two targeted messages per participation
+held in memory for at most thirty seconds after its stream drops
+(§4.5), and a
capability URL is a bearer that the person who holds it may paste
anywhere. Ruling 16 covers the token at rest, not in flight.
@@ -158,19 +160,21 @@ token, ruling 16), `created_by`, `created_at`, `revoked_at`. One meeting
may hold several links so a revoked one can be replaced. The token is
returned once, in the response that minted it, and never again.
-**`pending_guests`** — `id`, `meeting_id`, `address_fingerprint` (keyed
+**`pending_joins`** — `id`, `meeting_id`, `link_id` (the
+`meeting_links` row the browser followed), `address_fingerprint` (keyed
HMAC of the address under the sealer's purpose key, so the table never
holds a plaintext outsider address, calendar's rule for attendees),
`created_at`, `expires_at` (fifteen minutes). Written at `POST /signin`
-when an outsider asks for a magic link through a live meeting link
-(§3.2), read by the sign-in authorize callback and by `/continue`, and
-deleted when a grant is minted from it or when it expires. A restart
-does not lose it, so a magic link clicked after a deploy still works.
+when anyone, member or outsider, asks for a magic link while holding a
+live meeting link (§3.2); read by the sign-in authorize callback (for
+outsiders) and by `/continue` (for everyone); deleted when consumed or
+expired. A restart does not lose it, so a magic link clicked after a
+deploy still works.
**`guest_grants`** — `id`, `meeting_id`, `person_id`, `granted_at`,
`expires_at`, `admitted_at` (nullable), `admitted_by` (actor,
nullable). Minted at sign-in continuation when a non-member follows a
-live link (§3.2), or from a `pending_guests` row at `/continue`. One row
+live link (§3.2), or from a `pending_joins` row at `/continue`. One row
per person per meeting; following the link twice touches nothing. `admitted_at` and `admitted_by` are an audit of
the first admission and confer nothing: whether a guest is admitted
*now* is hub state (§4.2).
@@ -205,9 +209,8 @@ computed, never stored:
`expires_at` in the future. The row is the instance's own authority
under doctrine §5.2.
-Standing is what `/m/{sqid}/join`, a fresh `GET /m/{sqid}/stream` with
-no `peer`, and every `/api/` request check. It is computed on each
-request and never cached.
+Standing is the entry authority. §2.5 says which endpoints check it,
+and it is computed on each request and never cached.
### 2.4 Participation: who may continue
@@ -249,6 +252,32 @@ adds no cache of its own.
---
+### 2.5 The authorization contract, per endpoint
+
+One rule per endpoint, so §4 and §5 do not each restate it:
+
+| Endpoint | Authority | Checked |
+|---|---|---|
+| `GET /` , `POST /meetings`, confirm pages and their POSTs, `/m/{sqid}/remove/{peer}` | **Current member standing** | On the request. |
+| `GET /m/{sqid}/join`, `GET /m/{sqid}` | Standing (`member` or `guest`) **or** a live participation for this session in this room | On the request. |
+| `GET /m/{sqid}/stream` with no `peer` | Standing | On the request. Creates an unjoined participation. |
+| `GET /m/{sqid}/stream?peer=` | Participation bound to this session | On the request. |
+| `POST /m/{sqid}/signal`, `join` | Participation, **and** standing at that moment, because `join` is the entry | On the request, under the hub lock. |
+| `POST /m/{sqid}/signal`, `admit` | Participation with **current** `member` kind, re-derived from standing on this request, not only on the tick; **and** the target's entry authority (`member` standing or an unexpired grant) at this moment, under the same lock as the transition | On the request. |
+| `POST /m/{sqid}/signal`, every other type; `GET /m/{sqid}/ice` | Participation, admitted | On the request. Standing is not consulted; a guest's expired grant does not matter here (ruling 14). |
+| `/api/v1/*` | Agent token unrevoked **and** operator's current member standing | On the request. |
+
+A request by a participation whose kind is `member` re-derives standing
+on every request as well as on the tick; a member who has just been
+removed is dropped by the next thing they do, not the next tick. The
+tick (§2.4) exists for the quiet case. Promotion of a waiting guest on
+the tick (§2.4) goes through the same admission check as `admit`,
+including the target's entry authority, so a guest whose grant has
+expired is never admitted by either path; only an already-admitted
+guest survives expiry.
+
+---
+
## 3. Joining
### 3.1 Making a meeting and its link
@@ -292,30 +321,36 @@ gives one 404 for all four. Live gives one of three outcomes:
/signin` on the allowlist before rastrillo's `Begin`. Meet keeps that
gate with one exception: an address not on the allowlist is let through
when, and only when, the request carries a valid pending-join cookie for
-a live link. In that case Meet first writes a `pending_guests` row
-(meeting, keyed fingerprint of the address, fifteen minutes) and only
-then calls `Begin`. Nobody can cause mail to an arbitrary address
-without holding a live meeting link, and holding the link is the team's
-own choice.
+a live link. Whenever the cookie is present and valid, for a member
+address or an outsider's, Meet first writes a `pending_joins` row
+(the **link** the cookie names, its meeting, the keyed fingerprint of
+the address, fifteen minutes) and only then calls `Begin`. Nobody can
+cause mail to an arbitrary address without holding a live meeting
+link, and holding the link is the team's own choice.
**Admitting the emailed link.** rastrillo's `Authorize` callback
receives the verified address and nothing else, and Docs wires it to
the allowlist, which would 403 the outsider's click. Meet's `Authorize`
is: the address is on the allowlist, **or** its keyed fingerprint has an
-unexpired `pending_guests` row. This works in whichever browser the
-emailed link is opened, because the row was written before the mail
-went out and lives in the database, not in a cookie.
+unexpired `pending_joins` row whose link is still live. This works in
+whichever browser the emailed link is opened, because the row was
+written before the mail went out and lives in the database, not in a
+cookie.
**Continuation.** rastrillo has one configured landing path for a fresh
session and no per-attempt return value, so Meet's `SignedInPath` is
`/continue`. `GET /continue` resolves the signed-in person, then:
-1. If a `pending_guests` row matches their fingerprint and is unexpired:
- look the meeting up as though they had just followed a live link (so
- a link revoked during sign-in still 404s), mint the grant if they are
- a non-member, delete the row, redirect to the holding screen. The
- pending-join cookie, if present, is cleared; it is not consulted,
- because the row is the record.
+1. If one or more `pending_joins` rows match their fingerprint and are
+ unexpired, take the **most recent**. In one transaction: re-check
+ that its link is not revoked, its meeting not ended, and the row not
+ expired, exactly as `GET /j/{token}` would (so a link replaced during
+ sign-in still 404s even though the meeting has a new live link);
+ mint the grant if they are a non-member and hold none; delete every
+ pending row for this fingerprint; redirect to the holding screen. If
+ the re-check fails, delete the rows and fall through to 2 or 3. The
+ pending-join cookie, if present, is cleared and never consulted; the
+ row is the record.
2. Otherwise, if they are a member: redirect to `/`, the meeting list.
A member who opened `/signin` directly lands where they expect.
3. Otherwise: redirect to `/welcome`, the not-a-member page: "You're
@@ -323,36 +358,46 @@ session and no per-attempt return value, so Meet's `SignedInPath` is
If someone sent you a meeting link, open it again now." with sign-out
and nothing else. Scriptless.
-The other-browser case (magic link opened on a phone) is therefore not
-a fallback: step 1 finds the row regardless of cookies. Re-following the
-link also works, because a signed-in non-member takes the second bullet
-at the top of this section.
-
-**Mail budget.** The inherited limiter allows twenty attempts per client
-address and five per email address per fifteen minutes, keyed on
-`RemoteAddr`, which behind the platform edge is the edge, so that
-budget is shared by every visitor. Meet cannot separate guest and
-member traffic inside that limiter, so it separates it in front: Meet's
-gate counts outsider attempts, refused or not, **before** `Begin` is
-reached, at five per link per fifteen minutes and ten across all links
-per fifteen minutes. Outsider traffic can therefore consume at most ten
-of the shared twenty, leaving ten attempts per window for members. A
-team whose members need more than ten sign-ins in fifteen minutes while
-a link is under attack will see rate errors; that is the stated
-residue, and a trusted client address from the edge is the platform
-change that would remove it, filed as a question on the platform when
-slice 1 opens, not solved here.
+A member who followed a link while signed out therefore reaches the
+holding screen of that meeting, in whichever browser the emailed link
+is opened, by the same row an outsider uses. The rows differ only in
+what they authorize: for an outsider the row also admits the address at
+`Authorize`; for a member the allowlist does that and the row only
+carries the destination.
+
+**Mail budget, and what it does not buy.** The inherited limiter allows
+twenty attempts per client address and five per email address per
+fifteen minutes, keyed on `RemoteAddr`, which behind the platform edge
+is the edge, so that budget is shared by every visitor. Meet bounds
+what it can bound: attempts for a **non-allowlisted** address are
+counted before `Begin` is reached, at five per link per fifteen minutes
+and ten across all links per fifteen minutes. That stops a leaked link
+from being used to mail strangers at volume.
+
+It does not protect members, and revision 3's claim that it did was
+wrong: anyone can submit a **member's** address without any link,
+reach `Begin`, and consume the shared client-address allowance twenty
+times in fifteen minutes, or a single member's five-per-address
+allowance, after which that member cannot sign in for the rest of the
+window. The gate classifies an address, not the person typing it, and
+nothing in front of `Begin` can tell them apart. This is the residual
+denial of service for sign-in on every app that shares this edge and
+this limiter, not a Meet-specific one, and Meet records it rather than
+claiming a reserve. The fix is a trusted client address from the edge
+or a per-app limiter keyed on something better, and a question to that
+effect is filed on the platform when slice 1 opens.
**Responses.** Meet's own refusals (allowlist without a link, link
budget) redirect to `/signin?sent=1`, the same destination as a
successful `Begin`. rastrillo's own outcomes are left as they are:
-`?err=address` for a malformed address and `?err=rate` for its limiter,
-both of which describe the requester's own input or behaviour. The
-residue, stated plainly: while the shared budget is exhausted, a member
-sees `?err=rate` where a non-member without a link would have seen
-`?sent=1`, which distinguishes them. That window exists only under the
-attack the budget above bounds, and it is recorded here rather than
-claimed away.
+`?err=address` for a malformed address and `?err=rate` for its limiter.
+The residue, stated plainly: while a budget is exhausted, a member
+address gets `?err=rate` where an unknown address without a link gets
+`?sent=1`, which distinguishes the two; and because a single member
+address can be rate-limited on purpose, that distinction is reachable
+by an attacker at will, not only under global exhaustion. Membership of
+a specific address is therefore not a secret Meet can keep at the
+sign-in form, and the README says so.
### 3.3 The holding screen
@@ -406,8 +451,8 @@ message.
### 4.1 Transport
-One SSE stream per peer, `GET /m/{sqid}/stream`, under the session and
-the standing check, with the Docs live-sync heartbeat (15 s) and
+One SSE stream per peer, `GET /m/{sqid}/stream`, authorized per §2.5,
+with the Docs live-sync heartbeat (15 s) and
connection TTL (5 min) so hibernation cannot be held off by a dead
stream. The first event on a new stream is `hello {peer}`, the server's
peer id for this stream. Upstream messages are `POST /m/{sqid}/signal`
@@ -433,14 +478,17 @@ not yet joined. A room holds at most **sixteen participations** of any
state (unjoined, waiting, admitted, or held for resume); a stream that
would be the seventeenth is refused with 503 before anything is
allocated. An unjoined participation that has not posted `join` within
-thirty seconds is dropped with its stream. It is a 16-byte random value, shown in
-rosters so browsers can address each other, and it is not a credential:
+thirty seconds is dropped with its stream.
+
+The peer id is a 16-byte random value, shown in rosters so browsers can
+address each other, and it is not a credential:
every `POST /signal` must carry a `peer` that the hub finds **in this
room, bound to this session**; anything else is refused with 404 and
-nothing is relayed. A session may hold one peer per room; a second
-stream from the same session for the same room resumes the existing
-peer if it is within `resume_until` (§4.5) and otherwise replaces it,
-closing the old stream with `lost {reason: "replaced"}`.
+nothing is relayed. A session may hold one peer per room; a stream
+opened with `?peer=` resumes that participation (§4.5); a stream opened
+without `peer` while the session already has a participation in the
+room replaces it, closing the old stream with `lost {reason:
+"replaced"}`, which is terminal for the old tab (§4.5).
Every downstream envelope is built by the server: `type`, `from` (the
sender's peer id, from the binding, never from the body), the sender's
@@ -456,8 +504,8 @@ a multi-host fleet; that is noted, not solved.
| Type | Who may send | Recipients | Server does |
|---|---|---|---|
-| `join` | any peer with standing | — | Member: admit (subject to cap), `roster` to the joiner, `peer` to admitted peers. Guest: mark waiting, `knock` to admitted **members**. Idempotent for a peer already present. |
-| `admit {peer}` | an admitted member | — | Target must be waiting in this room. Under the hub lock: if admitted count is four, `full` to the admitting member and nothing changes; else mark admitted, `roster` to the guest, `peer` to the other admitted peers, `knock-gone` to members. |
+| `join` | a participation whose person has standing now (§2.5) | — | Member: admit (subject to cap), `roster` to the joiner, `peer` to admitted peers. Guest: mark waiting, `knock` to admitted **members**. Idempotent for a peer already present. |
+| `admit {peer}` | an admitted participation with current `member` kind (§2.5) | — | Target must be waiting in this room and hold entry authority now (§2.5). Under the hub lock: if the target's grant has expired, `lost {reason: "expired"}` to it and `knock-gone` to members; if admitted count is four, `full` to the admitting member and nothing changes; else mark admitted, `roster` to the guest, `peer` to the other admitted peers, `knock-gone` to members. |
| `leave` | any peer | — | Remove the peer. Admitted: `gone` to admitted peers. Waiting: `knock-gone` to members. |
| `offer`, `answer`, `candidate` | an admitted peer | the one admitted peer named in `to` | Relay the payload unread. |
| `chat {text}` | an admitted peer | all admitted peers | Fan out with the sender's name. 2 KB. Not stored. |
@@ -475,10 +523,8 @@ member who joins a room where a guest is already waiting therefore sees
the knock in the roster, not in a `knock` that had no recipient; `knock`
is the incremental form for members already present.
-**`lost`** carries a reason, and the reason decides the browser's
-response: `buffer` and `replaced` are transport outcomes and the
-browser resumes; `standing`, `expired`, `removed` and `ended` are
-terminal and the browser closes every peer connection and shows why.
+**`lost`** carries a reason, and §4.5 says what the browser does with
+each.
**The cap** is enforced at every transition into the admitted state,
under the hub lock: a member's `join`, an `admit`, and never on a resume
@@ -488,8 +534,8 @@ Queued guests can exceed four while waiting; admission checks the count
at admission time, so four queued guests and one member yield three
admissions and one `full`.
-The server validates type and shape and the sender's standing and
-admission, and reads nothing inside `offer`, `answer`, `candidate` or
+The server validates type and shape and the sender's authority per
+§2.5, and reads nothing inside `offer`, `answer`, `candidate` or
`chat` beyond the byte length.
### 4.4 Limits and logging
@@ -499,8 +545,9 @@ admission, and reads nothing inside `offer`, `answer`, `candidate` or
- `chat` text above 2 KB is refused.
- A peer may send thirty signal messages per second with a burst of
sixty; beyond that the hub replies 429 with `Retry-After`, and the
- browser's queue (§4.6) retries rather than drops. The client sends
- one message at a time per target, so 429 is reached only by abuse.
+ browser's queue (§4.6) honours it and retries; a legitimate
+ negotiation burst across three targets can touch the limit briefly
+ and loses nothing.
- A room holds at most sixteen participations in any state (§4.2) and at
most eight waiting; a ninth waiting `join` is refused with `full`.
- A session holds one peer per room (§4.2); a person holds at most four
@@ -516,20 +563,46 @@ admission, and reads nothing inside `offer`, `answer`, `candidate` or
### 4.5 Renewal, resume and restart
The five-minute TTL and any dropped stream are handled by **resume**,
-not by rejoining. When a stream closes, the peer stays in the hub for a
-thirty-second `resume_until` with its admission intact. The browser
-reopens `GET /m/{sqid}/stream?peer=<id>`; the hub finds the peer bound
-to this session, attaches the new stream, and sends it a fresh `roster`
-(§4.3) as the first event after `hello`. The browser **reconciles**
-against that roster: a peer connection to an id not in the roster is
-closed, an id in the roster with no connection gets one. That is how a
-browser that missed a `gone` while its buffer overflowed still closes
-the removed peer's connection: the roster it receives on resume is the
-authority, not the events it missed. No event goes to anyone else; the
-other browsers' peer connections were never affected because media is
-P2P. A resume does not consume a cap slot. A peer that misses the
-window is removed as a leave. `lost {reason: "buffer"}` and `{reason:
-"replaced"}` are followed by the same resume.
+not by rejoining. When a stream closes, the participation stays in the
+hub for a thirty-second `resume_until` with its admission intact. The
+browser reopens `GET /m/{sqid}/stream?peer=<id>&last=<json>`, where
+`last` maps each sender's peer id to the highest `seq` this browser has
+received from it. The hub finds the participation bound to this session
+and attaches the new stream. What it sends next depends on admission:
+
+- **Admitted**: `hello`, then a fresh `roster` (§4.3), then a **replay**
+ of every targeted message (`offer`, `answer`, `candidate`) held for
+ this participation in its replay ring with `seq` above `last`. The
+ ring holds the last thirty-two targeted messages sent to each
+ participation, for the resume window only, in memory, and is dropped
+ with the participation; that is the one place Meet's process holds
+ signalling for longer than the instant of relay, bounded by thirty
+ seconds and thirty-two messages, and it is stated in §0.1. If a
+ sender's messages above `last` are no longer all in the ring, the hub
+ sends `gap {from}` instead of a partial replay, and the browser
+ performs a **pair reset** with that sender (§4.6).
+- **Waiting or unjoined**: `hello`, then `state {waiting: true}`, and
+ nothing else. A waiting guest learns nothing about the room by
+ resuming, which is ruling 9's isolation carried through the resume
+ path.
+
+The browser **reconciles** against the roster it received: a peer
+connection to an id not in the roster is closed, an id in the roster
+with no connection gets one. That is how a browser that missed a `gone`
+while its buffer overflowed still closes the removed peer's connection:
+the roster on resume is the authority, not the events it missed. No
+event goes to anyone else; the other browsers' peer connections were
+never affected because media is P2P. A resume does not consume a cap
+slot. A participation that misses the window is removed as a leave.
+
+**`lost` reasons.** `buffer` is followed by a resume. `replaced` is
+**terminal for the displaced tab**: it closes every peer connection and
+shows "This meeting is open in another tab", with one button, "Use this
+tab instead", which opens a fresh stream without `peer` and so replaces
+the other tab in turn. A displaced tab never resumes on its own, so two
+tabs cannot fight; only a person's click moves the participation.
+`standing`, `expired`, `removed` and `ended` are terminal and the
+browser closes every peer connection and shows why.
A **process restart** empties the hub. Every browser's stream errors
and its resume is refused with 404 (unknown peer). On that refusal the
@@ -541,19 +614,27 @@ connections on the refusal, no guest keeps media flowing from before
the restart. That is the honest cost of keeping admission in memory,
stated rather than hidden behind persistence.
-### 4.6 Delivery: ordered, acknowledged, per pair
+### 4.6 Delivery: ordered, acknowledged, replayed
-SSE plus POST is not a session; the browser makes it one. For each
-target peer the browser keeps an **outbound queue** and sends one
-message at a time, in order, waiting for the POST's 2xx before the
+SSE plus POST is not a session; the browser and the hub make it one.
+For each target peer the browser keeps an **outbound queue** and sends
+one message at a time, in order, waiting for the POST's 2xx before the
next. Each `offer`, `answer` and `candidate` carries a `seq`, per
(sender, target), starting at one and incrementing; the server relays
-`seq` unchanged. A POST that fails (network, 5xx, 429) is retried with
-backoff, same `seq`, up to thirty seconds, then the browser treats the
-target as unreachable, closes that connection, and re-creates it on the
-next `roster`. The receiver keeps the last `seq` seen per sender and
-drops anything at or below it, so a retried POST that had in fact
-landed is harmless.
+`seq` unchanged and keeps a copy in the target's replay ring (§4.5). A
+POST that fails (network, 5xx, 429) is retried with backoff, same
+`seq`, up to thirty seconds, then the browser treats the target as
+unreachable, closes that connection, and re-creates it on the next
+`roster`. The receiver keeps the last `seq` seen per sender and drops
+anything at or below it, so a retried POST that had in fact landed is
+harmless, and a replay on resume delivers only what was missed.
+
+A **pair reset** is the recovery when replay cannot fill a gap: the
+browser that received `gap {from}` closes its connection to that peer
+and posts `reset` to it; the peer closes its side too; both then create
+a fresh connection and negotiate as for a new `peer`. A reset costs the
+pair a second or two of media and happens only after a resume whose
+gap exceeded the ring, which is rare by construction.
Candidates are applied only after a remote description is set; earlier
ones are held in a per-peer buffer, which the W3C pattern already
@@ -589,7 +670,7 @@ or parse is a startup refusal outside `-env development`, logged as one
line naming the variable and not its value. The decoded value is held
in memory and never logged.
-Per call, `GET /m/{sqid}/ice` (under the session and standing) returns
+Per call, `GET /m/{sqid}/ice` (an admitted participation, §2.5) returns
the `RTCIceServer` list. For each element, every URL is passed through
as given; URLs whose scheme is `turn` or `turns` carry `username =
"<unix expiry>:" + username_suffix` and `credential =
@@ -609,15 +690,22 @@ Credential expiry is a hard deadline on the relay, checked on allocate
and on every refresh: the allocation ends at expiry whether or not the
browser sends anything. The client therefore renews at two and a half
hours, and **renewal is per connection and complete only when the new
-path works**:
+path is proven**:
1. Fetch `/ice`. On failure, retry every minute; the tile shows a
warning from the first failure.
2. For each peer connection: `setConfiguration` with the new servers,
- `restartIce()`, and let perfect negotiation carry the offer. The
- connection is **renewed** when `iceConnectionState` reaches
- `connected` or `completed` after the restart, within thirty seconds.
- Otherwise restart again, up to three times.
+ `restartIce()`, and let perfect negotiation carry the offer. An ICE
+ restart changes the local ICE username fragment, so the restart has
+ a **generation** the browser can name: the new `usernameFragment`.
+ The connection is **renewed** when, within thirty seconds, (a) the
+ restart's offer/answer exchange has completed and `signalingState`
+ is `stable` with that answer applied, and (b) `getStats()` reports a
+ selected candidate pair in state `succeeded` whose local candidate
+ carries the new generation's `usernameFragment`. An
+ `iceConnectionState` change on its own proves nothing, because a
+ `completed` transport moves to `connected` merely by starting a
+ restart. Otherwise restart again, up to three times.
3. The old credential's deadline is kept as the **must-succeed-by**
time for every connection not yet renewed. A connection that is not
renewed by then shows "relay expired" with a retry, which runs step 2
@@ -695,14 +783,16 @@ and one script file so a second app can adopt it.
### 7.1 Lone shape
-Sign-in is Docs' `internal/docs/auth.go` adopted with the two changes in
-§3.2: the pending-join exception to the allowlist gate, and
-`SignedInPath` set to `/continue`. `MEET_ALLOWED_ADDRESSES`,
-`MEET_ORIGIN`, `MEET_INSTANCE_KEY`, `MEET_SMTP_*` and `MEET_DEK` are
-startup refusals outside `-env development`. The same 303 for every
-address. This file already lives in Docs, Calendar and Slides; Meet's is
-the fourth copy, and the extraction task Slides recorded stands. Meet
-adds none.
+Sign-in is Docs' `internal/docs/auth.go` adopted with the changes §3.2
+specifies in full: the pending-join exception to the gate in front of
+`Begin`, the `pending_joins` row written before `Begin`, an `Authorize`
+that admits allowlist-or-pending, `SignedInPath` set to `/continue`,
+and the outsider budgets. §3.2 also states which responses remain
+distinguishable. `MEET_ALLOWED_ADDRESSES`, `MEET_ORIGIN`,
+`MEET_INSTANCE_KEY`, `MEET_SMTP_*` and `MEET_DEK` are startup refusals
+outside `-env development`. This file already lives in Docs, Calendar
+and Slides; Meet's is the fourth copy, and the extraction task Slides
+recorded stands. Meet adds none.
### 7.2 Actors
@@ -826,15 +916,17 @@ own gates from §13; nothing is deferred to the deploy slice.
1. **Meetings, links, sign-in.** `persons`, `meetings`, `meeting_links`,
`guest_grants`; sealing; the list; create; the link shown once; the
two confirm pages and their POSTs; the `/j/` route with all three
- outcomes and the one 404; the pending-join cookie, `pending_guests`,
- the `Authorize` callback, `/continue` with its three outcomes, the
- allowlist exception and the two outsider budgets; `/welcome`;
+ outcomes and the one 404; the pending-join cookie, `pending_joins`,
+ the `Authorize` callback, `/continue` with its three outcomes and
+ the atomic re-check, the allowlist exception and the two outsider
+ budgets; `/welcome`; the §2.5 contract as middleware;
standing (§2.3) as a package with tests; `make release` stamping.
Scriptless throughout. Joining lands on a holding page that says the
room is the next slice.
2. **Holding screen and room, direct connectivity.** `devices.js`, the
hub with peer binding, participation, cap, limits, resume with
- roster reconciliation and the tick; the stream and `hello`; every
+ admission-branched output, the replay ring, `gap` and pair reset,
+ terminal `replaced`, and the tick; the stream and `hello`; every
message in §4.3 except `chat` and `share`; the removal confirm page;
`room.js` with perfect negotiation and the §4.6 queue; the
participation transitions; the `AGENTS.md` exception. `CARLOS_RELAY` absent. The browser gate: two
@@ -863,9 +955,11 @@ one-404 rule (1); the full emailed-link round trip for an outsider,
with the pending-join cookie present and absent at the click, the
`Authorize` callback admitting the pending address and refusing an
unknown one, revocation during sign-in, `/continue`'s three outcomes
-for member and non-member with present, absent and expired rows, and
-the two outsider budgets including outsider exhaustion followed by a
-member sign-in through the same client address (1); standing as a
+for member and non-member with present, absent and expired rows, a
+member's signed-out link journey completing in another browser, link A
+replaced by B between mail and click (404, no grant), and the outsider
+budgets plus the documented exhaustion of the shared allowance by
+repeated submission of one member address (1); standing as a
table test (1); hub tests for peer binding and refusal of a foreign or
cross-room `peer`, the sixteen-participation and unjoined-timeout
limits with streams opened and never joined, targeted relay only to
@@ -874,14 +968,19 @@ admitted peers in the room, a waiting guest receiving nothing but
members and not to guests, guest-first arrival then a member joining,
promotion of a waiting guest to member under and at the cap, the cap
at `join` and at `admit` including four queued guests and concurrent
-admissions, resume within and after the window with the roster sent,
+admissions, resume within and after the window with the roster sent to an admitted
+peer and only `state` to a waiting one, replay from the ring above
+`last`, `gap` when the ring is exceeded, `replaced` terminal for the
+displaced tab, `admit` of a guest whose grant expired since the last
+tick refused,
the 64-event buffer, the rate limit, an admitted guest continuing past
grant expiry on every endpoint and being refused fresh entry, a member
dropped on the tick (2); the §0.1 database and WAL tests (1) and the
captured-log test for join, admit, offer, candidate, a refusal and an
error (2), extended to accepted chat (4); the minting vector and the
renewal state machine including a successful fetch followed by a failed
-restart (3); the API reach check after an operator's removal (5);
+restart, and a `completed` transport that moves to `connected` on
+restart start without qualifying as renewed (3); the API reach check after an operator's removal (5);
`make ci` green on every slice.
**Browser.** Calendar's chromedp drive is recorded as not running in
@@ -899,7 +998,10 @@ the TTL shortened to seconds leaves the call connected (2); a member
removed from the allowlist mid-call is dropped on the next tick and the
other context sees `gone` (2); a context whose stream is closed while a
peer leaves resumes, receives the roster, and has closed the connection
-to the departed peer (2); after a server restart both contexts have
+to the departed peer (2); an offer whose SSE delivery is cut after the
+POST's 2xx is replayed on resume and the pair connects (2); a second tab
+on the same session displaces the first, which shows the take-over
+button and does not reconnect on its own (2); after a server restart both contexts have
closed their old connections before rejoining (2); renewal with the schedule shortened to
seconds produces a second `/ice` fetch and an ICE restart that leaves
the track flowing, and a failed fetch shows the warning (3); the
@@ -948,7 +1050,7 @@ half-closed, twelve new, request changes. Dispositions in revision 3:
| Finding | Disposition |
|---|---|
-| R2-1 authorize callback | `pending_guests` written before `Begin`; `Authorize` admits an allowlisted address or a pending fingerprint; works in any browser (§2.1, §3.2). Closes the R1-7 residue. |
+| R2-1 authorize callback | `pending_guests` (now `pending_joins`) written before `Begin`; `Authorize` admits an allowlisted address or a pending fingerprint; works in any browser (§2.1, §3.2). Closes the R1-7 residue. |
| R2-2 shared budget | Outsider attempts counted before `Begin`, five per link and ten overall per fifteen minutes, leaving ten of the shared twenty for members; residue and rastrillo's own `?err=` responses stated, not claimed away (§3.2). Closes the R1-8 residue. |
| R2-3 expiry versus authorization | Ruling 14: standing for entry, participation for continuing; `/signal`, resume and `/ice` authorized by participation; an admitted guest continues past expiry on every endpoint and cannot re-enter (§2.3, §2.4). Closes the R1-5 residue. |
| R2-4 missed `gone`, restart | Resume sends an authoritative `roster` and the browser reconciles; a restart's 404 makes the browser close every connection before rejoining; `lost` reasons split into resumable and terminal (§4.3, §4.5). Closes the R1-9 residue. |
@@ -961,6 +1063,25 @@ half-closed, twelve new, request changes. Dispositions in revision 3:
| R2-11 removal confirm | `remove` is no longer a signalling message; confirm page and guarded POST with authorization at execution (§3.5). |
| R2-12 the floor | Host-only gathering, ICE finds a direct path, peer-reflexive named; bilateral reachability dropped (§5.5, §0.3). Closes the R1-14 residue. |
-Revision 3 awaits Paul's review of the written form and a third Codex
+**Round 3**, GPT-6 via Codex against revision 3 at 1bd7161:
+`CODEX-FINDINGS-MEET-R3.md`: six round-2 findings closed, five
+half-closed, one worse; ten new (three P1); request changes.
+Dispositions in revision 4:
+
+| Finding | Disposition |
+|---|---|
+| R3-1 waiting guest resumes into the roster | Resume output branches on admission: admitted gets `roster` and replay; waiting or unjoined gets `state {waiting}` only (§4.5). Closes the R2-4 regression. |
+| R3-2 revoked link indistinguishable | `pending_joins` binds the originating link; `/continue` re-checks that link, its meeting and the row atomically with grant creation (§2.1, §3.2). |
+| R3-3 admit after expiry, before the tick | `admit` and tick promotion check the target's entry authority under the same lock as the transition (§2.5). |
+| R3-4 endpoint contracts still on standing | §2.5, one authorization row per endpoint; `join` checks standing, everything after admission checks participation only; member kind re-derived on every request (§2.5, §4.1, §4.3, §5.2 point at it). |
+| R3-5 no member reserve | The reserve claim is withdrawn; the residual denial of service through a member's address is stated as the edge-wide limitation it is, with the platform question filed at slice 1 (§3.2). |
+| R3-6 signed-out member loses continuation | `pending_joins` is written for members too and carries the destination; `Authorize` needs it only for outsiders; most recent row wins (§3.2). |
+| R3-7 downstream loss after 2xx | Resume carries `last` per sender; the hub replays from a thirty-two-message ring per participation; `gap` triggers a pair reset (§4.5, §4.6). |
+| R3-8 `connected` proves nothing | Completion requires the restart generation's answer applied and a succeeded selected pair whose local candidate carries the new `usernameFragment` (§5.3). |
+| R3-9 displaced tab fights back | `replaced` is terminal for the displaced tab; only a person's click takes the participation back (§4.5). |
+| R3-10 stale claims | §7.1 now defers to §3.2; the 429 claim is removed (§4.4). |
+
+Revision 4 awaits Paul's review of the written form and a fourth Codex
round. Neither has happened; this line is the record.
+
diff --git a/docs/superpowers/specs/CODEX-FINDINGS-MEET-R3.md b/docs/superpowers/specs/CODEX-FINDINGS-MEET-R3.md
new file mode 100644
index 0000000..783b8e4
--- /dev/null
+++ b/docs/superpowers/specs/CODEX-FINDINGS-MEET-R3.md
@@ -0,0 +1,96 @@
+# Findings on the Meet spec, revision 3
+
+GPT-6 via `codex exec`, read-only, round 3, reviewing commit 1bd7161 against `CODEX-FINDINGS-MEET.md` and `CODEX-FINDINGS-MEET-R2.md`. Verbatim.
+
+Reviewed revision 3 at `1bd71614ac654e4fe5103d33516b6191fca42e27`. No files modified. “Closed” below means the design answers the reported failure, not that implementation has passed its tests.
+
+| Round-2 finding | Status | Assessment |
+|---|---|---|
+| R2-1 callback | Closed | §3.2 explicitly changes `Authorize` and supplies database-backed authorization for an outsider opening the email in another browser. |
+| R2-2 mail budget | Half-closed | Guest-address traffic is bounded, but unauthenticated traffic claiming a member address bypasses both budgets. R3-5. |
+| R2-3 expiry authority | Half-closed | Participation supplies the missing authority, but endpoint contracts still require standing. R3-4. |
+| R2-4 resume/restart | Worse | Reconciliation answers missed `gone`, and restart closes old connections. Unconditional resume rosters introduce a waiting-room disclosure. R3-1. |
+| R2-5 delivery | Half-closed | Ordered POSTs and duplicate suppression help upstream delivery. A successful POST followed by downstream loss still loses negotiation permanently. R3-7. |
+| R2-6 queue/promotion | Closed | Members receive the waiting snapshot; promotion attempts admission under the cap, with an explicit retry when full. |
+| R2-7 ordinary continuation | Closed | Direct member sign-in reaches `/`; outsiders reach the specified `/welcome`. Meeting-link continuation for members has a separate regression. R3-6. |
+| R2-8 unjoined streams | Closed | The sixteen-participation limit applies before allocation and includes retained state; unjoined registrations expire. |
+| R2-9 renewal completion | Half-closed | Completion is now tracked, but the chosen ICE state can indicate the start of a restart, before replacement connectivity succeeds. R3-8. |
+| R2-10 sharing/tests | Closed | Stopping and negotiating rejection of the screen transceiver supports the remote-track ending assertion. Accepted-chat tests now belong to slice 4. |
+| R2-11 removal confirmation | Closed | §3.5 supplies the required confirmation URL, component and guarded POST. |
+| R2-12 network floor | Closed | Host-only gathering and possible peer-reflexive discovery replace the false bilateral-reachability condition. |
+
+| Half-closed round-1 finding | Status now | Assessment |
+|---|---|---|
+| R1-4 memory bounds | Closed | All room registrations are bounded, including pre-join and resume state. |
+| R1-5 live authorization | Half-closed | Entry and continued participation are separated, but admission at expiry and endpoint authorization remain inconsistent. R3-3, R3-4. |
+| R1-7 continuation | Half-closed | Outsider sessions work; originating-link revocation and member meeting continuation do not. R3-2, R3-6. |
+| R1-8 mail abuse | Half-closed | The added budgets do not establish the claimed member reserve. R3-5. |
+| R1-9 reconnect | Half-closed | Missed departures and restart are answered; waiting resume and replacement-stream ownership remain defective. R3-1, R3-9. |
+| R1-10 negotiation | Half-closed | Collision handling is sound in principle; delivery loss and renewal completion remain. R3-7, R3-8. |
+| R1-14 network floor | Closed | The revised floor survives the earlier counterexamples. |
+| R1-19 browser gates | Closed | The previous driver, identity-isolation, stop-sharing and slice-assignment defects are answered. Additional gates are needed for the failures below. |
+
+**R3-1 — P1 — §§4.3, 4.5: A waiting guest receives the call roster by resuming.**
+
+A waiting guest opens a stream, posts `join`, disconnects, and resumes within thirty seconds. §4.5 unconditionally sends a fresh roster after `hello`. §4.3 defines that roster as every admitted peer’s ID, name and kind. This contradicts the waiting guest’s permitted recipients and reopens R1-1’s isolation failure. It requires neither admission nor a forged peer. [Roster definition](/home/paulca/amadan.net/oficina/meet-spec/docs/superpowers/specs/2026-09-07-meet-design.md:470), [resume procedure](/home/paulca/amadan.net/oficina/meet-spec/docs/superpowers/specs/2026-09-07-meet-design.md:518).
+
+**Correction:** Branch resume output by admission. An admitted participant receives the authoritative roster; an unjoined or waiting participant receives only its own state. Test a waiting guest’s deliberate disconnect/resume and five-minute renewal.
+
+**R3-2 — P1 — §§2.1, 3.2: Continuation cannot identify the revoked link that authorized sign-in.**
+
+`pending_guests` records `meeting_id`, but no link ID or token hash. `/continue` ignores the cookie and promises to reject a link revoked during sign-in. After link A is replaced by B, the meeting remains live and has a live link. The pending row cannot distinguish an attempt authorized by revoked A from one authorized by B. Checking the meeting or its current links cannot enforce the promised revocation. [Pending schema](/home/paulca/amadan.net/oficina/meet-spec/docs/superpowers/specs/2026-09-07-meet-design.md:160), [continuation](/home/paulca/amadan.net/oficina/meet-spec/docs/superpowers/specs/2026-09-07-meet-design.md:313).
+
+**Correction:** Bind each pending continuation to its originating `meeting_links` row. Revalidate that specific link, the meeting and their deadlines when consuming the pending row, atomically with grant creation. Test A → pending sign-in → replace with B → click A’s emailed sign-in link.
+
+**R3-3 — P1 — §§2.4, 4.3: Admission can convert an expired waiting grant into indefinite participation.**
+
+Waiting-grant expiry is processed only on the fifteen-second tick. The `admit` operation checks that its target is waiting and that capacity remains; it does not check the target’s current grant. An admission arriving after expiry but before the tick therefore marks the guest admitted. At the next tick, the admitted-guest expiry rule does nothing, preserving the participation. This defeats the distinction between an already-admitted guest continuing and an expired waiting guest entering. [Expiry transitions](/home/paulca/amadan.net/oficina/meet-spec/docs/superpowers/specs/2026-09-07-meet-design.md:240), [`admit` checks](/home/paulca/amadan.net/oficina/meet-spec/docs/superpowers/specs/2026-09-07-meet-design.md:460).
+
+**Correction:** At every admission transition, check the target’s current entry authority as well as capacity. Apply the transition under the same synchronization as expiry. Test admission immediately after grant expiry without first running the tick.
+
+**R3-4 — P2 — §§2.4, 4.1, 4.3, 5.2: The participation authority still conflicts with the endpoint contracts.**
+
+§2.4 authorizes signalling, resume and `/ice` through participation after grant expiry. §4.1 still puts the stream under a standing check; §4.3 still validates the sender’s standing; §5.2 expressly puts `/ice` under standing. An implementation following those endpoint sections reproduces R2-3: the admitted guest cannot renew its stream or relay credentials after two hours. [Participation authority](/home/paulca/amadan.net/oficina/meet-spec/docs/superpowers/specs/2026-09-07-meet-design.md:214), [stream](/home/paulca/amadan.net/oficina/meet-spec/docs/superpowers/specs/2026-09-07-meet-design.md:409), [signalling](/home/paulca/amadan.net/oficina/meet-spec/docs/superpowers/specs/2026-09-07-meet-design.md:491), [ICE](/home/paulca/amadan.net/oficina/meet-spec/docs/superpowers/specs/2026-09-07-meet-design.md:592).
+
+**Correction:** Give each endpoint one authorization contract. Distinguish fresh entry, admitted continuation and member-only operations. Require current membership for privileged operations and apply member-loss transitions on requests as well as ticks. Keep the admitted guest’s continuation authority independent of grant expiry.
+
+**R3-5 — P2 — §3.2: The outsider budgets do not reserve ten attempts for members.**
+
+The gate classifies the submitted address, not the unauthenticated requester. An attacker submits one known allowlisted address twenty times without a meeting link. Every request bypasses both outsider budgets and reaches `Begin`. Its IP limiter runs before its address limiter, so even attempts refused after the first five consume the shared IP allowance. The next real member cannot sign in. The same mechanism permits targeted address-budget exhaustion; enumeration therefore is not confined to the stated globally exhausted window. [Budget claim](/home/paulca/amadan.net/oficina/meet-spec/docs/superpowers/specs/2026-09-07-meet-design.md:331), [limiter ordering](/home/paulca/go/pkg/mod/github.com/keymaildev/signin@v0.1.1/flow.go:117).
+
+**Correction:** Do not claim authenticated member capacity based on an unverified address. Either explicitly accept this larger residual failure or provide a trusted client-address/separate admission mechanism that survives it. Add a test exhausting the shared budget using repeated submissions of one member address.
+
+**R3-6 — P2 — §3.2: A signed-out member loses the meeting continuation.**
+
+A member follows `/j/A` while signed out and receives the pending cookie. `POST /signin` writes `pending_guests` only for an outsider address. After authentication, `/continue` deliberately ignores the cookie, finds no row and redirects the member to `/`. The original member meeting-link journey no longer reaches its holding screen. [Row creation](/home/paulca/amadan.net/oficina/meet-spec/docs/superpowers/specs/2026-09-07-meet-design.md:291), [continuation branches](/home/paulca/amadan.net/oficina/meet-spec/docs/superpowers/specs/2026-09-07-meet-design.md:309).
+
+**Correction:** Record meeting continuations for members as well as outsiders, while keeping outsider session authorization a separate purpose. Specify selection when an address has multiple pending meetings. Test signed-out member → meeting link → emailed sign-in → intended holding screen, including another-browser completion.
+
+**R3-7 — P2 — §§4.5, 4.6: POST acknowledgement does not acknowledge signalling delivery.**
+
+An offer POST receives 2xx, then the recipient’s SSE connection fails before delivering the offer. The sender advances its queue and never retries that offer. Resume returns the same peer roster, so reconciliation leaves the existing connection untouched. Neither roster reconciliation nor dropping duplicate sequence numbers reconstructs the missing offer. A later answer can be lost the same way, leaving negotiation stuck despite successful POSTs. [POST acknowledgement](/home/paulca/amadan.net/oficina/meet-spec/docs/superpowers/specs/2026-09-07-meet-design.md:546), [resume reconciliation](/home/paulca/amadan.net/oficina/meet-spec/docs/superpowers/specs/2026-09-07-meet-design.md:521).
+
+**Correction:** Define either bounded receiver acknowledgement/replay or an explicit, coordinated negotiation reset after signalling discontinuity. Specify sequence gaps and pair-generation changes. Test a 2xx offer or answer whose SSE delivery is lost, with both peers still present in the resumed roster. This belongs to slice 2.
+
+**R3-8 — P2 — §5.3: `connected` after restart can report renewal before the replacement path works.**
+
+The proposed completion predicate accepts `connected` or `completed` after restart. WebRTC explicitly permits an ICE transport in `completed` to transition to `connected` when restart begins. That transition can occur while the old path still carries traffic and the new negotiation has not succeeded. Meet can therefore discard the old must-succeed-by deadline prematurely and encounter the same hard expiry R2-9 described. [Completion predicate](/home/paulca/amadan.net/oficina/meet-spec/docs/superpowers/specs/2026-09-07-meet-design.md:616), [W3C ICE transport states](https://www.w3.org/TR/webrtc/#dom-rtcicetransportstate).
+
+**Correction:** Tie completion to the intended restart generation, its completed offer/answer exchange and successful replacement connectivity. A state transition caused by starting the restart must not qualify. Keep the old deadline until that proof exists. Test a previously completed connection whose restart begins but whose answer or replacement checks fail.
+
+**R3-9 — P2 — §§4.2, 4.3, 4.5: A replaced stream is instructed to reclaim ownership indefinitely.**
+
+Two tabs share one session and open the same room. The second replaces the first; the first receives `lost {reason:"replaced"}` and must resume. There is no stream-generation ownership rule that distinguishes this displaced tab from a legitimate reconnect. If its resume replaces the second stream, that tab receives the same instruction. If replacement instead changes the peer ID, the first tab’s 404 recovery opens another fresh stream and can displace the second again. [Replacement](/home/paulca/amadan.net/oficina/meet-spec/docs/superpowers/specs/2026-09-07-meet-design.md:440), [`lost` handling](/home/paulca/amadan.net/oficina/meet-spec/docs/superpowers/specs/2026-09-07-meet-design.md:478), [404 recovery](/home/paulca/amadan.net/oficina/meet-spec/docs/superpowers/specs/2026-09-07-meet-design.md:534).
+
+**Correction:** Make replacement terminal for the displaced tab, or define a stream-generation ownership mechanism that prevents it reclaiming the participation automatically. Test two tabs with the same session and verify that one remains connected without repeated replacement.
+
+**R3-10 — P3 — §§4.4, 7.1: Two explanatory claims remain false.**
+
+- §7.1 still describes “two changes” to Docs and “the same 303 for every address.” Revision 3 adds the callback, pending rows and budgets, and §3.2 explicitly retains distinguishable redirect destinations. Replace this stale summary with a reference to the complete §3.2 contract. [Adoption summary](/home/paulca/amadan.net/oficina/meet-spec/docs/superpowers/specs/2026-09-07-meet-design.md:698).
+- §4.4 says one in-flight message per target means 429 occurs only through abuse. Sequential requests can exceed thirty per second; three target queues also run concurrently. Remove the assertion and retain retry handling for legitimate negotiation bursts. [Rate claim](/home/paulca/amadan.net/oficina/meet-spec/docs/superpowers/specs/2026-09-07-meet-design.md:500).
+
+The requested local reuse sources support the revised sealer provenance, session callback shape, Sheets buffer behavior, Calendar feed mounting, Slides principal model and browser-driver correction. The three-hour credential mint and thirty-minute renewal margin match relay §2; the defect is proving renewal completion. The scriptless floor and corrected chat-test slice assignments survive this review.
+
+Verification limits: the supplied dumps still do not establish `rastrillo/rastrillo` discussion 17 or `oficina/suite` discussion 5. The claimed live relay deployment was not independently verified. The roadmap was read at `8291cb2` because its path is absent from the current platform checkout; relay revision 6 was available at `dcf0cad`. These limits are not findings of false attribution.
+
+**Verdict: request changes.**
\ No newline at end of file