fix(web): SSH environments on an older reused remote server explain the version mismatch instead of reconnecting forever#23647

merged3
main
fix/issue-16984
opened2d agobyJJulius Marminge
View on GitHubTwo-way sync enabledPR last imported 21h ago

Problem

After a desktop update, an SSH environment can stay on " is reconnecting" forever. When the remote host already has a T3 server running that the desktop did not start, the launcher adopts it whatever its version. The desktop's newly installed runner then mints the pairing token into the shared database. An older server cannot decode the newer scopes in that grant, so POST /oauth/token returns 500 access_token_issuance_failed on every attempt. The client treated that as a transient failure and kept retrying without saying why.

Repro with the real Linux release builds: start t3 v0.0.46-nightly.20261004.2652 serve --base-dir <home>. Mint a token against the same home with the 20261007.2787 CLI (auth pairing create --json, 13 scopes). Exchanging it returns HTTP 500. A token minted by the running Oct 4 build exchanges fine.

Change

In provisionDesktopSshEnvironment, a bearer-bootstrap failure becomes a blocked connection with an explanation when both of these hold:

  • the desktop IPC reports [ssh_http:500];
  • the remote descriptor's serverVersion differs from this client's version.

Blocked means no backoff loop. The message names both versions and says to restart the remote T3 server on the desktop's version, then retry. A blocked connection retries on the next wake signal, so it reconnects once the server has been restarted, without re-enabling the environment. Every other bootstrap failure behaves as before: matching versions, failures with no status or a non-500 status, and cancellation. A one-line comment on the desktop error notes that the renderer matches that prefix.

Scope and approval

Fixes https://github.com/pingdotgg/t3code/issues/16984 GitCafe: https://git.cafe/pingdotgg/t3code/issues/16984 Triaged bug via the automated issue flow.

Verification

  • vp test run apps/web/src/connection/platform.test.ts: 16 passed.
    • New: blocks with both versions when a server on another build rejects pairing fails on main. It gets ConnectionTransientError with Could not prepare the SSH environment: [ssh_http:500] … instead of ConnectionBlockedError.
    • New: keeps retrying pairing failures that are not a version mismatch covers a matching version with a 500, and differing versions with no HTTP status.
  • vp run check:changed: format, lint, knip, typecheck (2 packages) and tests all pass.
  • Server-side runtime transcript with the real release archives (linux-x64), access token redacted:
text
1$ curl -s 127.0.0.1:38917/.well-known/t3/environment | jq -r .serverVersion # running server (Oct 4)
20.0.46-nightly.20261004.2652
3$ t3-20261007 auth pairing create --base-dir <same home> --json # what the updated desktop's runner does
413 scopes
5$ POST /oauth/token (token-exchange) with that credential
6{"_tag":"EnvironmentInternalError","code":"internal_error","reason":"access_token_issuance_failed",...} HTTP 500
7$ t3-20261004 auth pairing create ... ; POST /oauth/token # control: token minted by the running build
8{"access_token":"<redacted>","issued_token_type":"urn:ietf:params:oauth:token-type:access_token","token_type":"Bearer",...}

Not checked: there is no before/after capture of the desktop connection banner over a real SSH session. Reproducing it end to end needs an SSH account whose ~/.t3 is not this machine's live T3 install, and none was available here. The changed decision is covered by the tests above, and the 500 by the transcript.

🤖 Generated with Claude Code (Claude Opus 5.5 in T3 Code; reviews by Claude Opus 5.5 and Claude Sonnet 5.5)

Takeover

No code changes; evidence and verification added below.

Evidence

Rendering: Linux Electron desktop (vp run dev:desktop, Electron 44.9.3) on Xvfb 1440×900×24, captured with ImageMagick import/ffmpeg x11grab. Before = merge-base f054d1b801, after = head 31019fac0f. Both sides run the same seeded statev2.sqlite (sha256 9657a7f8…). The remote is a real SSH host (a private sshd on 127.0.0.1:2222 with its own HOME) running the released t3 0.0.46-nightly.20261004.2652 from npm as an already-running server. The desktop's own runner mints the pairing token, as in the issue. Driver scripts: driver.tgz (sha256 356d5c4b…9618). Fixture matrix: matrix.md.

RowStateCaptured
R1aAdd environment over SSH, remote on another build answers 500before/after, 1440+390, light+dark
R1bSaved SSH environment, remote swapped to the older buildbefore recording + stills; after stills (banner)
R5Remote restarted on the matching build, then Retry nowbefore recording only
R2, R3aSame version + 500; different version + 502not captured
R3bDifferent version, failure with no HTTP statusafter log/steps only (ensure timeouts keep retrying)

