Cloud: a dropped app link reads Disconnected, not Failed to start

Closed
TE
6d ago
+574-14

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.disconnected or .revoked) reads "Disconnected" (CloudMachineStageInput.isDisconnect). "Failed to start" still appears for a failed create, a cloud.machine.connect the provider refused (CloudLinkError.failed and the rest), a failed resume, and a machine whose record says failed.
  • CloudMachineSession.showEnded now 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. refresh notes each session's connect count when its read begins (CloudMachineSession.connects), and CloudMachineSession.listed(_:connectsAtRead:) (the per-machine part of reconcile, 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.disconnected is in CmuxNextSidebar/Resources/Localizable.xcstrings for every locale, needs_review like its sibling strings.
  • plans/cmux-next/sidebar-owner.md has 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:

  • aRunningMachineWhoseLinkDroppedReadsDisconnectedNotFailedToStart
  • anEndedLinkReadsTheMachineListAgainAndShowsTheIdlePause
  • aMachineTheProviderFailedStillReadsFailedToStart (a guard: green before and after)
  • aConnectThatTheProviderFailedReadsFailedToStart (running and provisioning) and aRevokedLinkReadsDisconnected
  • aListReadThatBeganBeforeAConnectDoesNotPauseIt
  • aListReadThatBeganBeforeAConnectKeepsTheMachineLive

Red and green, per commit:

CI will show.

Observed / Not run

  • Observed: scripts/cmux-next/prepush-gate.py reports 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