Skip to content

BL-16690 Honor a collection's minimum Bloom version, upgrading in place - #8213

Open
andrew-polk wants to merge 2 commits into
Version6.4from
BL-16690-standard-update-path
Open

BL-16690 Honor a collection's minimum Bloom version, upgrading in place#8213
andrew-polk wants to merge 2 commits into
Version6.4from
BL-16690-standard-update-path

Conversation

@andrew-polk

@andrew-polk andrew-polk commented Aug 17, 2026

Copy link
Copy Markdown
Contributor

BL-16690: a collection can declare, in its .bloomCollection file, the oldest Bloom allowed to
open it (<MinimumBloomVersion>6.6</MinimumBloomVersion>, hand-edited — no UI). An older Bloom is
refused the collection and offered two ways forward: Upgrade Bloom (in place, through
Velopack) or Open a Different Collection. This PR is the whole feature, in two commits meant
to be read separately:

  1. The gate (cbc0cb5876) — reading and honoring the minimum, keeping it alive through
    CollectionSettings.Save(), Team Collections (the repository's copy governs, including
    mid-session lock-out when an administrator raises it while a member works), and the startup
    funnel in Program.cs. Details in its commit message; previously reviewed as BL-16690 Honor a collection's minimum Bloom version, upgrading in place #8206.
  2. The upgrade path (d291122539) — clicking Upgrade Bloom drives exactly the machinery
    behind the ordinary "new version available" toast, shown live in a progress dialog.

The upgrade path, briefly

  • UpdateReporter splits what the update code says from where it goes: ToastUpdateReporter
    keeps today's toasts exactly; ProgressUpdateReporter writes the same sentences plus Velopack's
    real percentage into a progress dialog, and carries the outcome back to the caller.
  • One window, always in front. The dialog stays up for endings that need reading (failure,
    offline, nothing-newer). On success it closes itself and Bloom restarts — the restart is the
    news, and Velopack shows its own progress while installing.
  • Cancel really cancels: Velopack abandons the transfer via a cancellation token; nothing is
    left downloaded, nothing installs on exit — even when the cancel lands in the same instant the
    download finishes.
  • If Bloom's own check is already running (it starts one 60s after a collection opens — on a
    slow connection that download can run 20+ minutes), the dialog says so, shows that download's
    live percentage while waiting it out, then proceeds through the ordinary path. Cancelling the
    wait abandons the wait, not Bloom's own download.
  • A progress dialog before any collection exists: ProgressDialogApi moves to
    ApplicationContainer (its handlers are static; registering per-project would throw on the
    duplicate key), and a websocket server is stood up for the duration — or reused mid-session,
    which is why BloomWebSocketServer.Dispose now clears Instance.

Bugs found along the way

  • The download size was reported as (0K) for any full download (pre-existing in the toast;
    now user-visible). Reports (88415K).
  • With Quiet verbosity, a failure to reach the update server was computed but never shown —
    an offline user got an empty dialog. Finished now says it if nothing else has.
  • MessageBoxIcon.Warning silently draws no icon (Warning == Exclamation, and the TSX only
    handles "warning"/"asterisk"); documented at the conversion, and the gate dialog asks for none.

Verified live (real Velopack feed, 6.4.105 → 6.4.108, no synthesized input)

  • The upgrade end-to-end: one click, bar 0→100, dialog closes itself, Update.exe installs,
    Bloom restarts into the same collection.
  • Cancel mid-download → clean exit, nothing installed, sq.version untouched.
  • Nothing-newer and offline (dead per-process proxy) endings, each readable in the dialog.
  • The startup gate reading a Team Collection repository's copy of the settings.
  • The mid-session Team Collection lock-out, including upgrading through it.
  • Waiting out an in-flight background download with its live percentage, and cancelling during
    that wait (the background download completed unharmed).

Not exercised: a network share vanishing between lock-out and reopening (the session-memory
fallback), and the two-hour cap on a wedged download.

🤖 Generated with Claude Code


This change is Reviewable

Devin review

Comment thread src/BloomExe/Collection/MinimumBloomVersionCheck.cs
Comment thread src/BloomExe/Collection/MinimumBloomVersionCheck.cs
Comment thread src/BloomExe/ApplicationUpdateSupport.cs
Comment thread src/BloomExe/ApplicationUpdateSupport.cs
@andrew-polk

Copy link
Copy Markdown
Contributor Author

