[Bug]: Desktop update can't relaunch when an SSH client starts a server during the restart#22996

opened24d agobyAajbarryiii

Before submitting

  • I searched existing issues and did not find a duplicate.
  • I included enough detail to reproduce or investigate the problem.

Area

apps/desktop

Steps to reproduce

  1. On host A, run T3 Code desktop with an update downloaded and ready to install.
  2. On machine B, connect T3 Code to host A over SSH and leave it connected.
  3. On host A, install the update.
  4. While A's desktop is stopped for the update, B loses its tunnel and reconnects. Its launch script finds no server on A and starts t3 serve against ~/.t3.
  5. A's updated desktop relaunches.

This depends on timing. B has to reconnect during the gap, which was about 8 seconds here.

Expected behavior

The updated desktop relaunches and owns its home. B waits through the update and then reconnects to the desktop's new server.

Actual behavior

The SSH-launched server takes the home's ownership lock first. The relaunched desktop shows "This T3 home is unavailable" and quits. The only way out is to find and stop the SSH-launched server by hand.

The old backend did stop cleanly (1.95 s, well under the 5 s stop timeout), so this is not the slow-shutdown race that the open PRs #22572 and #22345 address.

Impact

Blocks work completely

Version or commit

Updating 0.1.0 → 0.1.1 on the host. The SSH-launched server was 0.1.0.

Environment

Host: macOS 27.0, Apple Silicon, desktop app, no background service. Remote client: T3 Code desktop 0.1.0 on NixOS 26.05 (Linux 6.18), connected over SSH via Tailscale.

Logs or stack traces

shell
1Host-local times (UTC-7). host = host's desktop/server traces and ~/.t3/ssh-launch state; client = remote client's own traces.
2
37:49:51.9 client EnvironmentSupervisor.connectionLost for the SSH environment
47:49:54 host updater finishes stopping the desktop backend (1.95 s)
57:49:56.8 client reconnect: readiness on the old tunnel fails, then
6 ssh/tunnel.launchOrReuseRemoteServer runs the launch script (8.4 s)
77:50:01 host new server.log under ~/.t3/ssh-launch/<key>/;
8 a 0.1.0 `t3 serve --base-dir ~/.t3` starts
97:50:02 host updated desktop backend refused: home already owned (onStateDirOwned)
107:50:04 host SSH-launched server reports ready (headless)
117:50:05.3 client launch script returns; tunnel to the new server opens

Screenshots, recordings, or supporting files

No response

Workaround

Stop the SSH-launched t3 serve process, then reopen T3 Code.

Relation to #21423

#21423 describes the same lock conflict after a normal quit or reboot. It was closed because the ownership lock is working as designed, and automatically attaching the desktop to an existing server is a feature request (#20250, and #8115 explains why that is a large design). I agree with both points. This report is narrower. During a desktop update, the desktop is guaranteed to come straight back, and the SSH launcher starts a competing server in that short window anyway.

Proposed fix (small, no lock or protocol changes)

  1. Updater: before stopping backends in installDownloadedUpdate, write a short-lived marker, runtime/desktop-update-in-progress, next to the existing desktop-update-restart marker. Remove it wherever that marker is removed (resetInstallAction, recoverFailedInstall). It is a separate file because desktop-update-restart is consumed during shutdown for the T3 Connect tunnel handoff.
  2. SSH launch script: at the point where it would start a new t3 serve because no server was found, check the marker. If it is under 1 minute old, exit with "T3 Code on this host is installing an update; the connection will retry shortly" and start nothing. The 1-minute limit matches DESKTOP_UPDATE_RESTART_MARKER_TTL.

Nothing else changes:

  • The client's connection supervisor already backs off and retries after a failed launch.
  • On a later retry, the launcher's existing branch for adopting a running default-home server picks up the relaunched desktop.
  • The ownership lock, normal quit, crash and reboot behavior, and the client protocol and UI are all untouched.
  • No process is stopped, and the marker expires by itself if the desktop never comes back.

Limitation: only clients running a version with the updated launch script are protected, because the script and the t3 it runs come from the client. Older clients keep today's behavior until they update.

Implementation ready, no PR opened

CONTRIBUTING.md asks that a substantial bug fix link an issue maintainers have triaged, so I haven't opened a PR. The fix is ready on a fork branch, with focused contract tests, in case it's useful: https://github.com/pingdotgg/t3code/compare/main...ajbarryiii:t3code:fix/defer-ssh-launch-during-desktop-update (one commit on current main, +216/−31 across 6 files, mostly tests). It was written by Claude Opus 5.5 in Claude Code and reviewed by GPT-6.1 Sol in Codex.

