A pane on a remote server reads fine and never connects its socket. Captured what has been ruled out by direct test — the server accepts that exact key over wss with and without a browser Origin, on the first try — so the next session does not re-derive any of it. The remaining question is client-side lifecycle with several sockets mounted at once, and the first move is instrumentation rather than theory: the console says a close arrived during CONNECTING and does not say who called it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
4.3 KiB
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=<the same ofk_ key>opens on the first try from outside the browser. Tested twice: with noOriginheader, and withOrigin: http://localhost:9010(what the browser sends). BothOPEN. - The key. Same key, works over HTTP for
/chat/sessions, and opens the socket above. - Websocket auth not understanding API keys.
upgradeWsinserver.tsxusesresolveAuthToken, the same resolver as the HTTP doors, and comments say so explicitly.chatis anexecutioncapability and the owner passesisWsProviderAllowed. - Officer being stale on alpha. Pulled and restarted; the failure persists.
- Send being dropped. Fixed in
243bd04—useChatWebSocket.sendused toreturnsilently when the socket was notOPEN. 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:
- Instrument before theorising. Log
url,readyStateand 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. - Check whether the effect re-runs.
urlis a string, so it should be stable — butchatSocketUrl(serverId, localStorage.getItem('BEARER_TOKEN'))is recomputed every render, and if anything makesserverIdflip (say, a pane re-resolving its target) the URL changes and the socket is torn down mid-handshake, forever. - Suspect the transcript resolver in
ChatPane. It callsonTargetChangeon success, which rewrites the tab state, which produces a newtargetobject for every pane in that tab. If that ever loops, every pane's socket is rebuilt on every pass and none survives the handshake. - 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/mebefore 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 devon the Mac serves a STALE bundle whenever a new file is added to a workspace package — the symptom isX is not a functionfor an export that plainly exists. Restart it; do not debug it.- The Mac is
https://macbook.pastilhas.devthrough NPM on alpha (100.64.0.1→ Mac100.64.0.9:9010), and the cert is valid under strict TLS.