# Multi-pane chat: where it stands, and the one open bug Written 2026-08-10 at the end of a long session, so the next one starts from evidence instead of re-deriving it. ## The open bug **A pane pointed at a REMOTE server shows `Disconnected` and never gets a reply.** The pane opens, lists and reads that server's conversations fine (HTTP with the API key works). Sending produces nothing. Console, repeatedly: ``` WebSocket connection to 'wss://officer.pastilhas.dev/api/chat/ws?token=ofk_…' failed: WebSocket is closed before the connection is established. ``` The same message also appears for the LOCAL pane's `ws://localhost:9010/...`, yet the local pane works and shows connected. So the message alone is not the bug — something closes the remote one and it never comes back. ## Ruled out, each by direct test — do not re-test these - **The server.** `wss://officer.pastilhas.dev/api/chat/ws?token=` opens on the first try from outside the browser. Tested twice: with no `Origin` header, and with `Origin: http://localhost:9010` (what the browser sends). Both `OPEN`. - **The key.** Same key, works over HTTP for `/chat/sessions`, and opens the socket above. - **Websocket auth not understanding API keys.** `upgradeWs` in `server.tsx` uses `resolveAuthToken`, the same resolver as the HTTP doors, and comments say so explicitly. `chat` is an `execution` capability and the owner passes `isWsProviderAllowed`. - **Officer being stale on alpha.** Pulled and restarted; the failure persists. - **Send being dropped.** Fixed in `243bd04` — `useChatWebSocket.send` used to `return` silently when the socket was not `OPEN`. It now queues and flushes on open. That fix is real and worth keeping, but it did not resolve this: a socket that never opens never flushes. ## Where to look next `src/workspaces/hooks/src/useChatWebSocket.ts`, and specifically what happens with SEVERAL instances mounted at once — one per pane. The handlers look individually correct: `close` returns early when `socketRef.current !== socket`, the effect depends on `[url]` alone, and `isCleaningUpRef` is reset on mount. What has NOT been established is how those interact across three simultaneous instances plus React's dev StrictMode double-invoke, which creates, closes and recreates every socket on mount. Concrete things to try, cheapest first: 1. **Instrument before theorising.** Log `url`, `readyState` and instance identity on every create, open, close and retry. The console message says a close arrived during CONNECTING; it does not say who called it. That is the whole question and it is one log line away. 2. **Check whether the effect re-runs.** `url` is a string, so it should be stable — but `chatSocketUrl(serverId, localStorage.getItem('BEARER_TOKEN'))` is recomputed every render, and if anything makes `serverId` flip (say, a pane re-resolving its target) the URL changes and the socket is torn down mid-handshake, forever. 3. **Suspect the transcript resolver in `ChatPane`.** It calls `onTargetChange` on success, which rewrites the tab state, which produces a new `target` object for every pane in that tab. If that ever loops, every pane's socket is rebuilt on every pass and none survives the handshake. 4. **Try one pane pointed at the remote server, alone.** If it connects, the bug is about multiplicity rather than about the remote server, which would be the single most useful fact to have. ## What works, so it is not re-litigated - Adding a second server by URL + `ofk_` key, verified against `/api/auth/me` before storing. - Per-pane server chips; the list, the directory picker and the transcript all follow the pane's server. - Tabs, splitting to three panes, closing, renaming a tab, and the tab name winning the page title. - Reading a remote conversation end to end. ## Setup facts worth not rediscovering - Andre's Mac key lives in agent memory. Alpha needs its own; the one in the console above is alpha's. - `bun dev` on the Mac serves a STALE bundle whenever a new file is added to a workspace package — the symptom is `X is not a function` for an export that plainly exists. Restart it; do not debug it. - The Mac is `https://macbook.pastilhas.dev` through NPM on alpha (`100.64.0.1` → Mac `100.64.0.9:9010`), and the cert is valid under strict TLS.