cmux NEXT: never offer the running build as Update Ready
Right after NIGHTLY updated itself and relaunched (Leo, 2026-10-20 18:55), the sidebar showed "Update Ready / Automatic Updates / Restart to Update" again, with no version line under the title. Leo's rule: updates just install, and cmux never offers the build that is already running.
What was wrong
Two gaps let the card show for a build that is not newer than the running one:
- Sparkle's silent install never told cmux which build it staged. With
SUAutomaticallyUpdateon (the NIGHTLY defaults have it), Sparkle's own automatic driver stages an update throughupdater(_:willInstallUpdateOnQuit:immediateInstallationBlock:).UpdateDriverturned that into "Restart to Complete Update" but kept no item and cleared the detected version (UpdateDriver+SPUUpdaterDelegate.swift:58-67).UpdaterService.indicatorPhase(UpdaterService+Indicator.swift:13) then readstagedUpdate == nilanddetectedUpdateVersion == nil, giving.ready(version: nil). That is a card with no version line, which matches the screenshot. - Nothing compared a staged update with the running build.
UpdateIndicatorPhase(_:version:)maps every auto-update.installingstate to.ready, andUpdateFlow.footerPillandreadyCardshow any.ready. Sparkle can come back with an update for the build that now runs. After a relaunch,SPUUpdaterprobes for an installer that is still running (probeInstallerInProgressForHostBundleIdentifier) and, if it finds one, resumes it (resumeInstallingUpdate). It can also resume a download it kept (_resumableUpdate). In both cases it reports the installer's own item, so the build just installed came back as Update Ready.
Fix
UpdateDriverkeeps the item Sparkle passes towillInstallUpdateOnQuit(quitInstallItem).stagedUpdatereturns it while that "Restart to Complete Update" state holds, so the card gets its version, notes and build. The item is cleared when the update installs or a new download starts.UpdaterService.indicatorPhasedrops a.readyphase unlessoffersStagedUpdate(version:)passes. That check compares the stagedsparkle:versionwith the runningCFBundleVersion(SUStandardVersionComparator); with no build it falls back to "display version differs from the running one"; with neither it returns false. The flow, the card, the footer pill and the circle all read this phase, so none of them shows a build that equals or is older than the running one, or one whose version is unknown.
Changelog
- Fixed: After an update relaunches cmux, the sidebar no longer offers "Update Ready" again for the build that is already running.
Tests
Fleet runs (cmux-ci run --class light --script scripts/ci/package-test-lane.sh --arg=suite ..., Xcode 26.6):
- CmuxUpdater,
BackgroundInstallTests:- Red on 970e8743f5639:
sparklesSilentInstallNamesTheStagedUpdatefails (stagedUpdate?.versionString → nil); the other 12 pass. - Green on 59a5f1273445c: 13/13 pass.
UpdateControllerPipelineTestsandUpdateStateModelTestsalso pass.
- Red on 970e8743f5639:
- CmuxNext,
UpdateReadyNotNewerTests:- Red on 970e8743f5639 plus #22821's one-line palette fix: 4 of 5 fail, with 10 issues. The card shows "Update Ready" with detail "1.0.0" for the running build, "0.9.9" for an older one, and
nilfor an unknown one, andflow.phasestays.ready("1.0.0"). The newer-build control passes. - Green on 59a5f1273445c plus the same palette fix: 5/5 pass.
SilentBackgroundUpdateTests,UpdateLoadingIndicatorTests,UpdateCheckFeedbackTests,UpdatedCardCurrentBuildNotesTestsandUpdateReadyCardLayoutTestsalso pass (41 tests in all).
- Red on 970e8743f5639 plus #22821's one-line palette fix: 4 of 5 fail, with 10 issues. The card shows "Update Ready" with detail "1.0.0" for the running build, "0.9.9" for an older one, and
CmuxNext does not compile on base bc71d0f292889 (RegistryPaletteProvider+Families.swift:58: missing return, from #22764; the fix is open as #22821). The CmuxNext runs therefore used throwaway commits that add only that one-line return on top of each commit here. Those scratch branches are already deleted. Until #22821 lands, this PR's own CmuxNext compile is red for the same base reason.
Observed / Not run
- Observed: the fleet red and green runs above.
python3 scripts/cmux-next/prepush-gate.pypassed on both commits. I read Sparkle 2.10'sSPUUpdater,SPUBasicUpdateDriver,SPUUIBasedUpdateDriverandSPUAutomaticUpdateDriversources to trace the resume and silent-install paths. - Not run: no local build. I did not reproduce the relaunch on a real NIGHTLY, and Leo's
cmux update logfrom 18:55 was not read. So which Sparkle path produced his card (a resumed installer, a kept download, or a genuinely newer nightly published a minute later) is inferred, not confirmed. If it was a newer nightly, the card is correct, and with this fix it now shows that version. - Not changed: on a resume, Sparkle may still ask cmux's driver to install the same build again; this PR only keeps it from being offered.
🤖 Generated with Claude Code
Agent session: cosmic-gray