R1a: Add environment dialog

R1b: composer banner on a thread in the SSH environment's project. Before: "fs-29 is reconnecting", retrying forever. After: "fs-29 is offline" with Reconnect.

Recordings and steps (before):

What else changed between the before and after captures: nothing outside the error text and the banner's reconnecting→offline state. A provider-update toast appears on both sides and was dismissed before each still.

Round 5 completion — supersedes the round-1 missing cells and “Unverified” list below. Head 31019fac0f; merge-base re-derived as f054d1b801. One installed worktree, fresh synthetic state, Linux Electron/Xvfb. Real old released server for R1a/R1b/R5; deterministic HTTP fixture behind real SSH for controls. No new tests or reviews.

Verification log
  • Reviews (one round, head 31019fac0f): Claude Sonnet 5.5 → MERGE; Claude Opus 5.5 → MERGE. Neither found a bug or any change to what pairing or auth accepts. Both confirmed that blocked waits for a wake signal or an explicit retry, then re-pairs.

  • Real-runtime check at head: the Add flow against the 20261004 build returns [ssh_http:500] from bootstrap-ssh-bearer-session, and the dialog shows the version message. The merge-base shows the raw [ssh_http:500] error.

  • In the after R1b run, the explanation appears only after the attempt that reaches the 500. Earlier attempts hit unrelated, status-less ensure failures (remote swap race; SSH timeouts under host load), and those correctly kept retrying (steps).

  • Unverified: a clean after recording for R1b and R5; an after Settings-row still showing the blocked explanation; R2 (same version + 500); R3a (502); diff images; proof audit pass 2. Reason: the run went over its time budget while the shared VM was saturated by an unrelated Android build.

  • Model: Claude Opus 5.5 in T3 Code (lead, audit); reviews by Claude Opus 5.5 and Claude Sonnet 5.5.

  • Round 5 drivers: drivers-r5.tgz (archive sha256 2a16268b4c3402efc1009e3eb7c5c62400f7121ecbb6c291750e151c0fd2a575); metadata-only HTTP transcript, full decode checks, diff metrics, capture hashes, source hashes and PID/cleanup log: verification-r5.log. Each round-5 driver sha256: setup.py = bc7ee115081f9aacc24a415c051bb475ad4baf60c8270910cb431b204f1e3e8e; launch.sh = 14eb96a71a05023c3e3148957e7e5d7659ec0ff2ac19adec62fcd8f2ff2f45d1; record-r1b.sh = 0959dd3b70c1f578bb7218347f0013cda75fab7abeac283949c755c693e23aa3; record-r5.sh = 956fd734059e29e207bdc261bafcb49231ee9e1b14068abb501941c3882308ee; controls.sh = 32f8b2e62066891eb2313db3c94813112d0f2c01e9feea6e129da7df8fcfa675; controls-R2-after-captured.sh = e10ec48ece4663db3bea7fcdb789bf897b6dd44900b8567e9d0d1b4f926a5481; driver/cdp.mjs = ff970c2eb8aa798e91866ef8109feb36dd6fb5cf7adf3802623188d90ad1a31b; fake-remote.mjs = c3a93750bf3761a5e2359357160a1e4ea4e9062c0181a9b9778642c062ffc679; diff.py = ccb92aefe257ac91e6391e77052204dcb9947f206df5804e040ceb2d23a3028c; pids.py = 57b6e806f79f26331ec044539b3be40a8dd1cd80b52cca2275227d051c465c02; stop-port.py = 6a788682a91f020362611d307f9c90a0c3d9840de760e7a03140e639e354c111; audit-artifacts.py = 8bf873593d2d2f4c462dd5833bdd9e42ca3d6cf49f3b00c46e224d8e7d4bfc17; inspect-videos.py = 5cff910bab2cfeb33e5f06620589dfe132d8b99d529ceda4b63d20a4536b6687; bin/ssh = d40a4f8c6a9e6a370263ff57ae316c2a081a54038bdd68920b5f4985b764e10d; bin/pkill = 470555b8831d1ba5cbc878fed047d5e31ee2330e43a643214ef58e2b2d214290; ssh/forced.sh = 27361afee4882d50b8ba4551fb9fb9665ca49f2a684ac8ee49252175b5d4e101.

  • Operational exceptions: initial automatic dev seeding began reading/copying the live database; stopped immediately, aborted copy deleted, then fresh state with T3CODE_DEV_SEED=0. An orphan recorder PID was recovered from a process listing before cwd-checked SIGINT, breaching the requested PID-discovery rule. No database or aborted-seed data is uploaded. Missing xdotool and libsecret-1-dev were installed; interrupted/corrupt capture attempts were discarded. Teardown: task servers, sshd, Xvfb and Electron stopped through retained sessions/recorded port-owner PIDs and cwd checks; created worktree removed. Disk stayed ≥10 GB during capture (start 12 GB rounded). Model/harness: GPT-6.1-sol, Codex in T3 Code.

