Cloud: a dropped app link reads Disconnected, not Failed to start
Leo (2026-10-29): "a whole lot of this cloud stuff ought to get better..."
What was wrong
When a Cloud machine on the app server's link lost that link (after running fine for hours), its sidebar header read "Failed to start" in red. CloudCreationRows.machine turned every stage failure into SidebarMachine.Status.failed, and CloudMachineStageInput makes an ended link (linkEnded) or a failed resume (resumeFailure) a stage failure. "Failed to start" is wrong for a machine that started.
Also, unlike the legacy link (noteLink), the app link's showEnded never read the machine list again. When the idle auto-pause parked the machine while cmux stayed in front, the app kept the stale "running" record, so the header stayed failed instead of showing "Paused (idle)".
The fix
- New
SidebarMachine.Status.disconnected("Disconnected"). It has the attention tone and needs attention: a click connects it again, and the tooltip gives the reason. Only a link that went down or was revoked (CloudAppLinks.isDrop:CloudLinkError.disconnectedor.revoked) reads "Disconnected" (CloudMachineStageInput.isDisconnect). "Failed to start" still appears for a failed create, acloud.machine.connectthe provider refused (CloudLinkError.failedand the rest), a failed resume, and a machine whose record says failed. CloudMachineSession.showEndednow asks for a list read (refresher?(false), so at most one every 30 s), as the legacy path does. The reconcile it runs suspends the session, and a paused record makes the header read "Paused (idle)".- The list read is fenced to the connect.
refreshnotes each session's connect count when its read begins (CloudMachineSession.connects), andCloudMachineSession.listed(_:connectsAtRead:)(the per-machine part ofreconcile, moved unchanged) does not suspend a session that began a connect after the read began. That session also keeps its live status, pause reason and running start, and takes only the record's other fields. Without the fence, a click on Connect during that read could get a stale "paused" answer, and the suspend would cancel the click's connect. sidebar.machine.disconnectedis inCmuxNextSidebar/Resources/Localizable.xcstringsfor every locale,needs_reviewlike its sibling strings.plans/cmux-next/sidebar-owner.mdhas an entry for the new header word.
Test
CloudLinkEndedHeaderTests (CmuxNextAppTests) drives a real CloudMachineSession with an app link whose resolver refuses the connect, with no network:
aRunningMachineWhoseLinkDroppedReadsDisconnectedNotFailedToStartanEndedLinkReadsTheMachineListAgainAndShowsTheIdlePauseaMachineTheProviderFailedStillReadsFailedToStart(a guard: green before and after)aConnectThatTheProviderFailedReadsFailedToStart(running and provisioning) andaRevokedLinkReadsDisconnectedaListReadThatBeganBeforeAConnectDoesNotPauseItaListReadThatBeganBeforeAConnectKeepsTheMachineLive
Red and green, per commit:
- The first two are red at 8c7cf9b2b23 and green at 0e39a64cee3.
aConnectThatTheProviderFailedReadsFailedToStartis red at e99f16e293a and green at 6593edf7fc9.aListReadThatBeganBeforeAConnectDoesNotPauseItis red at 9e7652d999c and green at ce4320802fc.aListReadThatBeganBeforeAConnectKeepsTheMachineLiveis red at 09d89233b6c and green at the head.
CI will show.
Observed / Not run
- Observed:
scripts/cmux-next/prepush-gate.pyreports no new violations. Its warnings: App growth of +8 lines, and a notice that the change touches the sidebar owner's area. - Not run: the Swift build and the tests. This was a Linux lane with no local Swift, so the red and green results above come from CI, not from a local run. No real-app capture yet.
Changelog
Fixed: a Cloud machine whose link dropped after it ran now reads "Disconnected" (or "Paused (idle)" when the idle auto-pause parked it) instead of "Failed to start".
risk:normal
Captures: pending (before/after of a Cloud header whose link dropped)
Agent session: agent session
š¤ Generated with Claude Code