oficina / meet Public

spec: codex round 4 findings and Meet revision 5

Round 4: eight R3 findings closed, two half, four new (two P2, two
P3), request changes. Revision 5: pair generations held by the hub
with gen on every targeted message and typed reset and pair messages;
fresh entry over a live participation is 409 unless the take-over
button says so, takeover creates a new peer and ends the old, resume
of an ended peer is 410 with its reason, hello and 404 carry a boot id
so a restart is distinguishable; the 2.5 table reconciled with its
handlers; the revoked-continuation outcomes stated per path.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Paul Campbell pushed by paul@keymail.dev bbfe4406b5cffaba11ab40e4d1cd6034efc2bfc4 parent 79787b8
2 files changed, +197 −49
  • docs/superpowers/specs/2026-09-07-meet-design.md +118 −49
  • docs/superpowers/specs/CODEX-FINDINGS-MEET-R4.md +79 −0
diff --git a/docs/superpowers/specs/2026-09-07-meet-design.md b/docs/superpowers/specs/2026-09-07-meet-design.md
index 160f45d..bc31c34 100644
--- a/docs/superpowers/specs/2026-09-07-meet-design.md
+++ b/docs/superpowers/specs/2026-09-07-meet-design.md
@@ -1,8 +1,9 @@
# 🤖 Meet — design
Design, 2026-09-07. Direction approved by Paul in conversation; this is the
-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
+written form. **Revision 5**, answering the four findings of the
+fourth Codex review (`CODEX-FINDINGS-MEET-R4.md`); §14 has all four
+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.
@@ -258,12 +259,14 @@ 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 /`, `POST /meetings`, the replace-link and end-meeting confirm pages and their POSTs | **Current member standing** | On the request. |
+| `/m/{sqid}/remove/{peer}`, confirm page and POST | An **admitted** participation for this session in this room with **current** `member` kind | On the request, at execution. |
+| `GET /m/{sqid}/join`, `GET /m/{sqid}` | Standing (`member` or `guest`) **or** a live participation for this session in this room. §3.3 and §3.4 defer to this row. | 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`, `admit` | An **admitted** 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`, `leave` | Participation, any state | On the request. A waiting guest may leave. |
| `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. |
@@ -345,12 +348,19 @@ session and no per-attempt return value, so Meet's `SignedInPath` is
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);
+ sign-in is refused 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.
+
+ Two refusals, two outcomes, stated so the tests assert the right
+ thing: an **outsider** whose only pending row's link was replaced
+ between mail and click is refused earlier, at `Authorize`, and the
+ inherited rastrillo handler answers **403**; a **signed-in person**
+ whose row fails the re-check at `/continue` gets `/` or `/welcome`.
+ Neither path mints a grant. Meet does not change the inherited 403.
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
@@ -401,7 +411,7 @@ sign-in form, and the README says so.
### 3.3 The holding screen
-`/m/{sqid}/join` requires standing (§2.3) and is the page where a call
+`/m/{sqid}/join` is authorized per §2.5 and is the page where a call
needs scripts. Rendered server-side it shows the meeting's title, its
start time if it has one, the person's own name and standing, the floor
sentence from §5.5 when it applies, links to the meeting list and, for
@@ -484,11 +494,20 @@ 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 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).
+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 live participation in
+the room (attached or within its resume window) is refused with
+**409** and a body naming the existing peer, unless it carries
+`?takeover=1`, which is sent only by the "Use this tab instead" button
+(§4.5). A takeover **ends** the old participation: a new participation
+with a new peer id is created, `gone` goes to admitted peers for the
+old id, `lost {reason: "replaced"}` goes to the old stream if one is
+attached, and the new one proceeds as a fresh `join`. Nothing automatic
+ever displaces a tab.
+
+Every `hello` carries `boot`, a random id chosen when the process
+started, so a browser can tell a restart from any other refusal (§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
@@ -507,17 +526,19 @@ a multi-host fleet; that is noted, not solved.
| `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. |
+| `offer`, `answer`, `candidate` | an admitted peer | the one admitted peer named in `to` | Refuse with 409 if `gen` is not the pair's current generation (§4.6); otherwise relay the payload unread and keep a copy in the target's replay ring. |
+| `reset {to}` | an admitted peer | both peers of the pair | Under the hub lock, increment the pair's generation and send `pair {peer, gen}` to both, into their replay rings. Idempotent in effect: two resets yield one higher generation both sides converge on. |
| `chat {text}` | an admitted peer | all admitted peers | Fan out with the sender's name. 2 KB. Not stored. |
| `share {on}` | an admitted peer | all admitted peers | Fan out so tiles can be labelled. |
-| `hello`, `roster`, `peer`, `gone`, `knock`, `knock-gone`, `full`, `ended`, `lost` | server | as named | `roster` and `peer` go only to admitted peers; a waiting guest receives `hello`, then `roster` on admission, or `lost`, or `ended`, and nothing else. |
+| `hello`, `roster`, `peer`, `gone`, `knock`, `knock-gone`, `full`, `ended`, `lost`, `pair`, `gap`, `state` | server | as named | `roster`, `peer`, `pair` and `gap` go only to admitted peers; a waiting guest receives `hello`, `state`, then `roster` on admission, or `lost`, or `ended`, and nothing else. |
-`offer`, `answer` and `candidate` also carry a `seq` (§4.6), relayed
-unchanged.
+`offer`, `answer` and `candidate` also carry `seq` and `gen` (§4.6);
+`seq` is relayed unchanged and `gen` is checked.
**`roster`** is the authoritative snapshot: every admitted peer with its
-id, name and kind, and, when the recipient is a member, the **waiting
-queue** too. It is sent on admission, on resume (§4.5), and to every
+id, name, kind and the recipient's current **pair generation** with it
+(§4.6), and, when the recipient is a member, the **waiting queue** too.
+`peer` carries the same fields for one peer. It is sent on admission, on resume (§4.5), and to every
admitted peer whenever any participation's kind changes (§2.4). A
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`
@@ -571,8 +592,9 @@ 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
+ of every targeted message (`offer`, `answer`, `candidate`, `pair`)
+ held for this participation in its replay ring with `seq` above
+ `last`, or, for `pair`, with a generation above the roster's. 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
@@ -595,24 +617,32 @@ 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
-browser **closes every peer connection first**, then opens a fresh
-stream and posts `join` under its new peer id. Members re-enter at once
-and negotiate afresh on the new `peer` events. Guests return to waiting
-and must be admitted again, and because their browsers closed their
-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.
+**A refused resume** says why, in the status and a JSON body, and the
+browser's response is fixed per case:
+
+| Resume answer | Meaning | Browser does |
+|---|---|---|
+| `410 {reason: "replaced", by: <peer>}` | This participation was ended by a takeover from another tab on this session. The hub remembers ended-by-takeover peer ids for ten minutes so a tab that never received `lost {reason: "replaced"}` learns it here. | Terminal: close every peer connection, show "This meeting is open in another tab" and the button "Use this tab instead", which opens a fresh stream with `?takeover=1` (§4.2). Never automatic. |
+| `410 {reason: "removed" \| "expired" \| "standing" \| "ended"}` | Ended for that reason; remembered ten minutes. | Terminal, show why. |
+| `404 {boot: <id>}` with a **different** `boot` from the last `hello` | The process restarted. | Close every peer connection, open a fresh stream without `peer`, `join` again. |
+| `404 {boot: <id>}` with the **same** `boot` | The resume window lapsed and nothing else happened. | Close every peer connection, open a fresh stream without `peer`, `join` again. If that fresh stream is refused with 409, another tab owns the room now: show the take-over button, do nothing automatic. |
+| `409 {peer: <id>}` on a fresh stream | This session already holds a live participation in the room (§4.2). | Show the take-over button. |
+
+`lost` reasons on a live stream follow the same split: `buffer` is
+followed by a resume; `replaced`, `standing`, `expired`, `removed` and
+`ended` are terminal and the browser closes every peer connection and
+shows why. A displaced tab therefore never reclaims a room by itself,
+whether or not its `replaced` notice was delivered: the 410 on resume
+and the 409 on fresh entry both stop it, and only the button moves on.
+
+A **process restart** empties the hub and changes `boot`. Every
+browser's resume is refused with a 404 carrying the new `boot`; the
+browser closes every peer connection first, then joins afresh. Members
+re-enter at once and negotiate afresh on the new `peer` events. Guests
+return to waiting and must be admitted again, and because their
+browsers closed their 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, replayed
@@ -629,12 +659,30 @@ unreachable, closes that connection, and re-creates it on the next
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.
+**Pair generations** are how a pair recovers when replay cannot fill a
+gap, and they are held by the hub, not negotiated between browsers.
+Every unordered pair of admitted peers has a generation, starting at
+one when the pair first appears in a roster or `peer` event, which
+carry it. Every `offer`, `answer` and `candidate` carries `gen`; the
+hub refuses a stale `gen` with 409 and relays a current one. A browser
+that needs a fresh connection with a peer, because it received `gap
+{from}` or because its outbound queue to that peer timed out (§4.6
+above), posts `reset {to}`; the hub increments the pair's generation
+under its lock and sends `pair {peer, gen}` to both sides, through
+their replay rings, and the new generation also appears in any later
+`roster`. On `pair`, or on a `roster` whose generation for a peer is
+higher than the local one, a browser: closes its connection to that
+peer, discards its outbound queue and its inbound candidate buffer for
+that peer, resets its `seq` counters for the pair, creates a new
+connection tagged with the new generation, and negotiates as for a new
+`peer`. A `pair` at or below the local generation is a no-op, so
+duplicates and reordering are harmless; two simultaneous resets yield
+two increments and both browsers converge on the higher one; an old
+offer or answer arriving after a rebuild is refused by the hub (stale
+`gen`) or dropped by the receiver (lower `gen` than current). A reset
+costs the pair a second or two of media and happens only after a
+resume whose gap exceeded the ring or after a delivery timeout, both
+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
@@ -957,7 +1005,8 @@ with the pending-join cookie present and absent at the click, the
unknown one, revocation during sign-in, `/continue`'s three outcomes
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
+replaced by B between mail and click (outsider: 403 at `Authorize`;
+signed-in: `/` or `/welcome`; no grant either way), 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
@@ -971,8 +1020,13 @@ at `join` and at `admit` including four queued guests and concurrent
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,
+displaced tab including the case where the `lost` was never delivered
+(410 on resume, 409 on fresh entry, no automatic takeover), a same-boot
+404 followed by a clean rejoin, a different-boot 404 after restart, a
+stale-`gen` POST refused with 409, `reset` yielding one `pair` to each
+side with the same generation, two simultaneous resets converging, a
+`pair` at or below the local generation ignored, `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
@@ -999,7 +1053,10 @@ 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); an offer whose SSE delivery is cut after the
-POST's 2xx is replayed on resume and the pair connects (2); a second tab
+POST's 2xx is replayed on resume and the pair connects (2); a gap
+beyond the ring produces `gap`, a `reset`, a new generation on both
+sides and a reconnected pair, including when one side's `pair` was cut
+and it learns the generation from the roster on its own resume (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
@@ -1081,7 +1138,19 @@ Dispositions in revision 4:
| 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 4**, GPT-6 via Codex against revision 4 at 79787b8:
+`CODEX-FINDINGS-MEET-R4.md`: eight round-3 findings closed, two
+half-closed; four new (two P2, two P3); request changes. Dispositions
+in revision 5:
+
+| Finding | Disposition |
+|---|---|
+| R4-1 reset not a protocol operation | Pair generations held by the hub; `gen` on every targeted message, refused when stale; `reset` and `pair` are typed messages with recipients; `pair` rides the replay ring and the generation rides every roster; convergence and idempotency stated (§4.3, §4.6). Closes the R3-7 and R2-5 residues. |
+| R4-2 lost `replaced` notice | Fresh entry over a live participation is 409 unless the button's `takeover=1`; takeover creates a new peer and ends the old; resume of an ended peer is 410 with the reason for ten minutes; `hello` and 404 carry `boot` so a restart is distinguishable; nothing automatic displaces a tab (§4.2, §4.5). Closes the R3-9, R2-4 and R1-9 residues. |
+| R4-3 table disagreements | `leave` row participation-only; admission and removal rows name the admitted-member requirement; §3.3 defers to §2.5 (§2.5, §3.3). |
+| R4-4 revoked continuation outcome | Outsider: 403 at `Authorize`, inherited; signed-in: `/` or `/welcome`; no grant either way; §13 asserts those (§3.2, §13). |
+
+Revision 5 awaits Paul's review of the written form and a fifth Codex
round. Neither has happened; this line is the record.
diff --git a/docs/superpowers/specs/CODEX-FINDINGS-MEET-R4.md b/docs/superpowers/specs/CODEX-FINDINGS-MEET-R4.md
new file mode 100644
index 0000000..fc73372
--- /dev/null
+++ b/docs/superpowers/specs/CODEX-FINDINGS-MEET-R4.md
@@ -0,0 +1,79 @@
+# Findings on the Meet spec, revision 4
+
+GPT-6 via `codex exec`, read-only, round 4, reviewing commit 79787b8 against the three earlier findings files. Verbatim.
+
+Reviewed revision 4 at `79787b82e2ee675fabd2e144af0e03b06ff0cc97`. No files modified. “Closed” means the design survives the reported failure; it does not mean implementation has passed its gates.
+
+Eight R3 findings are closed. Two remain half-closed. The remaining blocking defects concern recovery in slice 2.
+
+| R3 finding | Status | Assessment |
+|---|---|---|
+| R3-1 waiting-room disclosure | Closed | §4.5 branches resume by admission. Waiting and unjoined peers receive their own state, without the roster or signalling replay. |
+| R3-2 originating-link revocation | Closed | `pending_joins.link_id` identifies the original link, and §3.2 revalidates it atomically with grant creation. Replacing A with B cannot authorize continuation through A. The claimed HTTP outcome remains inconsistent: R4-4. |
+| R3-3 admission after expiry | Closed | §2.5 checks the target’s current entry authority under the admission lock, including tick-driven promotion. The interval before the next tick no longer permits admission on an expired grant. |
+| R3-4 endpoint authorization | Closed | Admitted continuation now authorizes signalling, resume and `/ice`; member-loss checks also run on requests. The original two-hour interruption is answered. Other table inconsistencies remain advisory: R4-3. |
+| R3-5 member reserve | Closed as a disclosure correction | §3.2 withdraws the reserve and explicitly concedes targeted address exhaustion, shared-budget denial of service and attacker-triggered enumeration. It does not claim those failures are fixed. |
+| R3-6 member continuation | Closed | Members also get pending rows. The most-recent-row rule supplies destination selection and supports another-browser completion. |
+| R3-7 downstream signalling loss | Half-closed | Replay answers loss within the retained history. Recovery beyond that history depends on an incomplete reset protocol: R4-1. |
+| R3-8 premature renewal completion | Closed | §5.3 requires the completed exchange and a succeeded selected pair carrying the new local ICE generation. Merely entering `connected` cannot satisfy that predicate. `usernameFragment` exists in the [W3C candidate statistics contract](https://www.w3.org/TR/webrtc-stats/#dom-rtcicecandidatestats-usernamefragment) and is populated from the candidate in [libwebrtc’s collector](https://webrtc.googlesource.com/src/+/refs/heads/main/pc/rtc_stats_collector.cc). |
+| R3-9 replacement loop | Half-closed | Receiving `replaced` is now terminal. Losing that notice still allows automatic takeover: R4-2. |
+| R3-10 stale explanatory claims | Closed | §7.1 refers to the complete sign-in contract; §4.4 admits legitimate 429 responses and retains retries. |
+
+The unresolved residues from R3’s two tables, checked individually:
+
+| Earlier residue | Status now | Assessment |
+|---|---|---|
+| R2-2 mail budget | Closed as a disclosure correction | Shared exhaustion and enumeration are expressly conceded. |
+| R2-3 expiry authority | Closed | Admitted participation reaches all three continuation endpoints. |
+| R2-4 resume/restart, previously worse | Half-closed | Waiting-room disclosure and missed departures are answered; replacement recovery remains defective. R4-2. |
+| R2-5 delivery | Half-closed | Retained messages replay; a gap still lacks reliable coordinated recovery. R4-1. |
+| R2-9 renewal completion | Closed | Completion is tied to replacement connectivity and the intended ICE generation. |
+| R1-5 live authorization | Closed | Admission checks current entry authority; privileged requests recheck membership; the tick covers quiet connections. Instance-owned grants remain authority under doctrine §5.2. |
+| R1-7 continuation | Closed | Outsider authentication, originating-link binding and member destinations are supplied. R4-4 concerns the response claim, not unauthorized grant creation. |
+| R1-8 mail abuse | Closed as a disclosure correction | Link-gated outsider mail is bounded; the inherited member-address attack is explicitly retained. |
+| R1-9 reconnect | Half-closed | Admission preservation, reconciliation and restart handling survive; lost replacement notices do not. R4-2. |
+| R1-10 negotiation | Half-closed | Perfect negotiation and renewal completion survive; coordinated recovery after signalling loss remains incomplete. R4-1. |
+
+**R4-1 — P2 — §§4.3–4.6: Gap recovery depends on a reset message the protocol cannot reliably deliver.**
+
+`reset` appears only in the recovery prose. It has no row in the type-validated message table, and the sequencing and replay contracts expressly cover only `offer`, `answer` and `candidate`. Thus slice 2 cannot implement the specified gap recovery solely from the declared protocol. [Message table](/home/paulca/amadan.net/oficina/meet-spec/docs/superpowers/specs/2026-09-07-meet-design.md:503), [replay contract](/home/paulca/amadan.net/oficina/meet-spec/docs/superpowers/specs/2026-09-07-meet-design.md:574), [reset procedure](/home/paulca/amadan.net/oficina/meet-spec/docs/superpowers/specs/2026-09-07-meet-design.md:632).
+
+Adding the missing type alone does not fix delivery. A receives `gap`, closes its connection and posts `reset` to B. The POST succeeds, but B’s stream drops before delivering it. B resumes with the same roster; its old connection remains, and `reset` is absent from replay. The recovery mechanism has reproduced R3-7’s failure with a control message.
+
+Nor does reset establish a shared connection generation or retire old queued messages. A delayed or duplicated reset can close a newly negotiated connection; an old offer or answer can arrive after one side has rebuilt.
+
+**Correction:** Define reset as a complete protocol operation: admitted sender and same-room admitted target, reliable delivery, idempotency, a shared pair generation, and explicit retirement of old queues, replay entries and receive state. Both browsers must converge on the same generation before treating recovery as complete. Add slice-2 tests for lost reset delivery after 2xx, duplicate reset, simultaneous reset and delayed pre-reset signalling. Include `reset`, `gap` and `state` in §4.3’s complete message contract.
+
+**R4-2 — P2 — §§4.2, 4.5: Losing the terminal replacement notice still permits automatic takeover.**
+
+Tab A’s stream fails. Before A reconnects, tab B opens a fresh stream on the same session and replaces A’s participation. A cannot receive `lost {reason:"replaced"}` over its failed stream.
+
+Under the fresh-participation interpretation, A’s resume now gets an unknown-peer 404. §4.5 directs that browser to open a fresh stream automatically, which replaces B without a person clicking anything. If replacement instead preserves the peer ID, the design supplies no stream-ownership generation that prevents A’s resume from attaching to B’s participation. Calling the delivered notice terminal does not distinguish these cases. [Replacement](/home/paulca/amadan.net/oficina/meet-spec/docs/superpowers/specs/2026-09-07-meet-design.md:485), [terminal handling and automatic 404 recovery](/home/paulca/amadan.net/oficina/meet-spec/docs/superpowers/specs/2026-09-07-meet-design.md:598).
+
+**Correction:** Make displacement discoverable on resume even when its SSE notice was lost. For example, distinguish a stale tab from process restart using a server boot identifier and stream-ownership generation; refuse automatic fresh entry when another participation already owns this session’s room. Specify whether replacement creates a new peer, and its departure/admission notifications. Test A disconnected → B replaces A → A reconnects, with the `replaced` event deliberately undelivered.
+
+**R4-3 — P3 — §§2.5, 3.3, 3.5, 4.3: The “one rule per endpoint” table still disagrees with its handlers.**
+
+Three discrepancies remain:
+
+- §2.5 requires admission for every signal type other than `join` and `admit`; §4.3 permits `leave` from any peer. A waiting guest’s explicit leave is therefore rejected under the table.
+- §2.5’s `admit` row omits the sender’s admitted requirement, which §4.3 supplies. Its removal row similarly names only current membership, while §3.5 requires an admitted member participation.
+- §2.5 permits the holding page through standing **or** live participation; §3.3 says that page requires standing.
+
+These do not recreate R3-4’s admitted-guest signalling failure, but the promised single authorization contract still requires implementer reconciliation. [Endpoint table](/home/paulca/amadan.net/oficina/meet-spec/docs/superpowers/specs/2026-09-07-meet-design.md:254), [holding page](/home/paulca/amadan.net/oficina/meet-spec/docs/superpowers/specs/2026-09-07-meet-design.md:404), [removal](/home/paulca/amadan.net/oficina/meet-spec/docs/superpowers/specs/2026-09-07-meet-design.md:438), [message permissions](/home/paulca/amadan.net/oficina/meet-spec/docs/superpowers/specs/2026-09-07-meet-design.md:507).
+
+**Correction:** Give `leave` its own participation-only row, put admitted-member requirements explicitly on admission and removal, and make §3.3 defer to the chosen page-access rule.
+
+**R4-4 — P3 — §§3.2, 13: Revoked continuation cannot produce the uniformly claimed 404.**
+
+§3.2 says replacing the originating link during sign-in “still 404s,” then directs a failed continuation recheck to `/` or `/welcome`. For an outsider with only that pending row, the earlier `Authorize` check fails instead; the reused rastrillo admission handler returns 403. §13 nevertheless requires “404, no grant” for A replaced by B between mail and click. [Continuation branches](/home/paulca/amadan.net/oficina/meet-spec/docs/superpowers/specs/2026-09-07-meet-design.md:331), [prescribed test](/home/paulca/amadan.net/oficina/meet-spec/docs/superpowers/specs/2026-09-07-meet-design.md:959), [inherited admission handler](/home/paulca/go/pkg/mod/amadan.net/rastrillo/rastrillo@v0.26.0/auth/handlers.go:131).
+
+The original security failure is closed: none of these branches mints a grant through revoked A. The response claim and gate are wrong.
+
+**Correction:** Specify outcomes separately for authentication refusal and continuation recheck failure, then make §13 assert those outcomes and no grant. If 404 is required at both points, explicitly include the necessary auth-handler change.
+
+The checked reuse sources support the remaining claims about Docs’ sign-in and stream timings, Sheets’ hub and link isolation, Calendar’s sealing and feed mounting, Slides’ agent surface, and the scaffold. The relay list, credential shape and three-hour mint match relay §2 at `dcf0cad`. The scriptless floor survives. Slice ordering has no new later-slice dependency; slice 2 needs the recovery corrections above before its gates are sufficient.
+
+Verification limits remain for `rastrillo/rastrillo` discussion 17, `oficina/suite` discussion 5 and the claimed live relay deployment. The supplied discussion dumps do not establish those claims. This is not evidence of false attribution.
+
+**Verdict: request changes.**
\ No newline at end of file