Look for a newer build the moment the application starts #8

Open
jaap-jan wants to merge 1 commits from claude/version-check-startup-e06e9a into main
Owner

The first pass of the update loop waited two minutes. Every pass after it came six hours apart, which is right for a product that ships rarely — but the delay in front of the first one quietly excluded a whole way of using this application.

A client opened to reach one host and closed again is over before the two minutes are. Used that way it never checks at all: not once, not slowly, never. That is the machine ADR 0011 names as the real cost of distributing outside a store, and the mechanism to fix it was switched on the whole time and never reached.

The yield is what keeps this off the launch path. Start() is called from MainWindowViewModel.StartAsync ahead of the migration, so an inline first pass would run whatever the channel does before its own first await — Velopack reads the install layout from disk — between the user and their window. Yielding hands the rest of launch back and puts the check in a later turn. Task.Yield takes no token where Task.Delay did, so the loop body now observes cancellation at its head.

Two things deliberately unchanged:

  • The AUTOMATIC UPDATE CHECKS preference still gates the pass. "On start" means every start, not regardless of what the user asked for, and the setting is already on by default.
  • The data cost is unchanged rather than merely acceptable. A check is a few hundred bytes and the download only follows if something newer exists, so this moves the same traffic earlier without adding any. That matters most on the phone, where the same loop runs against AndroidUpdateChannel.

TheFirstPassRunsAtStart_RatherThanOnADelay drives the real loop rather than CheckOnceAsync, and waits on the pass rather than on a clock — a regression that puts a delay back does not fail on a margin, it spins until the suite's own cancellation ends it. §16.7 of the manual checks gains the sentence that reopening the application does what CHECK NOW does.

447 App tests and 153 layout tests pass.

The first pass of the update loop waited two minutes. Every pass after it came six hours apart, which is right for a product that ships rarely — but the delay in front of the first one quietly excluded a whole way of using this application. A client opened to reach one host and closed again is over before the two minutes are. Used that way it never checks at all: not once, not slowly, never. That is the machine ADR 0011 names as the real cost of distributing outside a store, and the mechanism to fix it was switched on the whole time and never reached. **The yield is what keeps this off the launch path.** `Start()` is called from `MainWindowViewModel.StartAsync` ahead of the migration, so an inline first pass would run whatever the channel does before its own first await — Velopack reads the install layout from disk — between the user and their window. Yielding hands the rest of launch back and puts the check in a later turn. `Task.Yield` takes no token where `Task.Delay` did, so the loop body now observes cancellation at its head. Two things deliberately unchanged: - **The AUTOMATIC UPDATE CHECKS preference still gates the pass.** "On start" means every start, not regardless of what the user asked for, and the setting is already on by default. - **The data cost is unchanged rather than merely acceptable.** A check is a few hundred bytes and the download only follows if something newer exists, so this moves the same traffic earlier without adding any. That matters most on the phone, where the same loop runs against `AndroidUpdateChannel`. `TheFirstPassRunsAtStart_RatherThanOnADelay` drives the real loop rather than `CheckOnceAsync`, and waits on the pass rather than on a clock — a regression that puts a delay back does not fail on a margin, it spins until the suite's own cancellation ends it. §16.7 of the manual checks gains the sentence that reopening the application does what CHECK NOW does. 447 App tests and 153 layout tests pass.
jaap-jan added 1 commit 2026-08-12 09:12:56 +00:00
Look for a newer build the moment the application starts
ci / build and test (pull_request) Failing after 8s
ci / desktop nightly (pull_request) Skipped
ci / api image (pull_request) Skipped
ci / android head (pull_request) Failing after 21s
e02491a6ca
The first pass of the update loop waited two minutes. Every pass after it came
six hours apart, which is the right interval for a product that ships rarely —
but the delay in front of the first one quietly excluded a whole way of using
this application.

A client opened to reach one host and closed again is over before the two
minutes are. Used that way, it never checks at all: not once, not slowly, never.
That is precisely the machine ADR 0011 names as the real cost of distributing
outside a store — quietly a year behind — and the galling part is that the
mechanism to fix it was switched on the whole time and simply never reached.

The delay's own argument is recorded in the diff it is being removed from, and
it was not a bad one: nothing anybody does in their first two minutes depends on
an update, and launch is already contending for the network with a schema
migration, a resumed sign-in and a first sync, at the one moment somebody is
watching the window. What it weighed was the cost of checking early against the
benefit of checking early. It never weighed the cost of not checking at all.

◆ THE YIELD IS WHAT KEEPS THIS OFF THE LAUNCH PATH, AND IT IS NOT DECORATION.
Start() is called from MainWindowViewModel.StartAsync ahead of the migration, so
an inline first pass would run whatever the channel does before its own first
await — Velopack reads the install layout from disk — between the user and their
window. Yielding hands the rest of launch back and puts the check in a later
turn, which is the same moment in every sense anybody can perceive and none of
the cost. So the answer to the delay's argument is not that it was wrong; it is
that a yield buys most of what two minutes bought.

Task.Yield takes no token where Task.Delay did, so the loop body now observes
cancellation at its head. Without that, an application closed during launch
spends its last moment asking a release channel about a build it will not run.

Two things deliberately not changed. The AUTOMATIC UPDATE CHECKS preference
still gates the pass — "on start" means every start, not regardless of what the
user asked for, and that setting is already on by default. And the data cost is
unchanged rather than merely acceptable: a check is a few hundred bytes and the
download only follows if something newer exists, so this moves the same traffic
earlier without adding any. That matters most on the phone, where the same loop
runs against AndroidUpdateChannel.

TheFirstPassRunsAtStart_RatherThanOnADelay drives the real loop rather than
CheckOnceAsync, which is the one thing that file otherwise avoids — and here it
is the point, because the claim is about when the pass happens rather than what
it does. It waits on the pass and not on a clock, so there is nothing to be
flaky about: a regression that puts a delay back does not fail on a margin, it
spins until the suite's own cancellation ends it. DisposingStopsTheLoop keeps
its assertion and gains a note that it is now a race rather than a formality.

§16.7 of the manual checks gains the sentence that reopening the application
does what CHECK NOW does. It is the step somebody following that section would
otherwise discover by accident.

447 App tests and 153 layout tests pass.
Some required checks failed
ci / build and test (pull_request) Failing after 8s
ci / desktop nightly (pull_request) Skipped
ci / api image (pull_request) Skipped
ci / android head (pull_request) Failing after 21s
You are not authorized to merge this pull request.
This pull request can be merged automatically.
View command line instructions

Checkout

From your project repository, check out a new branch and test the changes.
git fetch -u origin claude/version-check-startup-e06e9a:claude/version-check-startup-e06e9a
git checkout claude/version-check-startup-e06e9a
Sign in to join this conversation.
No Reviewers
No labels
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: DodoTech-Public/DodoSSH#8