Before submitting
Closest match is #4671 (closed), which reported the same symptom for a nightly desktop + older Android app. This is the same failure mode, but now it happens with the public Play Store app against released stable 0.0.31, i.e. the default path a new user takes.
Area
apps/server
(The WS/RPC boundary is in the server; the mismatched side is most likely the published mobile binary. See analysis.)
Steps to reproduce
- macOS, T3 Code desktop
0.0.31 running. Its embedded server listens on *:3773 (all interfaces).
- Install the T3 Code Android app from the Play Store (build available 2026-07-29).
- Put the phone on the same Wi-Fi as the Mac.
- Mint a pairing token on the Mac:
t3 auth pairing create --ttl 45m --base-url http://<LAN-IP>:3773
- In the app: Add Environment → HOST
http://<LAN-IP>:3773, plus the pairing code.
Expected behavior
The environment connects and stays connected.
If the client's RPC protocol version is not supported by the server, the handshake should fail with an explicit, actionable error (e.g. "client too old, update the app") instead of closing the socket silently and retrying forever.
Actual behavior
The environment is added successfully and displays the correct label pulled from /.well-known/t3/environment — then immediately enters:
Failed to connect. Reconnecting… Reason: <label> connection failed unexpectedly
…and retries indefinitely.
What actually happens on the wire, per server traces:
| Stage |
Result |
GET /.well-known/t3/environment |
✅ 200 |
POST /oauth/token (token exchange) |
✅ 200, bearer session created |
POST /api/auth/websocket-ticket |
✅ 200 |
GET /ws upgrade + authenticateWebSocketUpgrade |
✅ Success |
SessionStore.markConnected |
✅ Success |
ws.rpc.server.getConfig |
✅ Success (~40 ms) |
| Socket survival after that first RPC |
❌ closes 75–178 ms after upgrade |
Exactly one RPC is ever dispatched (server.getConfig). It succeeds. Then the socket dies. There is no server-side error in the trace — the only failures are benign CommandResolutionErrors from externalLauncher.resolveAvailableEditors probing editors that aren't installed.
Control test: a plain Node ws client from the same LAN, using a ticket issued the same way and mimicking the app's headers (Origin, User-Agent: okhttp/4.9.2, permessage-deflate), upgrades with 101 and holds the socket open indefinitely (verified 8 s, closed by me). So the server's upgrade path, deflate negotiation and auth are all healthy; the failure is specific to the app's RPC conversation.
Note for triagers: /ws spans record http.response.status_code: 204. That is not a symptom — my healthy 8-second control connection also logged 204 (it's the placeholder response for an upgraded socket). The only distinguishing signal is span duration.
Analysis: likely Effect RPC protocol skew
Per @juliusmarminge in #4671, #4643 ("Upgrade Effect and Alchemy betas", merged 2026-07-27T14:21:03Z) contained a breaking change in Effect's RPC protocol, and both sides must be updated together.
I pinned the exact publish boundary on npm:
| Version |
Published |
effect |
0.0.29-nightly.20260727.919 |
2026-07-27T13:25:55Z |
4.0.0-beta.78 |
0.0.29-nightly.20260727.920 |
2026-07-27T16:10:37Z |
4.0.0-beta.102 |
Every server a user can currently install is on the new side of that break:
t3@0.0.31 (latest, published 2026-07-29T20:53Z) → effect 4.0.0-beta.102
t3@nightly = 0.0.32-nightly.20260729.951 → effect 4.0.0-beta.102
- T3 Code desktop (Alpha)
0.0.31 → bundle strings reference 4.0.0-beta.102 (185×), plus a few 4.0.0-beta.103 (8×)
Since there is no installable server on the old side and no newer one to move to, the remaining suspect is the published mobile binary being compiled before #4643 — which would make this #4671 with the roles reversed, and would explain a connection that authenticates, serves one RPC, then drops.
I want to be explicit that I could not verify the app's bundled effect version — that part is inference, not measurement. Everything in the tables above is measured. Happy to run anything specific if it helps confirm.
Impact
Blocks work completely — the mobile app cannot connect to any currently installable server build.
Version or commit
- Server:
t3@0.0.31, and T3 Code desktop (Alpha) 0.0.31 (the build actually serving, on *:3773)
- Android app: Play Store build as of 2026-07-29
- I did not runtime-test
0.0.32-nightly.20260729.951; I only confirmed it declares the same effect 4.0.0-beta.102.
Environment
macOS (Darwin 25.5.0, arm64) · T3 Code (Alpha) 0.0.31 embedded server on *:3773 · CLI t3 0.0.31 on Node 22.22.3 · Android client okhttp/4.9.2 · direct LAN pairing, no relay involved.
Logs or stack traces
# phone's retry loop — every HTTP step succeeds (server.trace.ndjson)
18:51:27 GET /.well-known/t3/environment -> 200 Success
18:51:27 POST /oauth/token -> 200 Success
18:51:27 POST /api/auth/websocket-ticket -> 200 Success
18:51:28 GET /.well-known/t3/environment -> 200 Success
18:51:28 POST /api/auth/websocket-ticket -> 200 Success
... repeats with backoff indefinitely ...
# the /ws upgrade itself — well-formed request, auth OK, socket dies anyway
name: http.server GET
attributes:
http.route: /ws
http.request.header.upgrade: websocket
http.request.header.connection: Upgrade
http.request.header.sec-websocket-version: "13"
http.request.header.sec-websocket-extensions: permessage-deflate
http.request.header.origin: http://<LAN-IP>:3773
user_agent.original: okhttp/4.9.2
http.response.status_code: 204
durationMs: 99.647791
exit: Success
# child spans of that same trace: one RPC, succeeds, then the socket closes
EnvironmentAuth.authenticateWebSocketUpgrade Success
SessionStore.verifyWebSocketToken Success
SessionStore.markConnected Success
ws.rpc.server.getConfig Success (40.4 ms)
# (the remaining 292 spans are shell.isExecutableFile from editor discovery)
# control: Node `ws` client, same LAN, same headers, same ticket flow
+18ms UPGRADE http 101 ext="permessage-deflate"
+20ms OPEN
+8006ms -- STILL OPEN after 8s: socket HEALTHY --
Workaround
Use the web client served by the same server from the phone's browser instead of the native app:
http://<LAN-IP>:3773/pair#token=<pairing-token>
Because that client ships with the server, its RPC protocol matches by construction. It works fine on Android Chrome and can be added to the home screen. This is a usable stopgap for anyone arriving from the launch announcement and hitting this.
Before submitting
Closest match is #4671 (closed), which reported the same symptom for a nightly desktop + older Android app. This is the same failure mode, but now it happens with the public Play Store app against released stable
0.0.31, i.e. the default path a new user takes.Area
apps/server
(The WS/RPC boundary is in the server; the mismatched side is most likely the published mobile binary. See analysis.)
Steps to reproduce
0.0.31running. Its embedded server listens on*:3773(all interfaces).t3 auth pairing create --ttl 45m --base-url http://<LAN-IP>:3773http://<LAN-IP>:3773, plus the pairing code.Expected behavior
The environment connects and stays connected.
If the client's RPC protocol version is not supported by the server, the handshake should fail with an explicit, actionable error (e.g. "client too old, update the app") instead of closing the socket silently and retrying forever.
Actual behavior
The environment is added successfully and displays the correct label pulled from
/.well-known/t3/environment— then immediately enters:…and retries indefinitely.
What actually happens on the wire, per server traces:
GET /.well-known/t3/environmentPOST /oauth/token(token exchange)POST /api/auth/websocket-ticketGET /wsupgrade +authenticateWebSocketUpgradeSessionStore.markConnectedws.rpc.server.getConfigExactly one RPC is ever dispatched (
server.getConfig). It succeeds. Then the socket dies. There is no server-side error in the trace — the only failures are benignCommandResolutionErrors fromexternalLauncher.resolveAvailableEditorsprobing editors that aren't installed.Control test: a plain Node
wsclient from the same LAN, using a ticket issued the same way and mimicking the app's headers (Origin,User-Agent: okhttp/4.9.2,permessage-deflate), upgrades with 101 and holds the socket open indefinitely (verified 8 s, closed by me). So the server's upgrade path, deflate negotiation and auth are all healthy; the failure is specific to the app's RPC conversation.Note for triagers:
/wsspans recordhttp.response.status_code: 204. That is not a symptom — my healthy 8-second control connection also logged 204 (it's the placeholder response for an upgraded socket). The only distinguishing signal is span duration.Analysis: likely Effect RPC protocol skew
Per @juliusmarminge in #4671, #4643 ("Upgrade Effect and Alchemy betas", merged
2026-07-27T14:21:03Z) contained a breaking change in Effect's RPC protocol, and both sides must be updated together.I pinned the exact publish boundary on npm:
effect0.0.29-nightly.20260727.9194.0.0-beta.780.0.29-nightly.20260727.9204.0.0-beta.102Every server a user can currently install is on the new side of that break:
t3@0.0.31(latest, published 2026-07-29T20:53Z) →effect 4.0.0-beta.102t3@nightly=0.0.32-nightly.20260729.951→effect 4.0.0-beta.1020.0.31→ bundle strings reference4.0.0-beta.102(185×), plus a few4.0.0-beta.103(8×)Since there is no installable server on the old side and no newer one to move to, the remaining suspect is the published mobile binary being compiled before #4643 — which would make this #4671 with the roles reversed, and would explain a connection that authenticates, serves one RPC, then drops.
I want to be explicit that I could not verify the app's bundled
effectversion — that part is inference, not measurement. Everything in the tables above is measured. Happy to run anything specific if it helps confirm.Impact
Blocks work completely — the mobile app cannot connect to any currently installable server build.
Version or commit
t3@0.0.31, and T3 Code desktop (Alpha)0.0.31(the build actually serving, on*:3773)0.0.32-nightly.20260729.951; I only confirmed it declares the sameeffect 4.0.0-beta.102.Environment
macOS (Darwin 25.5.0, arm64) · T3 Code (Alpha) 0.0.31 embedded server on
*:3773· CLIt30.0.31 on Node 22.22.3 · Android clientokhttp/4.9.2· direct LAN pairing, no relay involved.Logs or stack traces
Workaround
Use the web client served by the same server from the phone's browser instead of the native app:
Because that client ships with the server, its RPC protocol matches by construction. It works fine on Android Chrome and can be added to the home screen. This is a usable stopgap for anyone arriving from the launch announcement and hitting this.