Repository navigation
cmux NEXT: never offer the running build as Update Ready - #22804
Conversation
…e Ready (red) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?harness=claude&session=01a15c01-a42d-7970-a4a7-2f0ac203c940&host=cosmic-gray
…e Ready Sparkle's silent driver staged through willInstallUpdateOnQuit without telling the host which item, so the card read .ready(version: nil) and nothing compared it with the running build. The driver now keeps that item as the staged update, and the indicator phase drops a staged update whose build is not newer than CFBundleVersion, or whose version is unknown. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?harness=claude&session=01a15c01-a42d-7970-a4a7-2f0ac203c940&host=cosmic-gray
|
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)
Not verified: macOS jobs did not run: compile and app tests were skipped on CI passes on Written by |
|
Bead |
|
owner: 01a15c01-a42d-7970-a4a7-2f0ac203c940 |
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. |
…er a newer version
The Debug Settings "Simulate pending update" switch and debug.updater
{action: "simulate"} move from the App target into a new CmuxNextUpdater
type, UpdateSimulation. The App passes the switch to UpdaterService.attach,
which follows it, and DebugUpdater's stage and simulate actions are thin
calls (UpdateSimulation.stageSample, UpdateSimulation.simulate).
CmuxNextApp source lines no longer grow over feat-cmux-next (95249 at
base and merged), and neither the UpdaterService nor the AppServices type
grows (check-no-godfiles with --base passes on the merged tree).
The simulation now stages a version that is not the running one: the
notes' own when they are a newer build, else the pretend 1.99.0, so the
gate that never offers the running build (#22804) still shows the card on
the newest nightly; the real notes stay. Turning the switch off cancels a
simulation still reading its notes. Builds of one day order by number.
A click that cannot open the preview no longer leaves it pinned, and
Control-click on the card opens its context menu again.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Agent-Session: https://cmux.com/deeplink/agent?harness=claude&session=01a15cfc-663f-7650-b9f7-64d198c6b88b&host=cosmic-gray
|
Shipped in:
|
|
Shipped in:
|
…e, click-to-pin preview, Simulate pending update (#22688) * cmux NEXT: Update Ready card gets a themed switch, a primary Restart to Update, a click-to-pin preview and Debug Settings Simulate pending update Agent-Session: https://cmux.com/deeplink/agent?harness=claude&session=e8f63fd9-94e8-4878-bcd4-704659aba1f2&host=cosmic-gray * Update Ready card: simulate pending update in the updater module, under a newer version The Debug Settings "Simulate pending update" switch and debug.updater {action: "simulate"} move from the App target into a new CmuxNextUpdater type, UpdateSimulation. The App passes the switch to UpdaterService.attach, which follows it, and DebugUpdater's stage and simulate actions are thin calls (UpdateSimulation.stageSample, UpdateSimulation.simulate). CmuxNextApp source lines no longer grow over feat-cmux-next (95249 at base and merged), and neither the UpdaterService nor the AppServices type grows (check-no-godfiles with --base passes on the merged tree). The simulation now stages a version that is not the running one: the notes' own when they are a newer build, else the pretend 1.99.0, so the gate that never offers the running build (#22804) still shows the card on the newest nightly; the real notes stay. Turning the switch off cancels a simulation still reading its notes. Builds of one day order by number. A click that cannot open the preview no longer leaves it pinned, and Control-click on the card opens its context menu again. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?harness=claude&session=01a15cfc-663f-7650-b9f7-64d198c6b88b&host=cosmic-gray --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
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:
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.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
Tests
Fleet runs (
cmux-ci run --class light --script scripts/ci/package-test-lane.sh --arg=suite ..., Xcode 26.6):BackgroundInstallTests:sparklesSilentInstallNamesTheStagedUpdatefails (stagedUpdate?.versionString → nil); the other 12 pass.UpdateControllerPipelineTestsandUpdateStateModelTestsalso pass.UpdateReadyNotNewerTests:nilfor an unknown one, andflow.phasestays.ready("1.0.0"). The newer-build control passes.SilentBackgroundUpdateTests,UpdateLoadingIndicatorTests,UpdateCheckFeedbackTests,UpdatedCardCurrentBuildNotesTestsandUpdateReadyCardLayoutTestsalso pass (41 tests in all).CmuxNext does not compile on base bc71d0f (
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-linereturnon 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
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.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.🤖 Generated with Claude Code
Agent session: cosmic-gray