Repository navigation
Conversation
The managed server runs on the default T3 home, so it writes the default runtime file too. On every reconnect the launch script read that file, took the managed server for an external one, stopped it, and launched a new one, which ended every provider session running on the remote. When the default runtime file names the PID this launcher recorded as managed, treat it as our own server and take its port from that file, so a stale remembered port cannot hide it either. Without the managed marker the server is adopted as external, as before. Adopting an external server no longer falls back to stopping the PID the runtime file names, and a server that just answered adoption is not probed again while it is alive, so a momentary stall cannot launch a second server on the same home beside it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ApprovabilityVerdict: Approved at Macroscope's review found this PR approvable — This is a focused SSH reconnect bug fix that preserves healthy managed servers and adds targeted coverage for adoption and process-lifecycle edge cases. Production behavior changes are confined to the generated launch script, with no product-default, schema, infrastructure, security, or static-analysis changes. Notes:
You can add or adjust custom eligibility rules. Learn more. |
|
Confirmed the pid-match path this PR fixes on a Linux host (systemd user service on 3773, desktop SSH environment over Tailscale, 0.0.46-nightly.20261010.2948). The launcher's Even with this fixed, a managed server stopping on that same home still deletes the service's Posted by Claude Code (Claude Opus 5.5) via |
Dismissing prior approval to re-evaluate 39394b6
Problem
Reopening the desktop app restarts the SSH-managed remote server, so every running remote thread ends with "Provider session did not survive a server restart."
The managed server runs on the default T3 home, so it writes
~/.t3/userdata/server-runtime.jsonitself. On every connect,REMOTE_LAUNCH_SCRIPTreads that file, takes the managed server for an external one, SIGTERMs the managed PID, marks the stateexternal, finds the port dead, and launches a replacement. The triage of #10934 assumed that "the launch script already reuses a live managed PID"; since #9843 made the recorded PID the realt3 serve, it no longer does, because that PID is now the same one the runtime file names.Fix
When the default runtime file names the PID this launcher recorded as managed, treat it as our own server and take its port from that file, then fall through to the existing managed-reuse path (still restarting on a runner change or failed readiness, as before). Taking the port from the runtime file also keeps a stale or missing remembered port, for example after a launch interrupted between writing the pid and port files, from hiding the live server and starting a second one on the same home. Without the managed marker (an interrupted launch, or a recycled PID) a matching PID does not prove ownership, so that server is adopted as external, as before, rather than restarted on a runner change.
The adoption branch also no longer falls back to stopping the PID named in the runtime file when the pid file is missing. That fallback stopped the server the script was about to reuse as external. Keeping that server alive exposed one more path: the external branch probed it a second time, so a momentary stall right after adoption launched a second server on the same home beside it. A server that just answered adoption is now not probed again while it is still running; if it exited during the handover (including as an unreaped zombie, which
kill -0still reports), the script falls back to the existing readiness check and launches a replacement. A host withoutpstakes that fallback too.This does not change what happens on desktop quit or disconnect (#10934 / #10951); it only stops the reconnect from replacing a healthy managed server.
Evidence
runStopAll/stopSessionsat 21:40:19 and a newt3 servestarted at 21:40:25. The remoteserver.logrecorded 15 server starts over a few days.packages/ssh/src/tunnel.test.tsrun the real launch script (node-script runner) against a fake server that answers readiness and writes the runtime file liket3 serve: first launch is managed, a reconnect keeps the same PID and port, a reconnect with a stale remembered port still keeps it, and a separate default-home server (a service) still takes over as external. That test fails onmain(expected 91007 to equal 90965: the reconnect replaced the server) and passes with the fix. A second test removes the managed marker and changes the runner, and checks the server is adopted as external and keeps running. A third adopts a default-home server that answers once and then stalls, and checks no second server is launched (onmainthis case stops the service and launches a managed one). A fourth adopts a service that exits right after answering, while the managed server is being stopped, and stays an unreaped zombie underexec sleep; it checks a new managed server is launched instead of returning the dead port. All four seed a free port so they never probe 3773, and stop servers by the PID the script recorded, including after a failed assertion.HOME: onmainthe reconnect replaced the managed PID; with the fix it kept it.vp test run src/tunnel.test.ts(23 passed, 5 runs),vp lintandtsc --noEmitforpackages/ssh.Not checked: archive-mode helpers (
__ssh-helper) behave the same as the node fallbacks the test uses; the guard itself is identical in both modes.Fix and PR by Claude Opus 5.5 (Claude Code), reviewed over six rounds with GPT-6.1-sol (Codex, xhigh effort), whose findings shaped the stale-port, missing-marker, stall and zombie handling.
🤖 Generated with Claude Code