Summary
When the desktop app connects to a remote host over SSH, every reconnect after the first SIGTERMs the healthy server that the client itself launched, then starts a new one. Every provider session (Claude, etc.) on the remote dies on each reconnect.
On my Mac host this caused 22 server restarts in ~18h on v0.0.45. Each one followed a tunnel reconnect (laptop sleep, Wi-Fi blip, app restart).
Root cause
This is in REMOTE_LAUNCH_SCRIPT in packages/ssh/src/tunnel.ts (lines from v0.0.45; the logic is unchanged on main @ cf3e714):
- The client starts the managed server with
--base-dir "$HOME/.t3" (L705) and writes managed to $MANAGED_FILE.
- Because of that base dir, the managed server writes its own pid and port to
~/.t3/userdata/server-runtime.json. That is the same file the script treats as the "default runtime" (DEFAULT_RUNTIME_FILE, L550).
- On the next launch,
resolve_default_runtime_port finds that file. The pid is alive and the origin is localhost, so it passes.
- The port answers
wait_ready, and REMOTE_MANAGED = managed (L646). The script assumes an external service has appeared and kills PID_TO_STOP, then marks the state external. But that pid is the client's own managed server, the same pid as in $PID_FILE.
- The
external branch re-probes the port. It just killed that server, so the probe fails, the state is cleared, and a fresh server is spawned (L695-709).
tunnel.test.ts (around L300-312) asserts the kill-then-adopt ordering. It does not cover the case where the runtime file belongs to the managed server itself.
Evidence (remote host)
~/.t3/ssh-launch/<hash>/pid == server-runtime.json.pid, and managed = managed.
server.log shows a new Listening on http://127.0.0.1:3773 22 times with no error before any of them.
server.trace.ndjson: ProviderService finalizer (runStopAll → stopSessions) at 10:16:58, last span 10:17:00. ssh-launch files rewritten and new server up at 10:17:07. This is a clean SIGTERM followed by an immediate relaunch.
Suggested fix
Only adopt or kill when the default runtime is not the managed server. For example, skip the kill branch when DEFAULT_RUNTIME_PID = REMOTE_PID and treat it as reuse. Alternatively, have the server record whether it is service-managed in server-runtime.json and check that.
Workaround
Run t3 service install on the remote host and remove ~/.t3/ssh-launch/<hash>/{pid,managed}. The client then adopts the service as external and never kills it.
Environment: desktop client → remote macOS (Darwin 24.5, arm64) over Tailscale SSH, t3 0.0.45 release archive.
Summary
When the desktop app connects to a remote host over SSH, every reconnect after the first SIGTERMs the healthy server that the client itself launched, then starts a new one. Every provider session (Claude, etc.) on the remote dies on each reconnect.
On my Mac host this caused 22 server restarts in ~18h on v0.0.45. Each one followed a tunnel reconnect (laptop sleep, Wi-Fi blip, app restart).
Root cause
This is in
REMOTE_LAUNCH_SCRIPTinpackages/ssh/src/tunnel.ts(lines from v0.0.45; the logic is unchanged onmain@ cf3e714):--base-dir "$HOME/.t3"(L705) and writesmanagedto$MANAGED_FILE.~/.t3/userdata/server-runtime.json. That is the same file the script treats as the "default runtime" (DEFAULT_RUNTIME_FILE, L550).resolve_default_runtime_portfinds that file. The pid is alive and the origin is localhost, so it passes.wait_ready, andREMOTE_MANAGED = managed(L646). The script assumes an external service has appeared and killsPID_TO_STOP, then marks the stateexternal. But that pid is the client's own managed server, the same pid as in$PID_FILE.externalbranch re-probes the port. It just killed that server, so the probe fails, the state is cleared, and a fresh server is spawned (L695-709).tunnel.test.ts(around L300-312) asserts the kill-then-adopt ordering. It does not cover the case where the runtime file belongs to the managed server itself.Evidence (remote host)
~/.t3/ssh-launch/<hash>/pid==server-runtime.json.pid, andmanaged=managed.server.logshows a newListening on http://127.0.0.1:377322 times with no error before any of them.server.trace.ndjson:ProviderServicefinalizer (runStopAll→stopSessions) at 10:16:58, last span 10:17:00.ssh-launchfiles rewritten and new server up at 10:17:07. This is a clean SIGTERM followed by an immediate relaunch.Suggested fix
Only adopt or kill when the default runtime is not the managed server. For example, skip the kill branch when
DEFAULT_RUNTIME_PID = REMOTE_PIDand treat it as reuse. Alternatively, have the server record whether it is service-managed inserver-runtime.jsonand check that.Workaround
Run
t3 service installon the remote host and remove~/.t3/ssh-launch/<hash>/{pid,managed}. The client then adopts the service asexternaland never kills it.Environment: desktop client → remote macOS (Darwin 24.5, arm64) over Tailscale SSH, t3 0.0.45 release archive.