Repository navigation
cmux NEXT: an SSH machine whose reconnect gave up reads Offline; reconnect on use never takes over a session in use - #28794
Conversation
…nnect on use never takes over a session in use HQ #4877 G4: once a later daemon reconnect spends its timed retries, the connection tells the service (DaemonConnection.observeRetriesSpent), the store marks it (DaemonStore.retriesSpent, cleared by the next connect or a new attempt), and the SSH header reads Offline instead of Connecting forever. Reconnect on it restarts the link client and the daemon connection with a fresh retry budget (SSHMachineSession.restart), instead of waking the spent loop or doing nothing on a connected link. HQ #4877 G5: the pane's reconnect on use runs machine.reconnect with origin .page (was .user), machine.reconnect forwards its origin, and remote.reconnect skips a link whose session is in use by another cmux daemon unless a person asked (user or cli origin). paneUse reads such a link as stuck, so the pane does not wait on it. Tests: SSHMachineLinkRecoveryTests (not run locally: no Swift on this VM; CI runs it). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-4bbc87e99e6352cca67a
…ent" Review follow-up on HQ #4877 G4: a server.busy refusal keeps a timed retry (RetryPacer.refusedBusy), so it must not mark the store offline. RetryPacer.waitsForEventOnly gates the retries-spent report. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-4bbc87e99e6352cca67a
…nges nothing Review follow-ups on HQ #4877 G4: the retries-spent hop applies only while its connection is still the service's (a Reconnect's restart replaced it), and is not reported from a cancelled or closed loop. SSHMachineSession.restart keeps its teardown task, cancelled by the next restart, Disconnect and close(), so a closed session never connects again. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-4bbc87e99e6352cca67a
|
Found 12 test failures on Blacksmith runners: Failures
...and 2 more test failures. View all on Blacksmith
|
|
Review (router-review, gpt-6-astra high fast): SHIP at 77564d2 Earlier rounds' findings are fixed at this head: the busy refusal past the budget (81ca529), the late retries-spent hop and the closed session's restart (77564d2). |
|
Review (codex gpt-6-astra high, next-review): SHIP at 77564d2 No blocking findings. |
…chine-link-recovery
|
cmux status (one comment, edited in place; each line is updated by the job that owns it) Review App of this head (app.zip, kept 1 day)
Default changesThis pull request changes 3 defaults at
Signed off on this commit by Merge receipt: merged as Regenerate the snapshot with Shipped in:
|
…ly gate Review follow-up on HQ #4877 G5: SSHMachineLink.start sends .attemptStarted before it checks mayAttempt, and the reducer cleared onlyUserUnblocks on it, so after one automatic endpoint retry reconnect on use (and an app activation wake) could take over a session in use elsewhere. .attemptStarted now keeps the flag, like a wake; it reopens only a gate that was open. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-4bbc87e99e6352cca67a
…p the in-use takeover person-only Review follow-up on HQ #4877 G5: SSH… on a saved host (remote.connect, remote.newGroup) and a workspace's Reconnect ran reconnect as a person whatever their caller. ActionInvocation.isPersonsRequest (user or cli) now decides byPerson on every action path, and SSHService.connect leaves a session in use alone for any other caller. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-4bbc87e99e6352cca67a
Bugbot needs on-demand usage enabledBugbot uses usage-based billing for this team and requires on-demand usage to be enabled. A team admin can enable on-demand usage in the Cursor dashboard. |
|
Review (router-review, gpt-6-astra high fast): SHIP at 09a1ccd |
…chine-link-recovery
|
Review (router-review, gpt-6-astra high fast): SHIP at c11edce c11edce merges the base tip a3f620e (clears the crash-class base red, #29032); the PR's own files are unchanged from 09a1ccd. |
Base moved the reconnect step to SSHConnectionMachine.Status.reconnectStep (#28799); this lane's SSHReconnectStep goes, and .restart with retriesSpent (HQ #4877 G4) joins base's API. SSHMachineSession.waitsForUser reads the link's gate directly, not only once the mirrored status reads .failed (review P1 follow-up at c11edce: the status reaches the session asynchronously). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-4bbc87e99e6352cca67a
|
Review (router-review, gpt-6-astra high fast): SHIP at 3d0fa56 3d0fa56 merges base c5b06d3 (base's #28799 moved the reconnect step to SSHConnectionMachine.Status.reconnectStep; this PR's .restart joins it), reads the link's in-use gate directly (the c11edce P1 follow-up), and adds an app_lines exception for the 24 App lines the PR's own move no longer pays for. |
…(offsets #28794's app_lines +24 exception) (#29551) a768273 (#28794, "an SSH machine whose reconnect gave up reads Offline; reconnect on use never takes over a session in use") landed with an app_lines +24 exception while the arch exceptions grow (STATUS 2208); a move PR from the same area must merge first. SidebarBridge.sshStatus(link:startupFailed:daemonConnected:compatibility: retriesSpent:), the pure mapping from an SSH link status and its daemon to the sidebar header's status (which #28794 itself extended with `retriesSpent`), moves unchanged to CmuxNextBridge, the package that maps daemon state to view models, as SSHConnectionMachine.Status.sidebarStatus(startupFailed:daemonConnected: compatibility:retriesSpent:). The App's sshStatus(_ session:) keeps the session read and calls it. CmuxNextBridge now depends on CmuxNextRemote (domain -> service). SSHLinkReconnectingTests and SSHMachineLinkRecoveryTests call the moved function. CmuxNextApp -25. No behavior change. Co-authored-by: cmux-bot[bot] <348531757+cmux-bot[bot]@users.noreply.github.com>
![Fix with [code]smith](https://pr-comments-assets.blacksmith.sh/codesmith/fix-with-codesmith-light-3x.png)
Closes gaps G4 and G5 of HQ #4877 ("Remote machines in NEXT: setup walkthrough, breaks, and recovery after a link drops"). Leo asked about this (ask-8fac5e5066, 2026-10-28): "after you've connected something remotely and whatever, that whole setup situation, like after we set it up, how good is it in terms of that whole, like, recovery flow, and, like, that kind of connection situation, and how reliable things are?"
From the issue:
What changes
G4: a reconnect that gave up reads Offline, and Reconnect starts a fresh connect
DaemonConnection.observeRetriesSpent: the reconnect loop reports each failed attempt after its timed budget (10 tries, about 80 s) is spent.DaemonService.start(remote:)hooks it up (DaemonService+RetriesSpent.swift) to markDaemonStore.retriesSpent. The next connect clears the mark, and so does a new attempt (beginConnectionAttempt). A late hop that lands while connected changes nothing.SidebarBridge.sshStatusmaps a link that is connecting, connected or reconnecting, with its daemon not connected and its retries spent, to.offline, not.connecting. Offline is red, the header offers Reconnect, andreconnectsOnUseis true, so opening the chat reconnects it.SSHReconnectStep.restart:SSHMachineSession.restart()stops the link client and the daemon connection, then connects again with a new retry budget, like Disconnect then Connect. Before, it was.nothing(link connected) or a wake that tried once..restartjoins base'sSSHConnectionMachine.Status.reconnectStep(Forget and Devices Remove each take out both the SSH machine and its acpmux peer #28799 moved it to CmuxNextRemote). Since that move no longer offsets this PR's App lines,plans/cmux-next/arch-ratchet.jsongets anapp_linesexception of 24 lines.G5: automatic paths never take over a session in use
SSHConnectionMachine.waitsForUser: true while the gate is closed forsessionInUse, until a.userwake.SSHMachineLinkmirrors it so it can be read without an actor hop.AgentTabs+WiringmachineLink.reconnect) now runsmachine.reconnectwith origin.page, not.user.machine.reconnectpasses its origin on toremote.reconnect.remote.reconnectcounts only.userand.clias a person's Reconnect, andSSHService.reconnect(_:byPerson:)does nothing on a session in use unless a person asked.SidebarBridge.paneUsereads an SSH session in use as.stuck, so the pane neither waits on it nor reconnects it.MachineLinkState.reconnectsOnUse(.failed)stays true for other failures, which can still be retried.No collision with #28623 (webviews only: the error card while a chat's machine reconnects) or #28407 (Cloud statuses in
MachineLinkState). Prior art: #26786 (merged; this PR fixes its G5) and #27060 (merged; first-connect recovery, whoseneedsRecoveryandreconnectStepcases are unchanged).Review fixes
server.busyrefusal past the budget keeps a timed retry, so it doesn't mark retries spent (RetryPacer.waitsForEventOnly, with a test inBusyRefusalReconnectTests).restart()keeps its teardown task, which the next restart, Disconnect andclose()all cancel (aRestartedSessionThatClosesNeverConnectsAgain)..attemptStarted, sent before the gate check) no longer clears the in-use user-only gate.remote.connect,remote.newGroupandreconnectWorkspacepassActionInvocation.isPersonsRequest(user or cli) asbyPerson.SSHService.connectleaves a session in use alone for any other caller.Follow-ups (P2, from the SHIP at 09a1ccd; owned by this lane, fix forward):
remote.connectwith a different cmux-tui binary path replaces the saved session (cx-vuvc) before the in-use check, so the new session isn't gated. Check the saved session first.waitsForUsernow reads the link's gate directly..wake(.user)/.connecton the link actor; a.sessionInUselanding in between gets opened. CarrybyPersoninto the actor and check there.retriesSpentis set, a wake that gets a busy refusal resumes timed retries but leaves the mark on, so the header reads Offline while it retries. Publish the transition back as well.Tests
Tests/CmuxNextAppTests/SSHMachineLinkRecoveryTests.swift(new):aSpentReconnectReadsOfflineNotConnecting: the header status for each link state, with and without spent retries.reconnectAfterSpentRetriesIsAFreshConnect:SSHReconnectStepgives.restart.theStoreMarksSpentRetriesUntilTheNextConnect: the mark is cleared on connect, and a late hop is ignored.reconnectOnASpentSessionStartsANewAttemptAndClearsTheMark: the fullSSHService.reconnectpath.aSessionInUseWaitsForAPerson:waitsForUseracross app-activation and user wakes.reconnectOnUseLeavesASessionInUseAndAPersonTakesItOver:paneUse == .stuck;byPerson: falsestarts no attempt, andbyPerson: truedoes.RemoteConnectRecoveryTests.reconnectDuringATransientInterruptionKeepsAnEstablishedDaemonsStartupis moved toSSHReconnectStepand its expectations are unchanged.Command:
swift test --filter 'SSHMachineLinkRecoveryTests|RemoteConnectRecoveryTests|SSHSessionInUseTests'(the CmuxNextAppTests suites in this PR's CI).Observed / Not run
scripts/cmux-next/prepush-gate.pypasses (no new violations, and arch-ratchet shows no App growth). The namespace lint passes.retriesSpent,waitsForUser,.restart), so on base they fail to compile rather than fail.Risk
risk:normal. This changes the SSH reconnect paths.
.page. For SSH machines this only matters when the session is in use. Paired servers and Cloud machines ignore the origin (wake()andretry()).cmux machine reconnectstill counts as a person.UI
The sidebar SSH header reads Offline (red dot, with Reconnect) where it used to show Connecting… forever.
No capture: getting there takes a live SSH link whose daemon reconnect runs out of its 10 timed retries (about 80 s), and no scene or fixture can drive
DaemonConnection's reconnect loop. The words and dot are the existing Offline header, which is unchanged.Changelog
Fixed: an SSH machine whose connection gave up shows Offline with Reconnect instead of Connecting… forever, and an open chat no longer takes over a machine's session that is in use elsewhere.
Agent session: agent session
🤖 Generated with Claude Code