If this scope is acceptable, I'm happy to open it as a PR.

Synced from GitHub

I saw #23005 got opened for this. Thanks for addressing it so quickly!

On the request for logs: here are the host and client logs from the original occurrence, covering the restart gap: https://gist.github.com/ajbarryiii/7546bdde3a0feb81d4897b1b8ac90488. In short, the old backend stopped cleanly at 07:49:54.5. The remote client started its launch script at 07:49:56.8, and the SSH-launched server took the home before 07:50:02.9. The updated desktop was then refused with ServerAlreadyRunningError on that launch and on every relaunch until I stopped that server by hand.

It's hard to replicate on demand. An update only goes one way, so each attempt needs a new release to install. The remote client also has to reconnect inside the ~8 s restart gap. I'll try to replicate once #23005 lands in a nightly, with both machines on that nightly, and report back.

Synced from GitHub

Same failure here on 0.1.1 → 0.1.2. Host is macOS on Apple Silicon (desktop app, no background service). The client is T3 Code desktop 0.1.1 on Linux, connected over SSH via Tailscale.

Host-local times:

text
119:16:50 desktop: updates.install writes runtime/desktop-update-restart, backendInstance.stop (3.7 s)
219:16:59 SSH-launched 0.1.1 `t3 serve --host 127.0.0.1 --port 3773 --base-dir ~/.t3` starts (PPID 1, SSH_CLIENT = the Linux client)
319:16:59 updated 0.1.2 desktop: backend start -> ServerAlreadyRunningError, code=78
419:17:26, 19:19:44, ... every relaunch refused the same way

The gap was about 9 s. As in the original report, the only way out was to stop the SSH-launched server by hand. The client then relaunched another one within seconds, so I had to reopen the desktop immediately to win the race. This happens on every update for me, so I'd love to see #23005 land.

Synced from GitHub

Note

🤖 Claude Opus 5.5 responding on behalf of AJ (@ajbarryiii)

I opened #26191, which builds on #23005. It adds the fix for that PR's open audit finding and end-to-end evidence. The test used packaged AppImages in a VM, with a client connected over real SSH while the host installed a real update. With the host's restart slowed so the backend was gone for several seconds:

  • main: the updated desktop was locked out of its home in 6 of 8 attempts.
  • The fix: under the same conditions, the client waited and attached to the updated desktop in 8 of 8 attempts.

Details: https://gist.github.com/ajbarryiii/045721f97d50f99069a67882c4dbd0eb

Synced from GitHub

Note

Posted by Claude Code (Opus 5.5) on behalf of the reporter.

Another occurrence, on a current nightly: updating 0.1.6-nightly.20261031.3969 to .3977. The host is a Mac on Apple Silicon running the desktop app with no background service, and the client is T3 Code desktop on a second Mac, connected over SSH via Tailscale.

text
1# Host local time, 2026-10-31
217:03:43 desktop: updates.install, backendInstance.stop (5.0 s)
317:03:48 ElectronUpdater.quitAndInstall
417:03:52 SSH-launched `t3 serve --host 127.0.0.1 --port 3773 --base-dir ~/.t3` starts (PPID 1, SSH_CLIENT = the other Mac)
517:03:53 updated desktop: backendInstance.start; SSH-launched server listening on 3773
617:03:54 updated desktop backend on port 3774: ServerAlreadyRunningError, code=78; error dialog, then the app quits

About 9 s from the start of the stop to the refusal, which matches the earlier reports here. Stopping the SSH-launched server and reopening the desktop straight away cleared it, and the client then attached to the desktop's server.

On this host the race was also harder to recover from, because the SSH-launched server didn't exit on SIGTERM. Details and the stack are in #22983, which covers the separate problem of a server hanging in process.exit() while holding the owner lock.

Synced from GitHub

Note

Posted by Claude Code (Opus 5.5) on behalf of the reporter.

Same lockout without any update, so the desktop-update-in-progress marker from #23005 / #26191 doesn't cover it.

Host: Ubuntu laptop, desktop 0.1.6 (.deb), no background service. Client: T3 Code desktop on a Mac, connected over SSH via Tailscale.

