| index bc31c34..7b6bdb2 100644 |
| --- a/docs/superpowers/specs/2026-09-07-meet-design.md |
| +++ b/docs/superpowers/specs/2026-09-07-meet-design.md |
| @@ -1,8 +1,8 @@ |
| # 🤖 Meet — design |
| |
| Design, 2026-09-07. Direction approved by Paul in conversation; this is the |
| -written form. **Revision 5**, answering the four findings of the |
| -fourth Codex review (`CODEX-FINDINGS-MEET-R4.md`); §14 has all four |
| +written form. **Revision 6**, answering the three findings of the |
| +fifth Codex review (`CODEX-FINDINGS-MEET-R5.md`); §14 has all five |
| 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 |
| @@ -261,6 +261,7 @@ One rule per endpoint, so §4 and §5 do not each restate it: |
| |---|---|---| |
| | `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. | |
| +| `/m/{sqid}/takeover`, confirm page and POST | Standing, **and** a live participation for this session in this room that belongs to another stream (§4.2) | On the request, at execution, under the hub lock. | |
| | `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. | |
| @@ -498,13 +499,26 @@ 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. |
| +**409** and a body naming the existing peer. A `GET` never ends a |
| +participation, and there is no query parameter that makes it do so. |
| + |
| +**Takeover** is the one way a person moves a room from one tab to |
| +another, and it is a confirmed POST like every other destructive |
| +action: `GET /m/{sqid}/takeover` renders `ui/confirm-form` ("This |
| +meeting is open in another tab. Use this one instead?"); `POST |
| +/m/{sqid}/takeover`, under CSRF, checks at execution that this session |
| +holds a live participation in the room, then under the hub lock |
| +**ends** it with the same state-dependent departure `leave` uses: an |
| +admitted peer yields `gone` to admitted peers; a waiting one yields |
| +`knock-gone` to admitted members; an unjoined one yields nothing. The |
| +old stream, if attached, gets `lost {reason: "replaced"}`, and the old |
| +peer id is remembered as replaced for ten minutes (§4.5). The response |
| +redirects to the room, whose fresh stream now opens without conflict |
| +and proceeds as a new `join`. If another tab opens a stream between the |
| +POST and that fresh stream, the fresh one gets 409 and the person sees |
| +the button again; nothing automatic ever displaces a tab, and a |
| +cross-site navigation cannot end a call because the ending is a POST |
| +behind CSRF. |
| |
| 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). |
| @@ -622,18 +636,19 @@ 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: "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 a link to the takeover confirm page (§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. | |
| +| `409 {peer: <id>}` on a fresh stream | This session already holds a live participation in the room (§4.2). | Show the link to the takeover confirm page. | |
| |
| `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. |
| +and the 409 on fresh entry both stop it, and only the confirmed POST |
| +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 |
| @@ -650,8 +665,14 @@ 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 and keeps a copy in the target's replay ring (§4.5). A |
| +(sender, target), starting at one and incrementing **for the life of |
| +the sender's participation, never reset**, across pair generations |
| +included; the server relays `seq` unchanged and keeps a copy in the |
| +target's replay ring (§4.5). Because `seq` is monotonic, the resume |
| +cursor `last` needs no generation: a new generation's first offer has |
| +a higher `seq` than anything the receiver saw before its stream |
| +dropped, so replay delivers it, and `gen` decides only whether a |
| +message is current. 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 |
| @@ -673,7 +694,7 @@ 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 |
| +that peer, keeps its `seq` counters (they never reset), 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 |
| @@ -912,7 +933,7 @@ A thin adapter over §8.2, deferred until a consumer asks by name. |
| The exception is **browser media participation** and nothing wider: |
| reaching a camera and another browser needs script. Every other screen |
| works with scripts off, including the meeting list, creating a meeting, |
| -seeing and copying a link, all three confirm pages, sign-in and |
| +seeing and copying a link, all four confirm pages, sign-in and |
| continuation, the `/welcome` page, the agent token page, and the server-rendered |
| parts of the holding screen and the room (§3.3, §3.4): title, start |
| time, standing, floor sentence, the paragraph that says what needs |
| @@ -1023,7 +1044,12 @@ peer and only `state` to a waiting one, replay from the ring above |
| 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 |
| +takeover of an admitted, a waiting and an unjoined participation |
| +producing `gone`, `knock-gone` and nothing respectively with an |
| +admitted guest observing, a takeover POST without the CSRF token |
| +refused, a resume with a nonzero old cursor after a missed `pair` |
| +receiving the new generation's offer by replay, 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, |
| @@ -1057,8 +1083,10 @@ 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 |
| +on the same session is refused with 409, confirms the takeover, and |
| +the first shows the takeover link and does not reconnect on its own, |
| +while a cross-site top-level navigation to any Meet URL ends no call |
| +(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 |
| @@ -1150,7 +1178,19 @@ in revision 5: |
| | 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 5**, GPT-6 via Codex against revision 5 at bbfe440: |
| +`CODEX-FINDINGS-MEET-R5.md`: three round-4 findings closed, one |
| +half-closed; three new (two P1, one P2), all in the takeover and the |
| +cursor revision 5 introduced; request changes. Dispositions in |
| +revision 6: |
| + |
| +| Finding | Disposition | |
| +|---|---| |
| +| R5-1 cursor across generations | `seq` is monotonic for the life of a participation and never resets; `gen` decides currency only, so replay by `seq` alone delivers a new generation's offer (§4.6). Closes the R4-1, R3-7, R2-5 and R1-10 residues. | |
| +| R5-2 takeover broadcasts a waiting departure | Takeover uses the same state-dependent departure as `leave`: `gone`, `knock-gone`, or nothing (§4.2). | |
| +| R5-3 takeover is a destructive GET | Takeover is a confirm page plus a CSRF-protected POST; a `GET` never ends a participation and no query parameter can; the 409 and 410 responses link to the confirm page (§2.5, §4.2, §4.5). | |
| + |
| +Revision 6 awaits Paul's review of the written form and a sixth Codex |
| round. Neither has happened; this line is the record. |
| |
| |