cmux NEXT updater: Restart to Update installs the newest build; a pulled build is dropped, never offered
Leo (2026-10-29): "those kinds of download/update things need to be more "latest" instead of me downloading whatever was the first one... but if there's multiple nightlies, i should only need to do one to get the latest"
Leo (2026-10-29), on the pulled-build banner: "what the f*** is this nightly was pulled, and why the f*** are the buttons like that? ... what the f*** does this nightly was pulled even fing mean? The rollback. I mean, why the f was it pulled? Like, why the f*** am I even getting a thing that I can download that I can just— that was pulled in the first place? Just f***ing cancel it, or, like, do something else about it. Just don't have that be the downloading thing."
Before: the sidebar showed "This Nightly Was Pulled" with Wait for the Fix and Roll Back (Leo's screenshot, not reproduced here).
After (the running build was pulled; the same card without the second line shows while Restart to Update fetches a newer build):
Capture tour-update-newest at e99ac550f29b (fleet build, capture mini, text checked for private data)
| Light | Dark |
|---|---|
![]() | ![]() |
Viewed after image: yes
Visual verdict: The sidebar footer card reading Getting the Newest Build with its pulled-build note and progress bar sits cleanly in light and dark, and it is unchanged at 15fffb8b since the port only hides cards for a held pulled build.
Takes back #25725: nothing of its behavior. This PR rewrites PulledBuildWatchTests (which #25725 extended for the Updated card's pulled-build count) because the banner those tests covered is removed. pulledBuilds and the Updated card's use of it stay, and theWatchReadsTheListAndTheRunningBuildsEntry still checks that every pulled build is read.
Changelog
Fixed: Restart to Update installs the newest build on the feed, and a pulled build is never offered.
What happened
Nightly 4091813159501 was staged at 18:16Z. By 18:19Z the feed already listed 4092299464301. Restart to Update installed the stale 4091813159501 anyway. That build was then pulled (it crashed in the fresh-home smoke), so the app showed "This Nightly Was Pulled" with Wait for the Fix and Roll Back, and a second update was needed.
The click already re-read the feed before installing (StagedUpdateRefresh.beforeInstall), but only when found updates download by themselves. Nightly asks before it downloads (updates.downloadAutomatically is off by default), so on the default setup the click never re-read the feed.
Change
- Restart to Update always ends on the newest build. The click re-reads the feed and
pulled.jsonwhether downloads are automatic or not. The feed has the existing 3 s deadline; a staged build is never installed beforepulled.jsonanswers (each read has a 15 s request timeout), and only a newer build this app can swap to settles the click sooner. Ifpulled.jsoncannot be read at all, the click installs, as before: the list is the only word that a build was pulled. The click itself is the person's yes to the download. If the feed has a strictly newer build, or the staged build was pulled, the staged installer is cancelled and the newest build downloads with no second click. The click carries over and installs it once it is staged. The notice card reads "Getting the Newest Build…" with progress and no buttons. - A pulled staged build is dropped quietly and the newest good build downloads in its place. The Update Ready card then asks before the relaunch, as every update does. Each pulled build is dropped only once (a cached appcast that stages it again is dropped without another fetch), so nothing loops.
- A pulled running build fetches and downloads the newest good build, once per launch. The card says one line: "This build was pulled." The relaunch still asks. With nothing newer on the feed, nothing shows.
- Removed: the "This Nightly Was Pulled" card, its Wait for the Fix and Roll Back buttons, the rollback wiring and its strings. The palette and menu's "Roll Back to Previous Version…" stays.
- The CI side is a separate PR: the fresh-home smoke gates the appcast write, so a build that fails it is never offered.
Stop condition and cleanup: newestFetch ends when the newest build is staged, when the check ends with nothing or fails, or when an install starts. No timer and no polling: the logic runs when the pulled list is read (launch, each update cycle, the click) and when a build is staged or found.
Carry fixes (review BLOCKs at b361383f and a3718b91)
Carried after the owning session ended, red tests first (UpdatePulledEdgesTests; red at 5830032c, 0e10b7da, ed9cb0d9, 78611ce2, e8b2f6ca, 48594c94 (BackgroundInstallTests) and cdd861fe (PulledBuildWatchTests); green at c7e82785):
- A pulled list slower than the click's 3 s deadline no longer loses a newer build: the deadline settles on the feed's answer, and a pulled answer that arrives later still settles the click.
- With an installer this app cannot cancel (Sparkle's own driver), every click reads the pulled list first, and a pulled staged build is never installed; the update sheet opens instead.
- The click waits for the pulled list before installing, judges the build staged when it settles (Sparkle may stage another while it waits), and with no cancellable installer never installs before the list answers.
- A pulled running build the list reports while a check runs is followed once that check ends, and a legacy feed's two pulled lists are read at once.
- Ported from the parallel carry #28074: Sparkle's driver dismisses a known-pulled found build before any download or prompt (
UpdateController.refusesUpdate), and a pulled running build still updates past a pulled staged build no one can cancel. - A staged build known pulled that this app cannot cancel shows no card, footer pill or ready card (also after a check the person asks for).
- A found build that is already known pulled is never offered as "Update Available" and never downloads, also when downloads ask.
- A carried fetch for the newest build ends when a pulled found build is dismissed, so "Getting the Newest Build…" never stays up.
Tests (fleet, cmux-ci run package-test-lane.sh suite)
At head c7e82785 (fleet steps 8f461828 and the CmuxUpdater BackgroundInstallTests run): UpdatePulledEdgesTests, UpdateClickInstallsNewestTests, PulledBuildWatchTests, UpdateAsksBeforeInstallTests, UpdateReadyNotNewerTests and UpdateInstallStatusTests pass. Earlier heads also passed SidebarCardSurfaceTests.
UpdateStagedYieldsToNewerTests and TipUpdateCheckTests fail at this head only in tests that also fail on base e219b3ac (fleet step 6f10f824) with the same assertions (h.feed.fetches counts and tipCheck.timer.isScheduled), and at this PR's original head b361383f (step 32fb9aab). They are not from this PR.
UpdateClickInstallsNewestTests.withDownloadsAskingOneClickStillInstallsTheNewestBuildcovers the case from Leo's Mac: 300 staged, 500 on the fake appcast, downloads asking. One click cancels 300, shows "Getting the Newest Build…", downloads 500 and installs it once staged.UpdateClickInstallsNewestTests.aClickNeverInstallsAPulledStagedBuildPulledBuildWatchTestscovers: the quiet swap of a pulled staged build, no loop when it is staged again, the one-line update of a pulled running build, and a quiet end when nothing is newer.
Observed / Not run
Observed: the suites above pass on the fleet at this branch (one run showed UpdateStagedYieldsToNewerTests.aScheduleChangeReplacesThePendingProbe failing once; see the comment below for the rerun). The capture above is a fleet build driven on a capture mini.
Not run: the new click test on base (it uses the cancellable and pulled-list seams this PR adds). By reading, base's beforeInstall returns install() at once when downloadsUpdatesInBackground is false, the nightly default. No live install against the real feed.
Risk: high (updater). It changes the install click's path when downloads ask, and removes a card. The existing click and staged-refresh suites still cover the automatic-download path.
After #28074
#28074 landed this PR's first part on feat-cmux-next (the click reads the feed, a pulled staged build is dropped, a pulled running build updates). This PR now carries only the remaining edge fixes on top of it, merged with base's updater as the foundation:
- The install click reads the pulled list even with no installer it can cancel, never installs a staged build before the list answers (only a newer build it can swap to settles the click sooner), and judges the build staged now.
- A pulled running build read while a check runs is followed when the check ends.
- The legacy feed's two pulled lists are read at once.
- A staged build known pulled that no installer can drop shows no card, footer pill or Update Ready card. A quit cancels it instead of installing it when this app holds its installer; while Sparkle's silent cycle still owns the installer (the same window base has), nothing can cancel it, so the quit proceeds and the log says Sparkle installs it (
15fffb8b; red at69cfba14, fleet stepe77984d7da5c0d4898060387; green at15fffb8bwithPulledBuildWatchTestsandUpdateClickInstallsNewestTests, step094ee35676f90668c3c664f7).
Red/green: UpdatePulledEdgesTests on base df9b52b7 failed 7 of 10 (fleet step e615cb37eb6282d90c7c9144); at d3ee6669 it passes with PulledBuildWatchTests, UpdateClickInstallsNewestTests and ReleasePushListenerTests (step 1950f9f569021aabfadd5553) and CmuxUpdater BackgroundInstallTests (step a2e995b7bdacad2ea5797425). UpdateStagedYieldsToNewerTests and TipUpdateCheckTests fail on base 00f9b034 with the same tests (step 25fefa55cec8a2b85c076ef6); aScheduleChangeReplacesThePendingProbe flips on base too (step fd0843a948e415f92b4033dd) and passed at d3ee6669 on a rerun (step 10b7798b17b91e9805f3686f).
Agent session: agent session
🤖 Generated with Claude Code

