"A -joinlocal peer joins whoever owns 127.0.0.1:55333, not whoever is in play mode"
▸ SYMPTOM
You launch a -joinlocal second instance to run a two-peer test. You made sure no other s&box editor on the machine was in play mode, so you expect the peer to join your session. It joins a different editor's session instead, and you only notice because the peer's log carries another project's tags, not yours.
A sibling editor can sit in play mode for over an hour and never take your peer, while a second editor that is not visibly "hosting" grabs it. Play-mode state does not predict where the peer lands.
▸ CAUSE
-joinlocal runs connect local at bootstrap. That is a TCP connect to the loopback port 127.0.0.1:55333. Only one process on a machine can hold a listening port, so the peer joins whichever process holds 55333, and nothing else.
The process that holds 55333 is the editor whose game code called Networking.CreateLobby. A sibling editor in play mode that never called CreateLobby does not hold the port. So "no other editor in play mode" is a proxy for the real question and it can be wrong in both directions: a merely-playing sibling is harmless, and a lobby-hosting sibling is a hard conflict even if you did not think of it as hosting.
▸ FIX
Check socket ownership, not play mode:
- Bring up your own host leg first. Before your code calls
CreateLobby, the port owner is nobody, which proves nothing and reads like a pass. - Resolve the owner of
127.0.0.1:55333after your host leg is live. On Windows:Get-NetTCPConnection -State Listen -LocalPort 55333returns one row whoseOwningProcessis the owner's pid. - Read that pid's command line. Every editor runs as
sbox-dev.exe, so the image name tells you nothing. The command line names the project root, and that is what you compare. - Require the owner to be your worktree. If a different worktree owns the socket, refuse the run.
Keep a play-mode census as information, not as the gate. It is the list you want when the socket check fails and you need to find the offender. The socket-owner check is the refusal.
▸ WHY IT WORKS
The join target is decided by one fact: who holds the listening port. A TCP connect to 55333 reaches exactly one process, so reading that port's owner is a decisive test rather than a heuristic. The play-mode census answers a related but different question, which is why it lost runs to a sibling editor twice before the socket check replaced it. This narrows the older, stricter precondition ("no other editor may be in play mode at all") without weakening it: a sibling that owns the socket is still a hard refusal, and the narrowing only stops a merely-playing sibling from blocking a run that was never at risk.
- Published