Skip to content

fix(ssh): reconnecting no longer restarts the managed remote server - #17684

Open
puccez wants to merge 1 commit into
pingdotgg:mainfrom
puccez:fix/ssh-launch-self-kill
Open

puccez wants to merge 1 commit into
pingdotgg:mainfrom
puccez:fix/ssh-launch-self-kill

Conversation

@puccez

@puccez puccez commented Oct 10, 2026

Copy link
Copy Markdown

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.json itself. On every connect, REMOTE_LAUNCH_SCRIPT reads that file, takes the managed server for an external one, SIGTERMs the managed PID, marks the state external, 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 real t3 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 -0 still reports), the script falls back to the existing readiness check and launches a replacement. A host without ps takes 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

  • Observed on a Linux remote (desktop 0.0.45): desktop quit at 21:40:04, relaunch ran the launch script at 21:40:16, the remote server logged runStopAll / stopSessions at 21:40:19 and a new t3 serve started at 21:40:25. The remote server.log recorded 15 server starts over a few days.
  • New tests in packages/ssh/src/tunnel.test.ts run the real launch script (node-script runner) against a fake server that answers readiness and writes the runtime file like t3 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 on main (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 (on main this 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 under exec 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.
  • Same three phases run by hand on the Linux remote in an isolated HOME: on main the reconnect replaced the managed PID; with the fix it kept it.
  • vp test run src/tunnel.test.ts (23 passed, 5 runs), vp lint and tsc --noEmit for packages/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

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>
@github-actions github-actions Bot added vouch:unvouched PR author is not yet trusted in the VOUCHED list. size:M 30-99 changed lines (additions + deletions). labels Oct 10, 2026
macroscopeapp[bot]
macroscopeapp Bot previously approved these changes Oct 10, 2026
@macroscopeapp

macroscopeapp Bot commented Oct 10, 2026 •

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Approved at 39394b6

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:

  • Diff unchanged. Approvability was decided on eligibility alone.

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

@coderabbitai

coderabbitai Bot commented Oct 10, 2026

Copy link
Copy Markdown

Review in Change Stack →Review in Change Stack →

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration
  • Configuration used: Path: .coderabbit.config.ts
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 8ed7f4c2-1caa-41e0-b7ef-a4febaf4f814

📥 Commits

Reviewing files that changed from the base of the PR and between eddc8fb and 39394b6.


📒 Files selected for processing (2)
  • packages/ssh/src/tunnel.test.ts
  • packages/ssh/src/tunnel.ts

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



📝 Walkthrough

Walkthrough

The SSH tunnel launcher now distinguishes its own managed server from a server recorded in the default runtime file. It reuses or adopts servers based on their recorded PID, and checks adopted process state during external-server validation.

Changes

SSH tunnel server reuse and adoption

Layer / File(s) Summary
Managed server selection and handover
packages/ssh/src/tunnel.ts, packages/ssh/src/tunnel.test.ts
The launcher reuses its managed server when its PID matches the default runtime record. For a different ready server, it records the PID as adopted and stops only a marked managed server. Integration tests cover reuse, handover, and external-server cases.
Adopted server validation
packages/ssh/src/tunnel.ts, packages/ssh/src/tunnel.test.ts
The launcher skips another readiness probe when the adopted process is running and is not a zombie. Tests cover an adopted server that stalls after its first response and one that exits during handover.

Priority: ➖ Normal

Estimated code review effort: 3 (Moderate) | ~20 minutes

Change: Bug fix

Suggested reviewers: juliusmarminge


Merge Risk

Merge Risk: ⚪ Minimal · up to 39394

This change keeps SSH reconnects from replacing a healthy managed remote server, and no concrete merge-blocking issue was found. Normal checks are sufficient before merging.

Security Architecture Review

Security architecture risk: 🔵 Low · up to 39394

The change avoids unnecessary server termination while preserving SSH access and remote-loopback forwarding. No new security finding was established. Crash recovery and simultaneous connections from independent clients remain areas of limited assurance.

Retained concerns
No architecture-level concerns identified.

Security review details

Security Blast Radius

  • inferred — Process termination and server selection operate within the selected SSH account's authority. Because launches and adoption share the default remote home, their lifecycle effects can reach sessions and state using that home, not only one connection's ownership directory. The PR does not add a new account or forwarding destination.

Trust Boundaries and Controls

  • observed — Launch orchestration retains the existing SSH authentication options. The Node runtime reader requires a positive integer PID, an integer port, a loopback HTTP origin and PID liveness. Managed reuse retains readiness validation; external adoption retains its first readiness probe, and tunnel startup separately waits for HTTP readiness before returning the tunnel.

Resilience and Maintainability Implications

  • observed — Normal launch-readiness failure terminates the spawned PID and removes ownership files. The stop script excludes external servers and retains ownership files if a process does not stop within its wait period. Ensure and disconnect share a per-target lock within one manager. Ownership files remain separately written, and the launch script contains no remote lifecycle lock; these mechanisms are unchanged from the base.

Hardening Proposals

  • proposed — Consider binding managed ownership to process-start identity and coordinating lifecycle transitions across independent clients sharing a remote home. This would strengthen pre-existing PID-reuse and concurrency assumptions; it is a hardening proposal, not an observed PR security finding.



Pre-merge checks | Passed 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check Passed The title clearly identifies the primary fix: SSH reconnects no longer restart the managed remote server.
Description check Passed The description explains the problem, fix, scope, verification results, limitations, issue context, and agent usage. It uses “Fix” and “Evidence” instead of the template’s “Change” and “Verification” …
Linked Issues check Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check Passed Check skipped because no linked issues were found for this pull request.


✨ Finishing Touches
🧪 Generate unit tests (beta)
  • 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.

@Trillx

Trillx commented Oct 11, 2026

Copy link
Copy Markdown

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 pid file and server-runtime.json named the same managed server. It was SIGTERMed and relaunched 16 times in about 27 hours, and each restart closed the running Claude session within 1–3 s. Details and timestamps: #15986 (comment)

Even with this fixed, a managed server stopping on that same home still deletes the service's server-runtime.json. #9045 covers that.

Posted by Claude Code (Claude Opus 5.5) via t3 triage.

@juliusmarminge juliusmarminge added the macroscope-review Opt PRs made by unvouched contributors in for Macroscope review. Vouched contributors auto-reviews label Oct 11, 2026 — with ChatGPT Codex Connector
@macroscopeapp
macroscopeapp Bot dismissed their stale review October 11, 2026 07:02

Dismissing prior approval to re-evaluate 39394b6

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

macroscope-review Opt PRs made by unvouched contributors in for Macroscope review. Vouched contributors auto-reviews size:M 30-99 changed lines (additions + deletions). vouch:unvouched PR author is not yet trusted in the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants