The backend stamps `seq` once per event before its transport fan-out, so the
same frame arriving on two of the renderer's sockets to one process carries
the same (session_id, seq); `JsonRpcGateway` is per socket and cannot see the
other copy. `JsonRpcGateway.dispatchEvent` now tags each event with the
`replay_epoch` its socket adopted from `gateway.ready` (per process, not per
socket), and both fan-ins in `use-gateway-boot.ts` (primary + registry
secondaries) pass through one `createGatewayEventDedupe()` gate keyed by
(epoch, session_id, seq) before any store runs.
A duplicate is the same key within 30 s. Not "seq not above the highest
seen": the backend restarts a session's counter at 1 when the session leaves
its 64-session replay ring, and a high-water mark would swallow the whole
restart of any short session. Seq-less / session-less events always pass.
Bounded: 2048 seqs per session, 256 sessions LRU.
Live, with the router fix reverted so two sockets still join the chat: the
DOM never doubled a word (0/38 samples vs 4/40 on main) and the doubled
"reply was cut short" card is back to one.
Closes#120007. Part of #120005.