text
1# Host local time
211-02 18:28 host reboots; the desktop app is not started
311-02 18:33 Mac reconnects; the launch script finds no server and starts a managed
4 `t3 serve --host 127.0.0.1 --port 3773 --base-dir ~/.t3` (0.1.6, PPID 1, SSH_CLIENT = the Mac)
511-03 09:17:50 desktop opened on the host; its backend on port 3774 exits 78:
6 ServerAlreadyRunningError: A T3 Code server already owns ~/.t3/userdata
711-03 09:18:00 error dialog closed, app quits

The SSH-launched server had no agent children and stopped promptly on SIGTERM. Reopening the desktop straight away worked.

The update marker can't help here: nothing is in progress, so there's nothing to wait for. Any time the client connects while the host's desktop isn't running (after a reboot, before login, after quitting the app), it starts its own server, and the desktop is locked out whenever it's opened later, possibly hours afterwards. Autostarting the desktop at login narrows this, but a host sitting at the login screen still loses.

Possible directions: when the owner is a managed SSH-launched server (ssh-launch/<key>/managed) with no running turns, the desktop could offer to stop it and take over the home instead of quitting; or the managed server could release the home when a local desktop asks for it. The dialog wording for this case is tracked in #22820.

Synced from GitHub

@gwadson the marker in #26191 only helps when the desktop is about to come back, like during an update. After a reboot or a quit there's nothing to wait for, so that case needs the desktop to take over from an idle SSH-launched server when it starts.

The two fixes would work together, since the marker still keeps an update from spinning up a server that gets killed seconds later. A takeover has open questions, though: what happens to the other machine's running turns, and what the dialog says (#22820). So I think it deserves its own issue. Happy to open one and work on it once a maintainer weighs in.

Synced from GitHub

Note

Posted by Claude Code (Opus 5.5) on behalf of the reporter, from a t3 triage session.

Same lockout without an update, on 0.1.6. This adds one detail: while the SSH client stays connected, the managed server comes back within about 30 s of being killed, so stopping it by hand only helps if the desktop is opened straight away.

Host: work Mac Studio, Apple Silicon, macOS 27.0, desktop app T3 Code (Beta) 0.1.6, no background service (t3 service status: not installed). Client: T3 Code desktop on a personal PC with this host saved as an SSH environment, connected through a self-hosted Teleport SSH proxy (not Tailscale). The host has no network route back to the client. ~/.t3/ssh-launch/<id>/managed was present, and the launched server ran the 0.1.7 runtime.

text
1# Host local time (UTC+3), 2026-11-04
216:07:12 managed `t3 serve --host 127.0.0.1 --port 3773 --base-dir ~/.t3` (0.1.7, PPID 1) listening, T3 Connect tunnel up
316:09:02 desktop 0.1.6 backend on 3774 -> ServerAlreadyRunningError, code=78
416:11:18 same, on relaunch
516:12:49 same, on relaunch
616:13:xx SIGTERM to the managed server
716:13:26 managed server is listening again (new PID, PPID 1). The desktop app was not running.
816:16:00 desktop relaunch -> ServerAlreadyRunningError, code=78
916:16:xx SIGTERM again
1016:16:59 managed server is listening again (new PID, PPID 1)

Workaround that worked: kill the managed server and open the desktop app straight away, before the client relaunches it. After that, updating the desktop to 0.1.7 and quitting and reopening it both worked, because the desktop owned the home and the client attached to it.

The dialog still pointed at t3 service uninstall, which doesn't apply here (#22820). #30580, which makes the desktop attach to a running local server, looks like the fix for this case.

Synced from GitHub

Same lockout on T3 Code Nightly 0.1.8-nightly.20261105.4218, after a plain quit, not an update. Host: MacBook Pro, Apple Silicon, Darwin 25.6.0, desktop app, Local environment on, no background service. Client: T3 Code Nightly desktop on a MacBook Air with the host saved as an SSH environment, connected over Tailscale.

Timeline (host local time, UTC-3):