Checks

All checks passed

Added 3 commits

  • chore(desktop): note the renderer matches the SSH status prefix31019fa
  • test(web): reproduce #16984 SSH pairing against a reused server on another build535de26
  • fix(web): SSH pairing against a reused older server explains the version mismatchdeb1b67
Synced from GitHub

Approvability

Verdict: Needs Human Approval

Macroscope's review found this PR requires human approval — This is a narrowly scoped SSH version-mismatch bug fix with tests, changing only the handling of a specific bearer-pairing failure while leaving other retries unchanged. Because it changes authentication/pairing failure behavior on a production connection path, it warrants human review.

You can add or adjust custom eligibility rules. Learn more.

Synced from GitHub

Review in Change Stack →

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration
📥 Commits

Reviewing files that changed from the base of the PR and between f054d1b8018721bd997d99cbb918a7b17bf098a4 and 31019fac0f9d55584cfae7e66c6b13ab938ca451.

📒 Files selected for processing: 3
  • apps/desktop/src/ipc/methods/sshEnvironment.ts
  • apps/web/src/connection/platform.test.ts
  • apps/web/src/connection/platform.ts

Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 3 remain after this review.

📝 Walkthrough

The web client now identifies marked SSH bootstrap failures that occur when the remote server and desktop versions differ. It reports both versions and instructs the user to restart the remote server. Other failures retain the existing error mapping.

Changes: SSH pairing version mismatch

Layer / File(s)Summary
Detect and report version-mismatched SSH pairing failures
apps/web/src/connection/platform.ts, apps/web/src/connection/platform.test.ts, apps/desktop/src/ipc/methods/sshEnvironment.ts
The web client returns a configuration-blocked error when a bootstrap failure contains [ssh_http:500] and the remote server version differs from APP_VERSION. Tests cover matching versions and failures without the marker. A desktop comment documents the marker.

Priority: ➖ Normal

Estimated code review effort: 2 (Simple) | ~10 minutes

Change: Bug fix · Severity of issue fixed: Medium

Suggested reviewers: t3dotgg

Merge Risk: ⚪ Minimal · up to 31019

The change explains the known version-mismatch pairing failure. No concrete additional merge-blocking issue is established by the available evidence.

Pre-merge checks: Passed 4
Check nameStatusExplanation
Title checkPassedThe title clearly and concisely describes the main fix: explain SSH version mismatches instead of retrying indefinitely.
Linked Issues checkPassedIssue #16984 requires the SSH environment to reconnect, or T3 to explain the required action when an older remote server causes the pairing failure. The PR maps [ssh_http:500] plus a remote `serverV…
Out of Scope Changes checkPassedThe changes are limited to the SSH provisioning decision, regression tests, and a comment that documents the renderer's [ssh_http:500] dependency. These changes directly support issue #16984. No unr…
Description checkPassedThe pull request description follows the required structure. It explains the problem, change, scope, issue link, focused tests, runtime verification, UI evidence, limitations, and agent details. The s…
Full details: Linked Issues check

Explanation

Issue #16984 requires the SSH environment to reconnect, or T3 to explain the required action when an older remote server causes the pairing failure. The PR maps [ssh_http:500] plus a remote serverVersion mismatch to ConnectionBlockedError. The explanation names both versions and instructs the user to restart the remote T3 server on the desktop version and retry. The blocked state retries on the next wake signal without restoring the backoff loop. Tests cover the version-mismatch case and preserve transient retry behavior for matching versions and failures without the HTTP 500 marker. The reported changed-file checks passed.

Full details: Out of Scope Changes check

Explanation

The changes are limited to the SSH provisioning decision, regression tests, and a comment that documents the renderer's [ssh_http:500] dependency. These changes directly support issue #16984. No unrelated behavior or unrelated files are identified. The repository diff was unavailable, so this scope assessment relies on the supplied changed-file summary.

Full details: Description check

Explanation

The pull request description follows the required structure. It explains the problem, change, scope, issue link, focused tests, runtime verification, UI evidence, limitations, and agent details. The scope section identifies the triaged issue, although it does not include a specific maintainer approval comment.

✨ Finishing Touches
📝 Generate docstrings
  • Commit to this branch
  • Create a new PR
🧪 Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Comment @coderabbitai help to get the list of available commands.