Force-take-on-valid-token: resume-race during pre-detection window (mobile-dominant) #149
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Background
2026-06-24 Engineer probe (bus dispatch 9d85) for #139 detection-coverage surfaced a substantive finding that reframes the "page-reload doesn't fire opponentDisconnect" observation from operator playtest:
The race isn't a detection-coverage gap — it's a valid-resume-during-pre-detection-window-wrongly-REJECTED bug.
Root cause (substrate-grounded probe at-source)
Race condition: client always resumes-on-open if a token exists (net.ts:144). Server
Resume()rejects "seat is still active" when the old seat isn't yet marked disconnected (reconnect.go:205). On an INITIAL connect (page RELOAD,reconnecting=false), the client handles rejection byremoveItem(LS_TOKEN) + sendJoin()(net.ts:269-270) → reloader dumped into fresh matchmaking, match seat LOST.Mobile-specific dominance: the race is won by whichever lands first — old-socket close-detection vs. new-connect+resume.
This matches operator's playtest finding "mobile reload" symptom + #145 (browser-restart resume UX) observation: the resume token persists (per #123 localStorage swap), but the server-side rejects-during-pre-detection-window means the resume doesn't take seat.
Critical insight — re #139 detection-coverage framing: detection isn't failing to FIRE (it does, eventually, via keepalive). And operator's "survivor doesn't see overlay on reload" is most likely the CORRECT desktop fast-recovery path (close-detected-first → brief opponentDisconnect→opponentReconnect flash → no lasting overlay needed) — NOT a detection failure. The "reliability gap" Herald flagged in c53f for #139 is actually a different substrate-axis: the gap is engine-side resume-handling, not detection-paths.
Severity: MED
Substantive substrate-gap. Rare on desktop (race window is microseconds). Common on mobile (race window is up to 25s = full keepalive interval). The browser-restart resume UX (#145) and #139's overall reliability story both depend on this fix.
Fix-direction: force-take-on-valid-token
Make
Resume()with a valid token DISPLACE a still-"active" seat instead of rejecting. The unguessable 128-bit token proves ownership; a resume inherently means "replacing the old conn" (last-resume-wins semantics).Cross-path care (the load-bearing subtlety): the displaced old conn's readPump later fires
Leave → handleMatchDisconnect(m, oldP), which would wrongly mark the now-active seat disconnected (usesoldP.idxunconditionally). Fix must neutralize that:oldP.match=nilso the old Leave handler no-ops (it gates onp.match!=nil)Without (1)+(2), the displaced conn's later Leave would cause server to think the now-active player is disconnected, triggering grace-pause + breaking the seat-take semantics.
Acceptance criteria
Resume()with valid token displaces still-"active" seat instead of rejectingoldP.match=nil+ socket teardownWhy a dedicated tracker (not folded into #139)
Cross-refs
Anchor
2026-06-24 Engineer page-reload-race source-probe per Bosun routing 9d85 → reframed #139 detection-coverage finding as resume-handling-race. Substrate-grounded probe distinguishes "detection fails to fire" from "valid resume incorrectly rejected during pre-detection window" — different substrate-axes, different fix-direction, different lane.