text
115:13:52 last desktop trace event; the user quits T3 Code on the host
215:14:02 client relaunches over SSH: ~/.t3/runtime/versions/0.1.8-nightly.20261105.4218/t3 serve
3 --host 127.0.0.1 --port 3773 --base-dir ~/.t3
4 (PPID 1, SSH_CLIENT = client's Tailscale IP, ~/.t3/ssh-launch/<key>/managed)
515:14:36 host desktop relaunch picks :3774, backend exits 78 ServerAlreadyRunningError,
6 "This T3 home is unavailable" (again at 15:14:48 and 15:15:41)
7~15:17 SSH-launched server stopped with SIGTERM; lock released
815:18:16 client starts a new managed server over SSH, same command, takes the lock again
915:19:19 host desktop relaunch refused again, exit 78

The way out: quit the desktop app on the client. That also stopped the SSH-launched server on the host (see #27183). The lock was free afterwards and the host desktop could start.

The dialog says "such as the background service", which sent us looking for a launchd service that didn't exist. Pointing at ~/.t3/ssh-launch/*/managed and the client's address would have saved the search (#22820).

Synced from GitHub

@ajbarryiii thanks, #30580 (attach to a running local server) looks like it covers our case, so I'll follow that.

Synced from GitHub

Note

🤖 Claude Opus 5.5 responding on behalf of CopyJosh

Same lockout on T3 Code Nightly 0.1.8-nightly.20261107.4340, with one new detail: the SSH-launched server ran an older version than the host app, and it logged a divergent migration history against the host's database.

Host: Mac, Apple Silicon, macOS 27, T3 Code (Alpha) Nightly 0.1.8-nightly.20261107.4340, no background service (t3 service status: not installed). Client: an Arch Linux machine, connected over Tailscale SSH. The Mac also has the Linux machine saved as an SSH environment, so each machine launches servers on the other.

Timeline (host local time):

text
119:54:44 host app not running; SSH from the Linux machine starts
2 ~/.t3/runtime/versions/0.1.8-nightly.20261106.4247/t3 serve
3 --host 127.0.0.1 --port 3773 --base-dir ~/.t3
4 (PPID 1, ~/.t3/ssh-launch/<key>/managed, run-t3.sh pins
5 T3_ARCHIVE_VERSION='0.1.8-nightly.20261106.4247')
6~19:56 opening the 20261107 app shows "Another T3 Code server ... is using
7 this data directory"; the SSH-launched server is killed by hand
819:57:16 a new 20261106 server starts through the same ssh-launch key
9 (run-t3.sh rewritten at launch, same pin)
10~19:59 SSH session closes, the managed server exits; the 20261107 app
11 starts and owns ~/.t3 (server-runtime.json serverVersion 20261107.4340)

The ssh-launch/<key>/server.log on the host shows 4 such launches in total, not only today's.

The version mismatch: the 20261106 server logged this against the host's statev2.sqlite, which the newer app had migrated:

text
1WARN: Database migration history diverges from this build; recorded migration ids are skipped, not reconciled by name.
2 divergent: [
3 '77:RepairBooleanSettledOverride (unknown to this build)',
4 '78:DropOrchestrationEventCorrelationIndex (unknown to this build)',
5 '79:DelegatedRecoveryIndexes (unknown to this build)'
6 ]

So the launch script can start an older server on a home whose schema is newer than that server knows. That is a data risk in addition to the lockout. #30580 fixes the lockout by attaching, but the app would then attach to that older server.

Not verified: which process on the Linux machine sent the 20261106 pin. Its own server had run 20261107.4340 since 19:55, and a few minutes later 20261107.4340 was its only installed runtime. The connection catalogs are encrypted, so I could not read the environment entries.

Workaround: the same as above. Stop the SSH-launched t3 serve, then open the app before the client reconnects.

Synced from GitHub

Note

Posted with help from Claude Code on behalf of the account owner.

Same lockout on stable 0.1.7 (not Nightly), after the macOS desktop auto-updated 0.1.6 → 0.1.7. A few details that may help:

Setup: Host is a MacBook Air (Apple Silicon, macOS 27), desktop app, no background service. Client is a Linux laptop with the T3 Code .deb (0.1.6 at the time), with the Mac saved as an SSH environment over Tailscale.

What we saw:

  1. During the update restart, the client's launch script started a 0.1.6 t3 serve --base-dir ~/.t3 on the host. It logged Database migration history diverges from this build (migrations 77–79 unknown), because the 0.1.7 desktop had already migrated the database.
  2. The relaunched desktop failed with ServerAlreadyRunningError (exit 78).
  3. Killing the SSH-launched server didn't help, because the client relaunched it within about 1 second, every time. Updating the client to 0.1.7 only changed the version of the server it relaunched.
  4. Following the dialog's advice, we tried t3 service install. The desktop app then failed the same way against the service, so we uninstalled it. While swapping it out, the reconnected client grabbed the home again.
  5. The only reliable recovery was to disconnect the environment on the client, kill the server, relaunch the desktop, then reconnect. After that, ~/.t3/ssh-launch/<key>/managed reads external and the client attaches to the desktop's server correctly.

Side effect: each kill cancelled whatever run was in progress on the host (terminalizedRuns: 1 in shutdown reconciliation).

+1 for #30580. Separately, the dialog suggesting the background service is misleading for desktop users (related: #22820).

Synced from GitHub

Same lockout on T3 Code Nightly, without an update. One detail differs from the earlier reports: here the SSH-launched server is newer than the host's desktop app.

Setup: The host is a Mac mini (Apple Silicon, macOS 27.0) running the desktop app T3 Code (Nightly) 0.1.8-nightly.20261109.4429, with no background service. The client is T3 Code desktop on a MacBook, with the mini saved as an SSH environment. The client's exact version is unknown, but the runtime it installed on the host is 0.1.9-nightly.20261110.4452.

Timeline (host local time, UTC-5):

text
123:43:56 client's launch script starts ~/.t3/runtime/versions/0.1.9-nightly.20261110.4452/t3 serve
2 --host 127.0.0.1 --port 3773 --base-dir ~/.t3
3 (PPID 1, ~/.t3/ssh-launch/<key>/managed present; sshd-session holds a tunnel to :3773)
423:45:56 host desktop (0.1.8-nightly) backend on :3774 exits 78:
5 ServerAlreadyRunningError: A T3 Code server already owns ~/.t3/userdata
623:46:03 same again on relaunch

The host's backend log shows the same refusal earlier that day, at 14:36 and 18:00, so this keeps happening on this host.

Why the version direction matters: Earlier reports described an older SSH-launched server meeting a newer database. Here the client installed a newer runtime on the host than the host's own app. That server now owns the database, at migration 80 according to /.well-known/t3/environment. Once the user stops it, the older host desktop may hit the reverse migration problem. So attaching to the running server (#30580) or taking it over probably also needs a version check. Otherwise the host ends up stuck on the older app.

The dialog also tells the user to deal with an "older background service", but no service is installed. That's the problem described in #22820.

Note

Posted by Claude Code (Claude Opus 5.5) on behalf of the reporter, from a t3 triage session.

Synced from GitHub

Note

Posted by Claude Code (Opus 5.5) on behalf of the reporter, from a t3 triage session.

Same lockout on stable 0.1.9. Each machine has the other saved as a linked environment.

Setup: Two Apple Silicon Macs on the same LAN, both on macOS 27.2. Both run the desktop app T3 Code (Beta) 0.1.9, with no background service. Each Mac has the other saved as an SSH environment.

What happened: An update was queued on the host. The host app did not close itself to install it, as it usually does. After a manual quit, the desktop app showed "This T3 home is unavailable" and quit. Every relaunch failed the same way.

The dialog sends you the wrong way. In 0.1.9, onStateDirOwned in apps/desktop/src/backend/DesktopBackendPool.ts shows one fixed message. It names only the background service or an older service, and its steps (t3 update, t3 service uninstall/install, T3CODE_HOME) are all for one device. It never says that a linked machine may have started a server over SSH. The app then quits, so you cannot reach the setting to unlink the machine. Every report in this thread quotes the same dialog. #22820 covers the same dialog for background-service causes.

Ownership gap: The linked machine reconnects as fast as it can. Its launch script starts a server and takes the owner lock before the host's desktop app does. The desktop app cannot attach to a server it did not start, so it is locked out. #30580 looks like the fix for this part.

Timeline (host local time, UTC+5:30, 2026-11-12):

text
110:28:03 SSH-launched `t3 serve --host 127.0.0.1 --port 3773 --base-dir ~/.t3` (0.1.9, PPID 1)
2 starts, from ~/.t3/ssh-launch/<key>/
310:41:10 desktop backend on :3774 -> ServerAlreadyRunningError, code=78
410:42:48 same, on another relaunch (4 refused launches between 10:12 and 10:42)
510:57:54 the SSH-launched server was stopped; the client starts a new one within seconds
611:00:08 stopped again; the client starts another one within seconds
7 after T3 Code on the client was quit, the server stayed up until it was killed by hand

Details that may help:

  • The SSH-launched server and the host desktop app were the same version (0.1.9). A version mismatch is not required to trigger this.
  • ~/.t3/runtime/desktop-update-restart was still present a day after it was written (2026-11-11 19:03).

Workaround:

  1. Remove the link between the two machines.
  2. Start T3 Code on each machine on its own, so each one launches its local server.
  3. Link the machines again.