Repository navigation
Description
What happened
My iPhone runs the SwiftUI TestFlight app and reaches my Windows desktop host three ways: home LAN, Tailscale, and T3 Connect. While trying to recover from an earlier disconnect, I used Settings → Connections → Revoke others on the desktop. That revoked the phone's direct pairing, which is fair enough.
The app never told me to pair again. At times the thread list showed the host as offline (red crossed-out Wi-Fi icon and red host name, while iOS itself showed Wi-Fi connected), and the connection screens said disconnected or reconnecting. Signing out of T3 Connect and back in brought the Connect route back. But the app still keeps trying the revoked pairing over the LAN about every 20 seconds, more than seven hours after the revoke.
Diagnosis
Host records (desktop 0.0.46-nightly.20261009.2873):
auth_sessions: the phone's only direct (bearer) pairing was revoked at 2026-10-09T12:01:58Z by a Revoke others batch. No direct session has been issued to the phone since. New T3 Connect sessions were issued at 18:32Z and 19:31Z.server.trace.ndjson, 2026-10-09 19:04–19:35Z, user agentT3Code/58: over the LAN route the app discovers the host fine, then gets 401 on every websocket ticket request. Over the same 30 minutes the T3 Connect route works normally. The host is healthy; only the revoked direct credential fails. (An earlier window, 18:18–18:47Z, had 73 such 401s, 67 onwebsocket-ticketand 6 on/api/orchestration/shell, before the new Connect session existed.)
Client source (read at t3code/rebuild-mobile-app-swift @ 52912fe7e3; TestFlight build 58 may differ slightly, so line numbers are approximate):
- The "Pair again" state (
needsPairingDetail: "This computer no longer accepts the saved pairing. Pair again.") is gated on the environment having a single route:NativeFeatureClient.swift~L317:if activeLoad?.credentialRejected == true, environment.routes.count == 1- ~L5936:
load.credentialRejected && load.environment.kind != .managedDPoP && load.environment.routes.count == 1; otherwise the state falls through to.disconnectedwith "That server is currently unreachable."
- With more than one route,
recoverEnvironmentRoute(~L10213) re-resolves routes every 15 s.routeRejectionIsFinal(~L10206) only becomes true when that recovery is itself rejected. When another route still works (here T3 Connect), the rejection is never final. The revoked direct route is retried indefinitely, with no prompt and no back-off.
So with several routes a revoked pairing shows up in one of two ways. If no other route is authorised, the host is reported as unreachable. If another route works, the app keeps hammering the dead route in the background. In neither case does it ask the user to pair again.
Steps to reproduce
Not yet run deliberately; this is the sequence from the host records.
- Pair the SwiftUI app with a desktop host directly (LAN and/or Tailscale) and also sign in to T3 Connect, so the environment has several routes.
- On the desktop, open Settings → Connections → Revoke others.
- Reopen the SwiftUI app. While Connect is not yet authorised, the host shows as offline/disconnected rather than "Pair again".
- Sign in to T3 Connect again. The app works through Connect, but the host trace shows
POST /api/auth/websocket-ticket→ 401 on the direct route roughly every 20 s, indefinitely.
Expected: a "Pair again" prompt for the revoked direct route, and retries on that route stop or back off.
Version
SwiftUI TestFlight 0.1.0 (58); desktop 0.0.46-nightly.20261009.2873 (ec80933ac8)
Environment
iPhone, iOS 27; Windows 11 desktop host; routes: LAN, Tailscale Serve HTTPS, T3 Connect
Evidence
# server.trace.ndjson, 2026-10-09 19:04–19:35Z, user agent T3Code/58
LAN route GET /.well-known/t3/environment 200 x69
LAN route POST /api/auth/websocket-ticket 401 x68 (median interval 22.8 s)
Connect POST /api/auth/websocket-ticket 200 x38
Connect GET /ws 204 x38
Connect GET /api/orchestration/shell 200 x26 (0.46-3.56 s)
# auth_sessions (phone, user agent T3Code/58)
direct bearer pairing revoked 2026-10-09T12:01:58Z (Revoke others) no replacement issued
T3 Connect sessions issued 2026-10-09T18:32:22Z, 19:31:42Z workingRelated issues
- [Bug]: After re-pairing, an environment's other routes keep the expired login, and removing a route deletes the new one #17014: the same pattern in the React Native app and
client-runtime(other routes keep a dead login after re-pairing). This report is the SwiftUI app, which is a separate codebase, so a fix there won't cover it. - [Bug]: SwiftUI app stays stuck syncing/reconnecting until the other T3 app's device entry on the same iPhone is removed #13994: my earlier SwiftUI reconnect report. That one involved valid sessions and a slow host; this one is a revoked credential, so I've kept them separate.
- [Bug]: T3 Connect reports "Your cloud sign-in changed" when the host is just overloaded #11004: the opposite mislabel (a slow host reported as a sign-in problem). Both are about the app showing the real reason for a failed connection.
Fix applied or workaround
Nothing changed on the machine. Likely workaround: pair the phone again over the direct route, or remove and re-add the environment (the workaround confirmed in #17014).
Update, 2026-10-10: re-pairing on the LAN did not fix the Tailscale route
A few minutes after filing this, the phone was paired again over the home LAN (new direct session issued 2026-10-09T19:53:57Z, still valid). That fixed the app at home. The next morning, away from home on mobile data, the host was unusable again. Desktop is now 0.0.46-nightly.20261009.2886. Times below are UTC; the phone was on UTC+11.
What the app showed (23:57Z): the thread list header said "JAFFA-11 offline" with the crossed-out icon, and threads on that host showed its name in red. The phone was on mobile data and, as the host logs show, reaching the desktop over Tailscale.
What the host saw (server.trace.ndjson, user agent T3Code/58, 23:48Z to 00:08Z):
- The phone reached the host the whole time, over both the Tailscale Serve hostname and the raw tailnet IP.
GET /.well-known/t3/environmentandGET /api/auth/sessionreturned 200 in 1-4 ms, repeatedly. - At 23:58:14 to 23:58:29 it sent eight
POST /api/auth/websocket-ticketrequests over the Tailscale hostname, with growing gaps (0.4 s up to 6 s). All eight returned 401 withSessionTokenRevokedError. - After those eight, it made no further websocket-ticket requests until the new pairing 12 minutes later, and only kept polling discovery and session.
- No T3 Connect requests appear in that window. The phone's last Connect session (issued 19:31Z) had expired at 20:31Z.
The host logs don't record which credential was sent. Because the LAN pairing was valid at the time, the likeliest explanation is that the Tailscale routes were still using the credential revoked on 2026-10-09 and the LAN re-pair didn't update them. That would be the #17014 pattern (other routes keep a dead login after re-pairing) in the SwiftUI app. Throughout, the app showed the host as offline rather than asking to pair again.
Recovery:
- At 00:03Z I created a fresh pairing for the Tailscale address on the desktop and entered it in the app's Add environment form.
- 00:10:39Z:
POST /oauth/tokenover Tailscale returned 200, and the host issued a new direct session. - 00:10:54Z: a second
POST /oauth/tokenreturned 401, consistent with the single-use code having already been redeemed. - The form showed: "This iPhone could not reach the server. Check the address and network, then try again." The host was reachable throughout. I can't tell which of the two attempts this message followed, but neither was a network failure.
- 00:11:00Z: the app connected over Tailscale anyway (websocket-ticket 200,
/api/orchestration/shell200 in 2.7 s), with the new session in use. - 00:11:04Z: the app also got a new T3 Connect token and connected through Connect. I had switched T3 Connect off and on in the app earlier and it hadn't reconnected then. From the host side I can't tell whether the toggle or the new pairing triggered it.
What this adds to the report:
- Pairing again on one direct route doesn't appear to refresh the credential the other direct routes use. Re-pairing on the LAN left the Tailscale routes failing. The workaround above only fixes the route it was done on.
- When the only usable routes reject the credential as revoked, the app reports the host as offline even though it is answering discovery requests within milliseconds. It never shows "Pair again".
- The Add environment form says the server couldn't be reached when the code exchange actually succeeded, or failed because the code had already been used.
Filed by
Initial investigation: OpenAI Codex (gpt-6.1-sol) via t3 triage. Host-record checks, Swift source reading and this write-up: Claude (Opus 5.5) in T3 Code. 2026-10-10 update: host-log extraction by OpenAI Codex (gpt-6.1-sol); checks and write-up by Claude (Opus 5.5) in T3 Code.
— Geoff. I'm not a developer; AI agents gathered this evidence from my machine and wrote it up, so please flag anything that doesn't add up.
Activity
juliusmarminge commentedon Oct 22, 2026
Classified as a connection-recovery bug; no exploitable vulnerability is reported. Checked open issue titles through the last 90 days: the React Native report #17014 is a different client, and #13994 concerns valid sessions, so neither is a duplicate.
Read the SwiftUI README and NativeFeatureClient.swift at cached branch commit 4619c0bca6. reconcileEnvironmentLoads still gates needsPairing on routes.count == 1; routeRejectionIsFinal and recoverEnvironmentRoute also control rejected-credential recovery across routes. This is source inspection, not a runtime reproduction. Fetching current source failed with an HTTPS transfer timeout.
The triage hard stop prohibits changes to authentication or credential handling. Handing this to Julius for that review; no fix was written. SwiftUI device behavior remains unverified on this Linux host.