[Claude Opus 5 from Andrew Polk's machine during preflight]

Consulted Devin — 2026-08-18, up to commit 577ffd2dbd3bd830f81e6bba3e48d349f02dd31d

Five rounds, one per pushed commit. Devin found real bugs in this work each time; here is the record.

Two of them were severe and would have shipped.

  • Registering ProgressDialogApi at application level while ProjectContext still registered it too threw on a duplicate dictionary key, so every collection failed to open — "Bloom had a problem loading the … collection". My own live testing never caught it, because the collection I was testing with is one Bloom deliberately refuses to open, so no ProjectContext was ever built. Reproduced deliberately to confirm, then fixed in 8870d7af.
  • The mid-session Team Collection lock-out started a second websocket server on the port the open collection already held, which makes Init report that Bloom "cannot start properly" and quit. Fixed in 8870d7af, and then again properly in 41ac07ae once Devin pointed out that Instance is never cleared on Dispose, so the check could be answered by a dead server.

Also fixed from its findings: an upgrade could be handed to Velopack twice; a Cancel landing in the same instant as the download completing would restart Bloom anyway (fixed, then fixed properly in 118fa8a0 when Devin showed the first guard never fired); the toast and the dialog could both start a download; two paths reported "nothing newer available" after offering an update they had just found; a missing doc comment; and a mojibaked em dash plus a stray UTF-8 BOM I had introduced.

One suggestion taken and then reverted: disposing the reporter's wait handle. Three attempts each had their own race, because there is no safe moment to release it while a cancelled download may still signal it. 577ffd2d documents why it is deliberately not disposed instead.

One assessed as not an issue and resolved with the reasoning on its own thread: the non-Windows #if !__MonoCS__ wait, which cannot be reached because that symbol is not defined in any build config.

Three left open for the developer, each on its own thread above. They are all really one question — what should Cancel mean? Today the download continues and installs on next exit, the usual "update is ready" toast is suppressed on that path, and if a background check is already running the button cannot help at all.

Devin reports no open bugs at this commit; all twelve it raised across the five rounds are marked resolved, with reasons naming the fixes. Informational items not mirrored here: 27. CI (pr-automation, track) green. CodeRabbit has auto_review.enabled: false in this repo, so it does not review.

@andrew-polk
andrew-polk force-pushed the BL-16690-standard-update-path branch 3 times, most recently from 8618989 to 9f829fc Compare August 19, 2026 17:46
@andrew-polk

Copy link
Copy Markdown
Contributor Author

[Claude Opus 5 from Andrew Polk's machine during preflight]

Consulted Devin again — 2026-08-19, up to commit 9f829fcf4d1a92adaee0e140689c03a39ed464a0

Three further rounds, after Andrew's decisions came back. No open bugs at this commit.

The headline decision was that Cancel must actually cancel — "definitely can't have a button which doesn't cancel". Velopack now receives a cancellation token through the reporter and abandons the transfer. Verified against a real download: cancelled at 30%, then Bloom was exited gracefully so any exit handler would have run — no installer ran, sq.version unchanged, no completed package left behind.

Devin found three things in that work, all fixed:

  • The one-download-at-a-time guard lumped "already finished downloading" in with "still downloading" and reported the finished state as a failed attempt, so someone asking to upgrade could be sent away to pick another collection while the new Bloom sat ready.
  • A cancel landing in the same instant the transfer completed still left an install arranged for the next exit, because the await returned normally rather than throwing. That is now checked explicitly, which is what makes Cancel mean it even in that sliver.
  • Clearing the shared websocket reference on shutdown (needed so this dialog can tell a live server from a dead one) could turn a book upload finishing as a collection closes into a crash. Two call sites were relying on the static pointing at a disposed-but-non-null server, where sending was harmlessly a no-op. Both now check.

Also carried out from the decision report: the NothingNewerAvailable message is shortened; the dead UpdateOutcome/UpdateResult are deleted; the two-hour backstop and the undisposed wait handle are kept with the decisions recorded as code comments; and the gate dialog no longer asks for a warning icon, since nothing in this ticket calls for one and no working way to request one exists — that gap is documented where the conversion happens.

Two threads above remain open by choice: the "a background check is already running" case, where Andrew asked for the user experience to be spelled out first (posted there), and verifying the offline path live, which is now a checklist item in the PR description.

The branch is squashed to a single commit, as asked. Suites: 2871 passed, 1 failed — the opt-in Reading App Builder real-build test, which needs a toolchain this machine lacks.

@andrew-polk
andrew-polk force-pushed the BL-16690-standard-update-path branch 6 times, most recently from 17cb2ae to 410ae82 Compare August 19, 2026 20:04
Comment thread src/BloomExe/Collection/MinimumBloomVersionCheck.cs
Comment thread src/BloomExe/UpdateReporter.cs
Comment thread src/BloomExe/web/BloomWebSocketServer.cs
@andrew-polk

Copy link
Copy Markdown
Contributor Author

[Claude Opus 5 from Andrew Polk's machine during preflight]

Consulted Devin — 2026-08-19, up to commit 410ae8291785dcd0bbd240113fe45953c647a10b

Eight rounds on the second commit, which makes a caller join an update attempt already in flight rather than being told Bloom is busy. No open bugs at this commit.

That commit exists because of the decision recorded on the thread above: the "Bloom is already working on checking for updates" case looked like a refusal even though the upgrade was under way. What decided it was not frequency but bandwidth — Bloom's own check runs once, sixty seconds after a collection opens, so the window is that check plus its download: about twelve seconds here, but over twenty minutes at 500 kbit/s, and those users are the ones a lock-out hurts most.

It is by far the most interaction-heavy change in this PR, and Devin found seven defects in it, which is a fair comment on that. Two would have broken Bloom outright and both were mine, made minutes earlier:

  • The dialog's own reporter was left out of the watcher list on the "an update was found earlier and is now accepted" path, so it watched a window that never moved, with a Cancel that could not stop the download.
  • Then, after I simplified that away, the watcher list was replaced when a check turned into its download — dropping anyone who had joined during the check. That is precisely the case the commit exists to fix.

The other five, each real:

  • joinable.Add(reporter) ran while holding the status lock, and joining replays messages to the UI — a deadlock waiting to happen, and against the rule the comment two lines above it stated. Found by reading my own diff rather than by Devin.
  • The "Update Now" toast's accept action closed over the watcher list and handed it to itself, which my own self-reference guard then rejected outright, so nothing downloaded for anyone with automatic updating off. Fixed by never passing the list where an individual reporter belongs.
  • A CancellationTokenSource cannot be un-cancelled, so re-arming a list for a further attempt carried an old cancel into it and abandoned the next download silently. Re-arming is gone entirely; a new attempt gets a new list.
  • The restart toast could be handed a null restart action on the dialog's path, and mid-session that toast really renders.
  • Replaying the backlog to a joining toast reporter announced a check the user never asked about. Only a watcher that wants catching up gets it now — and a Verbose manual check gets its immediate acknowledgement back, which that fix had removed.

Three further flags were assessed rather than changed, each with the reasoning on its own thread: the mid-session dialog's UI-thread affinity (unchanged by this PR — the message box before it was also modal), the undisposed cancellation registrations (the decision Andrew already recorded for the wait handle covers them), and the Instance-on-Dispose change, where I did act — two callers were relying on the static pointing at a disposed-but-non-null server, including a book upload finishing as a collection closes.

Verified live on each iteration: a real upgrade 6.4.105 → 6.4.108, and a cancel at 30% followed by a graceful exit which ran no installer and left sq.version untouched. The joining path itself is still not exercised — reaching it needs a Team Collection whose repository changes while a check is in flight, which the rig cannot set up.

Suites: 2871 passed, 1 failed — the opt-in Reading App Builder real-build test, which needs a toolchain this machine lacks. One run also failed ProblemLanguages_KeepsAsteriskMatch, a network-dependent license-checker test in a file this branch does not touch; it passes 3/3 in isolation and did not recur.

The fix-and-re-review cycle ran far past preflight's cap of four. Each round found something real, so stopping earlier would have meant shipping it.

@andrew-polk
andrew-polk force-pushed the BL-16690-velopack-spike branch from 69225b1 to cbc0cb5 Compare August 19, 2026 23:17
@andrew-polk
andrew-polk force-pushed the BL-16690-standard-update-path branch from 410ae82 to d291122 Compare August 19, 2026 23:17
@andrew-polk andrew-polk changed the title BL-16690 Upgrade through the normal update path instead of a private one BL-16690 Honor a collection's minimum Bloom version, upgrading in place Aug 19, 2026
@andrew-polk
andrew-polk changed the base branch from BL-16690-velopack-spike to Version6.4 August 19, 2026 23:20
@andrew-polk
andrew-polk force-pushed the BL-16690-standard-update-path branch from 6d2145e to 4ee539d Compare August 19, 2026 23:32
Comment thread src/BloomExe/Collection/MinimumBloomVersionCheck.cs
Comment thread src/BloomExe/Collection/MinimumBloomVersionCheck.cs
@andrew-polk

Copy link
Copy Markdown
Contributor Author

[Claude Fable 5 from Andrew Polk's machine during preflight] Consulted Devin on this PR up to 4ee539da57a001b06014f081c694ab5c2ac586d1 (two rounds, this preflight run).

Round 1 (on d291122539): one non-severe bug — the cancel-vs-finish sliver — mirrored and answered in this thread (left as a documented, reviewed decision; also raised in the preflight report for the developer). Of six informational flags, three were acted on in the squashed commit (two stale comments fixed, a write-only property removed), one was factually mistaken (ParseOrNull still has a caller), and two describe deliberate choices documented in code.

Round 2 (on the final 4ee539da57): no bugs. One Investigate flag — the repo's empty answer lifting a local requirement — confirmed as the intended design in this thread. Remaining informational flags describe pre-existing behavior (a failed attempt parking update checks for the session) or rare cosmetics (the bar restarting after a waited-out download), none introduced by this PR.

CI is green on the current head; the full C# suite passes (2871, with the one known opt-in RAB environmental failure in an untouched fixture). The gate commit's content carries its byte-identical review history from #8206.

@andrew-polk
andrew-polk marked this pull request as ready for review August 20, 2026 15:41

@greptile-apps greptile-apps Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Your trial has ended. Reactivate Greptile to resume code reviews.

@andrew-polk andrew-polk left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@andrew-polk resolved 4 discussions.
Reviewable status: 0 of 19 files reviewed, 5 unresolved discussions.

@andrew-polk andrew-polk left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@andrew-polk reviewed 19 files and all commit messages.
Reviewable status: 0 of 19 files reviewed, 5 unresolved discussions.

@StephenMcConnel StephenMcConnel left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One minor comment that might not be fixable.

@StephenMcConnel reviewed 19 files and all commit messages, made 2 comments, and resolved 5 discussions.
Reviewable status: all files reviewed, 1 unresolved discussion (waiting on andrew-polk).


-- commits line 243 at r3:
What does this have to do with minimum version? Did your branch get polluted? Everything following seems unrelated.

@andrew-polk andrew-polk left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@andrew-polk made 1 comment.
Reviewable status: all files reviewed, 1 unresolved discussion (waiting on StephenMcConnel).


-- commits line 243 at r3:

Previously, StephenMcConnel (Steve McConnel) wrote…

What does this have to do with minimum version? Did your branch get polluted? Everything following seems unrelated.

Wow. Yeah, apparently it just blindly took messages from a merge or something and folded those into the squashed commit message. I'll have it fix it.
I don't pay much attention to commit messages. I guess I should.

andrew-polk and others added 2 commits August 20, 2026 15:55
A collection can declare, in its .bloomCollection file, the oldest Bloom
allowed to open it: <MinimumBloomVersion>6.6</MinimumBloomVersion>, added by
hand (no UI), in preparation for Cloud syncing. An older Bloom refuses the
collection and offers to upgrade in place or open a different one. Only
major.minor are compared; unparseable values are ignored.

The value survives CollectionSettings.Save(), which rebuilds the file. For a
Team Collection the repository's copy governs, read straight from the shared
folder at the gate. That includes LIFTING a requirement -- a refused member
never syncs, so a mistaken minimum would otherwise lock them out forever --
and a minimum arriving while a member works locks them out mid-session. When
the repository is unreachable, the local file plus anything the repository
said earlier in the session decides.

Only interactive collection opening is gated; the command-line paths and
BulkUploader are deliberately left alone.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…dialog

Upgrade Bloom now drives the same machinery as the "new version available"
toast. A new UpdateReporter separates what the update code says from where
it goes: the toasts are one reporter, unchanged; a progress dialog is
another, usable before any collection is open (ProgressDialogApi is
registered once at the application level, and a websocket server is created
for the duration, or reused mid-session).

- The download shows Velopack's real percentage; on success the dialog
  closes itself and Bloom restarts. Failure, offline, and nothing-newer
  endings stay open to be read (offline used to produce an empty dialog).
- Cancel really cancels: Velopack abandons the transfer and nothing
  installs on exit.
- If Bloom's own update check is already running, the dialog waits it out,
  showing that download's percentage, then proceeds through the normal
  path. Cancelling the wait leaves Bloom's own download alone.
- The size in "Downloading update to..." no longer reads (0K) for full
  downloads.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@andrew-polk
andrew-polk force-pushed the BL-16690-standard-update-path branch from 4ee539d to d59debd Compare August 20, 2026 22:56

@greptile-apps greptile-apps Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Your trial has ended. Reactivate Greptile to resume code reviews.

@andrew-polk andrew-polk left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@andrew-polk made 1 comment.
Reviewable status: all files reviewed (commit messages unreviewed), 1 unresolved discussion (waiting on StephenMcConnel).


-- commits line 243 at r3:

Previously, andrew-polk wrote…

Wow. Yeah, apparently it just blindly took messages from a merge or something and folded those into the squashed commit message. I'll have it fix it.
I don't pay much attention to commit messages. I guess I should.

Done.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants