diff --git a/src/workspaces/hooks/src/useChatWebSocket.ts b/src/workspaces/hooks/src/useChatWebSocket.ts index e59f955b..7f1104b0 100644 --- a/src/workspaces/hooks/src/useChatWebSocket.ts +++ b/src/workspaces/hooks/src/useChatWebSocket.ts @@ -24,7 +24,16 @@ export const useChatWebSocket = ({ url, onMessage, onOpen }: UseChatWebSocketPar const connect = () => { if (isCleaningUpRef.current) return; - if (socketRef.current && socketRef.current.readyState === WebSocket.OPEN) return; + // CONNECTING counts as ours, not just OPEN. The deferred teardown below keeps a remount's socket + // alive mid-handshake, and this is what reclaims it: checking only OPEN meant an effect re-run + // built a SECOND socket and orphaned the first, which then stayed open forever with its own `open` + // handler bailing on the socketRef mismatch. + // + // This is per-instance and does NOT explain the two sockets a /chat/new load opens — measured with + // a WebSocket-constructor counter, those come from two separate `useChat` instances mounting, each + // with its own refs. Unresolved, and tracked separately; both connect, so it reads as healthy. + const existing = socketRef.current; + if (existing && (existing.readyState === WebSocket.OPEN || existing.readyState === WebSocket.CONNECTING)) return; const socket = new WebSocket(url); socketRef.current = socket;