Repository navigation
fix(ssh): route the remaining expired-lease readers through the reattach predicate - #18378
Conversation
📝 WalkthroughWalkthroughSSH lease handling now distinguishes unmarked expired leases from superseded or recycled leases. Termination can require reconnects for unmarked expired leases. Kill-intent pruning retains those leases while removing retired routes. Reset tests cover all expired lease variants. Orphan relay sweep coverage confirms that recycled relay IDs do not stop the corresponding PTY. Merge Risk: 🟠 High · up to Terminating SSH sessions can kill a newer active PTY that reused a recycled relay ID. This should be fixed before merge. 🚥 Pre-merge checks | ✅ 3 | ❌ 2❌ Failed checks (2 warnings)
✅ Passed checks (3 passed)
Full details: Description checkExplanation The description provides detailed change rationale and test results, but it does not follow the repository template. It omits the required Linked Issue entry, Visual Proof section with an N/A justification, checklist confirmations, and several template headings. Resolution Update the description to include all required template sections. Add a linked issue using the required
✨ Finishing Touches 💡 1📝 Generate docstrings 💡
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Team
Run ID: cd070698-562a-4f84-b9f8-892add75bd1e
📒 Files selected for processing (7)
src/main/ipc/ssh-connection-handlers.tssrc/main/ipc/ssh-relay-reset-resume.test.tssrc/main/ipc/ssh-terminate-sessions.test.tssrc/main/persistence-ssh-pending-pty-kill.test.tssrc/main/persistence/leasing-ssh-ptys/ssh-pty-kill-intent-operations.tssrc/main/ssh/ssh-orphan-relay-pty-sweep.test.tssrc/main/ssh/ssh-orphan-relay-pty-sweep.ts
Included review availability: Your plan provides up to 10 included reviews per hour; 3 remain after this review.
| if (lease.state === 'terminated') { | ||
| continue | ||
| } | ||
| // Why: 'expired' records that reattach gave up, never that the remote shell died — those are | ||
| // precisely the orphans, so the user's terminate action has to be able to reach them. | ||
| trackPtyId(lease.ptyId, lease.state !== 'expired') | ||
| // Why the predicate and not `state !== 'expired'`: an `expired` lease carrying no | ||
| // retirement mark records only that reattach gave up, never that the remote shell died, so | ||
| // it is exactly the orphan the user's terminate must reach — and reaching it needs the | ||
| // relay, which is what the fence below demands. Only `supersededBy` / `relayIdRecycled` | ||
| // prove the route is dead for good, and those stay unowned. | ||
| trackPtyId(lease.ptyId, sshRemotePtyLeaseAllowsReattach(lease)) | ||
| } | ||
| const ptyIds = Array.from(ptyIdsByRelayId, ([relayPtyId, appPtyId]) => ({ | ||
| relayPtyId, |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Exclude retired leases from provider shutdown candidates. In ssh:terminateSessions, sshRemotePtyLeaseAllowsReattach(lease) returns false for an expired lease with relayIdRecycled, but trackPtyId still adds its ID to ptyIds. SshPtyProvider.shutdown sends that relay ID without expectedIncarnationId, so pty.shutdown can kill the newer PTY using the recycled ID. Skip leases that fail the predicate when building shutdown candidates, while preserving their unverifiable accounting.
There was a problem hiding this comment.
✅ No new issues found.
Reviewed changes
ssh:terminateSessionsreconnect fence — unmarkedexpiredleases now count as owned and raiseSSH_TERMINATE_RECONNECT_REQUIREDwhen no provider is attached;supersededBy/relayIdRecycledleases stay unowned.- Kill-intent row disposal —
isDisposableKillOnlyLeasenow gates on!sshRemotePtyLeaseAllowsReattach, retaining unmarkedexpiredrows while still collectingterminatedand marked-expiredrows. - Documentation-only —
doResetRelay's bulk expire and the orphan sweep'sclientClaimskeep their raw state checks, with the reasons recorded. - Tests — two tests that fail without the source edit, plus characterization coverage for the divergent cases.
The two behavior changes are narrow and consistent: the shutdown list in terminateSessions is unchanged (only the reconnect fence flag moves), and isDisposableKillOnlyLease differs from the old condition solely on the unmarked-expired case. The predicate is already the rule reattachKnownPtys and the pane-recovery reader use, and removeSshTargetWithBestEffortCleanup tolerates the reconnect refusal, so #2626 stays preserved.
DeepSeek Pro (free via Pullfrog for OSS) | 𝕏

Follow-up to the six-PR lease stack (#17951, #17957, #17965, #17966, #17969, #18013), which split the meaning of
expired:SshRemotePtyLeaseStatehas nounverifiablemember, so "we don't know" is persisted asexpired, and anexpiredlease is an orphan unless a mark says its route died for good.sshRemotePtyLeaseAllowsReattachis that rule.Three
main-side readers still testedstate === 'expired'directly. They do not all want the same answer — this audits each one and migrates only the two that were asking the reattach question.Migrated
ssh:terminateSessionsreconnect fence (src/main/ipc/ssh-connection-handlers.ts). Anexpiredlease carrying no retirement mark records only that reattach gave up, so it is exactly the orphan the user's "End Remote Terminals" has to reach — and reaching it needs the relay. It now counts as owned and raisesSSH_TERMINATE_RECONNECT_REQUIRED, which the renderer answers by connecting and retrying (terminateSshSessionsWithReconnect).supersededBy/relayIdRecycledleases stay unowned and keep being swept opportunistically, which is what preserves #2626: a target that can no longer answer never blocks its own removal (removeSshTargetWithBestEffortCleanupalready tolerates the refusal).Kill-intent row disposal (
src/main/persistence/leasing-ssh-ptys/ssh-pty-kill-intent-operations.ts).isDisposableKillOnlyLeasecollected any pane-lessterminatedorexpiredrow once itspendingKillwas retired. For an unmarkedexpiredlease that row is the client's last route back to a shell that may still be running: deleting it takes the id out of the bulk reattach set (reattachKnownPtys) and out of the orphan sweep's leave-alone list in one write, turning a process left running on purpose into a sweepable one. It also contradicted the TTL path's own comment that an order aging out "observes nothing about the process". Now gated on!sshRemotePtyLeaseAllowsReattach, soterminatedand marked-expiredrows are still collected.Kept raw, with the reason recorded
doResetRelay's bulk expire. This asks which routes the force-stop just invalidated, not which leases may be reattached. An already-expiredlease has no route left to retire:markSshRemotePtyLeaseearly-returns on an unchanged state, and any local handle this connection really holds arrives throughgetPtyIdsForConnection, so migrating here is a strict no-op. Reset also must not upgrade it toterminated— killing the relay daemon makes its PTYs permanently unreachable, which is not evidence they exited (docs/reference/ssh-execution-boundary.md). Nothing can adopt a stranger either: the replacement relay namespaces every id under a freshptyIdMintEpoch, so an old orphan lease can only fail its next reattach.ssh-orphan-relay-pty-sweep.ts'sclientClaims. Precedent here isterminal-binding-recovery.ts, which deliberately uses a different predicate because it asks a different question. This one asks "may this client stop the process", and every non-terminatedstate answers no either way —routedandexpiredLeasePtyIdsare both skips, so sorting the marked leases intoroutedwould move nothing but the skip-reason string. Both marks are written by paths that leave the remote process running on purpose, so they must keep refusing the stop rather than authorizing one.Tests
Two behavior changes, two tests that fail without the source edit (verified by stashing only the source files):
ssh:terminateSessions demands a reconnect for an unmarked expired leasekeeps an unmarked expired lease when its intent ages outPlus coverage for the leases the predicate excludes and the raw check does not, since those are where the two diverge: superseded and
relayIdRecycledcases on the terminate fence, both marks on the kill-intent disposal,relayIdRecycledon the sweep, and all three expired shapes pinned as untouched by reset. The pre-existing #12661 test now drives a superseded lease, keeping the "no forced reconnect for a route that died for good" case on the lease that actually has that property.Verified:
pnpm tc,pnpm test src,pnpm run check:code-quality:changed,pnpm test:e2e:ssh-docker.