Repository navigation
Reclaim leaked CowTree pages, and prove a failed MSI upgrade rolls back exactly - #1634
Merged
Merged
Conversation
Since the disk budget became the store's page footprint, a page the store leaked counted against the budget and evicted a live entry for good. Leaks came from three places, and each is now fixed where it happens: - A crash: pages added to the file after the last durable meta were in no tree and no list, and recovery marked them live. The meta page now records the file's data-page count (meta layout version 2), and recovery frees every page past it. A version-1 meta still opens and reclaims nothing; its slot is rewritten as version 2 at the next meta write. - Fsync and None durability: only a batched flush wrote a free list, so every restart marked every free page live. Every meta write, under every durability, now goes through one FilePageStore::CommitMetaLocked that writes the list, then the meta, then truncates the free tail and releases the pending frees. Under Fsync, an extra fsync is paid only when a list page was written. The startup refusal of --storage-max-disk with a non-batched durability is deleted. - A clean close: CowTree frees replaced pages after the meta write, so the last commit's frees reached no list. The destructor now restates the last durable meta under the next txnId, with the list. Only the destructor does this, because a tree still running would reuse that txnId. The tests reproduce each leak and assert the reopened PagesInUse. Each fix was switched off in turn and its own cases went red; the Fsync/None restart case depends on two of them, so it goes red under either. A pre-existing test assumed one meta write after a damaged-slot recovery. It now asserts the exact sequence, and it still catches the #726 slot-parity bug. The rules and the operator docs say what changed, including that a None store can be refused Corrupt after a power loss. Stores written before this change keep the pages they already lost. The fsync-failure window found on the way is #1633. Closes #1624 Signed-off-by: Christian Parpart <christian@parpart.family>
FastCacheDiscardRollbackState deleted the saved service registrations as a deferred action, inside the install script. A failure after it rolled back with the state already gone, so each exact restore read a missing key and did nothing. It is now a commit action, which Windows Installer runs only after the whole script succeeded and never during a rollback. It also runs from System64Folder now, since reg.exe needs no install root. The shipped package also gains FastCacheFailForTest: a deferred action that fails the install last, only when the secure property FASTCACHE_FAIL_FOR_TEST="1" is set. It exists so that CI tests the package that ships. check-msi-custom-actions.ps1 gains step 11. It allows a rollback-state delete only in the commit phase, with deferred deleters as stated rows, and requires a scheduled commit-phase discard to exist. It states the spellings it cannot see. check-wix-service-table pins the new action, its secure property, and its place as the only action before InstallFinalize. Signed-off-by: Christian Parpart <christian@parpart.family>
…exactly (#1629) A new step in the Windows package job: - installs build N, fastcached-only, into a custom root; - upgrades to N+1 with the node added and FASTCACHE_FAIL_FOR_TEST=1; - requires the machine to equal N afterwards: both services' full registry keys, their running state, the installer values, both firewall groups, the installed product and the rollback key. Two controls must go red: a copy with the exact restores switched off, and a copy with the discard moved back into the script. The second is the defect the previous commit fixes. A final successful run of the same upgrade must change everything the failed one was required to keep. MsiServiceTable.psm1 gains the helpers, each driven by its self-test: - an installation snapshot that waits for a stable service state; - a pure, multiset comparison that names every difference; - a control copy whose patch is read back before it is trusted; - one MSI scalar reader. A failed firewall read is now an error, not an empty group. Closes #1629 Signed-off-by: Christian Parpart <christian@parpart.family>
The first CI run measured the subject green: all 10 fields were equal to build N after the forced failure. But the first control, with the exact restores switched off, also came back with no difference, so it could not show anything. Two things made it blind: - A second rollback action, FastCachedRestoreRegistration, re-registers the service on its own. It is inferred to be the restorer, and control A now pins that. - CPack's WiX generator searches the old product's InstallLocation for INSTALL_ROOT (FindInstallLocation in properties.wxi, measured), so an upgrade keeps N's custom root, and fastcached's ImagePath never changed. The fix makes N's registration differ from what any re-registration writes. After every install of N, the step appends one harmless, explicit flag (--log-level=info, the default level) through Win32_Service.Change and asserts it. A re-registration rebuilds the command line from its own argv and drops the flag; only the exact restore, which copies the live key, puts it back. The controls become three: - A: every rollback row that rewrites fastcached's registration is off, and the result must be red. - B: only the re-registration is off, and the result must be empty. That proves the exact restore works on its own. - C: B plus the discard moved back into the script, and the result must be red. B and C differ only in the discard phase. The discrimination leg requires only fields a successful upgrade really changes, and asserts that the custom root is kept. Signed-off-by: Christian Parpart <christian@parpart.family>
…roduct's own uninstall entry Uninstalling an install made to a custom INSTALL_ROOT never removed its services. CPack sets INSTALL_ROOT only by a search of Uninstall\[WIX_UPGRADE_DETECTED]\InstallLocation, so only during an upgrade. Every maintenance transaction (repair, feature change, uninstall) fell back to the default root, so each of the 33 actions that run from INSTALL_ROOT, or name a binary under it, targeted the wrong path. The uninstall logged it as Info 1721, ignored it, and removed the files, which left the service registered with nothing to run. The new CI step measured it: in the /x of build N, INSTALL_ROOT read C:\Program Files\fastcached\ while the component directories read the custom root, and FastCached stayed registered. Fix: when the product is installed, a RegistrySearch of its own Uninstall\[ProductCode]\InstallLocation feeds a property-setting action (type 51) that sets INSTALL_ROOT before CostFinalize, in both sequences. Type 51 rather than SetDirectory, because MsiSetTargetPath is not for maintenance transactions. One resolved property also fixes the working directories, the PATH entry and ARPINSTALLLOCATION. Step 12 of check-msi-custom-actions.ps1 requires that resolver whenever a scheduled action uses INSTALL_ROOT, and check-wix-service-table pins its rows. A locally built package (WiX 5.0.2) carries both rows and passes ICE63. The step's NothingInstalled assertions after every custom-root uninstall prove the behaviour in CI. Signed-off-by: Christian Parpart <christian@parpart.family>
…ed, and step 12 states what it pins Windows Installer forbids changing a directory's target path during a maintenance transaction, and the restriction covers every method, the type-51 property action included. The resolver complies because it sets INSTALL_ROOT to the product's own InstallLocation, where the product already is. The rulebook and the fragment's comment had implied that only type 35 was restricted. A product installed by a build without the resolver keeps the old behaviour for its own repair or uninstall until it has been upgraded once. That is accepted, since there is no back-compat before production ready. Step 12 now states its exclusion truthfully. It changes the count of actions judged, never the verdict, because an empty set is itself a failure. It also pins the resolver's search type, bitness and sequences. Signed-off-by: Christian Parpart <christian@parpart.family>
… every transaction is judged for what it started PR 1634's CI failed "an upgrade deselecting fastcached" with 1603: the node could not bind 6674 because FastCached held it, a service the table had registered manual and never starts. Windows Installer's Restart Manager put it there. A silent install always uses Restart Manager: at InstallValidate it shuts down each service holding a file the transaction replaces, and at the end of the install it starts that service again, whatever the service table decided. The upgrade from 0.3.0, with FastCached running, therefore started FastCached after the table had made it manual and stopped it for the node. It could not bind the port the node holds and terminated three times before msiexec returned (System log 7031, not a failure-action restart: the first said "1 time(s)"). Its 30 s recovery restart then took 6674 while a later transaction had the node stopped. The row assertion after the upgrade read Manual and Stopped, between two restarts, so the race decided which run went red, while the start itself happened on every such upgrade. Restart Manager also cost the rollback its restart. The ServiceControl rows (Stop="both" Wait="yes") stop each service before its files are replaced, and StopServices records a rollback start only for a service it found running. Restart Manager's shutdown came first, so StopServices found nothing to stop. The fix is MSIRESTARTMANAGERCONTROL=Disable, one Property row of the fragment, so the service table and the ServiceControl rows own every stop and start, and their rollback every restart. Not DisableShutdown, which keeps Restart Manager detecting files in use under a scope its documentation states conditionally. Not MSIDISABLERMRESTART=1 alone, which keeps the shutdown at InstallValidate and with it the lost rollback start. CPack's template and generated sources define no such property: a package CPack and WixUI built carries no row. msi-custom-action-commands gains step 13. It requires exactly one Property row with the value Disable, and refuses a SetProperty of it, which Windows Installer ignores. wix-service-table pins the row. Both refusals say that Restart Manager restarts a service the table left stopped. Deleting the row and setting DisableShutdown or 0 each fail both checks; a SetProperty fails step 13. The CI steps "Upgrade from the 0.3.0 MSI, then between two builds of this one" and "A failed upgrade restores the previous installation exactly" now judge every transaction for what happened DURING it. Assert-TransactionServiceEvents refuses an unexpected-termination event (7031, 7034) of either service since the transaction began. It also refuses a process that a service the row leaves Manual ran under, other than the one it ran under when the transaction began. Invoke-Msiexec watches both services while msiexec runs, because current Windows no longer logs 7036 (none among thousands of service control manager events on Windows 11 26200). Each verdict is a pure function over records. The self-test drives it over the 0.3.0 upgrade CI ran and over each thing that must not count. It drives the event parse over a real 7031's binary data, the reader against the live System log in both directions, and the watch over a real process. 103 cases become 123. No transaction of either step expects a termination, so none is exempted. The rule is in platform-service-and-config.md beside the service table. It records the residual: the upgrade from 0.3.0 still runs 0.3.0's own removal, whose package sets no such property. AGENT.md carries the rule as a tripwire on the service-table bullet, inside the line budget (two paragraphs rewrapped; the budget ratchets to 1635). doc-subject-checks.sh makes --source-dir absolute before any check runs: a relative directory named a different tree for each check that runs from elsewhere, and three of the seventeen failed on a good tree. The judgement of what a transaction did to the services is part of Invoke-Msiexec. Every call names the service table row the transaction leaves (-Leaves) or a named expectation with its reason (-Expect). A call naming neither, both or an unknown one is refused before msiexec starts. The parameters are not Mandatory, because a missing Mandatory parameter prompts, and on a CI runner that is a hang rather than a refusal. All 31 calls in build.yml state one, the steps the first change did not touch included. Two transactions cannot honestly state a row: - RollsBack: the failed upgrade, its controls and the refused-pin repair. A rollback restarts what it stopped, so no start is judged, and every termination still is. - NodeCannotBind: the step that holds 6674. The node it adds cannot bind, and CI measured that refusal logged as 7034. Assert-ServiceTable judges the same transaction once more, over the window its stability interval adds. The start check now covers what an expectation leaves NOT RUNNING (stopped or unregistered), where it covered what a row leaves Manual. So DaemonAloneUnstarted and 0.3.0's unstarted node are watched too. 7036 is a third witness. The claim that Windows no longer writes it was false: PR 1634's runner (Windows Server 2025) logs "entered the running state", and only this repository's Windows 11 26200 host writes none. The node's own start is the positive control, and the pass line says which state held: live, ABSENT, or unchecked. A transaction that starts the node and logs no 7036 is therefore named rather than read as clean. The event parse reads a 7045's ServiceName item and a 7036's state. The live self-test case asserts the filter only, so a host whose newest SCM event is an install no longer fails it. A failed CIM poll is counted and named, and msiexec is waited for on every way out of the watch. The refusal names a cause per finding kind: it blames Restart Manager for a stray start, not for a termination. Each verbose log's RESTART MANAGER lines are printed, unjudged. The rule, the fragment and both checks now say what was MEASURED (FastCached started at 12:12:47 and crashed three times) apart from what is INFERRED (that Restart Manager started it). They also say what Disable gives up: non-service processes holding a replaced file, and the stop margin a maintenance removal had. The 7031/7034 comment now follows the finite recovery steps. Self-test: 123 cases become 144. Ten neuters each go red at their own case: - terminations ignored; - the baseline ignored; - 7036 ignored; - MayTerminate ignored; - the watch's catch removed; - the control answering live; - neither accepted; - Invoke-Msiexec unjudged; - the second judgement removed; - the start check narrowed back to Manual. The Invoke-Msiexec case starts its process through a seam, so even a neutered refusal never starts msiexec. Signed-off-by: Christian Parpart <christian@parpart.family>
…s and the actions around each finding PR 1634's CI refused the upgrade from 0.3.0 for a start of FastCached nobody asked for, and its printout said less than it should have. Show-MsiLog printed three section headers over EMPTY sections, for a log a Select-String had read a moment earlier. Its lines went to the OUTPUT stream. Called from Invoke-TransactionJudgement, whose output Invoke-Msiexec captures in `Write-Host (Invoke-TransactionJudgement ...)`, they were captured, and the judgement's throw discarded them; the headers, written to the host, survived. Every line now goes to the host, and the self-test asserts Show-MsiLog writes nothing to the output stream, through that same captured shape. The RESTART MANAGER lines after each transaction were the first four, which are the outer package's own: they cut the nested removal's session they were cited for. Every line is printed now, up to a stated cap of 100, and a log past the cap says how many it cut. A refused judgement now also prints, from the verbose log: - every RESTART MANAGER line; - the lines from 30 s before to 5 s after each finding's time: actions starting and ending, service control operations, custom actions, Product: lines, a nested product's start (Running product) and Restart Manager. Windows Installer stamps its lines in the host's local time of day, with no date, so each finding's UTC time is read on that clock, unstamped lines carry the previous stamp, and a window across midnight still matches. Each finding now carries its Time for this. Self-test: 144 cases become 151. Neutered, each goes red at its own case: - the tail back on the output stream: the log-on-refusal case; - a cap of four: the restart-manager-lines case; - no window on refusal: the log-on-refusal case; - an action's stamp ignored: the log-clock case; - no midnight wrap: the lines-around midnight case. Signed-off-by: Christian Parpart <christian@parpart.family>
… package's removal has Windows Installer restart it after the upgrade The diagnostics run of PR 1634 MEASURED where the stray start of FastCached comes from, in the upgrade from 0.3.0: - this package logged "RESTART MANAGER: Disabled by MSIRESTARTMANAGERCONTROL property"; - 0.3.0's removal, which RemoveExistingProducts runs as a nested installation with 0.3.0's own properties, opened a Restart Manager session of its own and "Successfully shut down all applications in the service's session that held files in use"; - the OUTER engine then logged "Failed while restarting applications. Error: 352". That came after "Installation completed successfully" and after every action of this package, commit actions included. Between the two lines, FastCached was started and crashed on the port the node holds. No property of this package reaches that session. RemoveExistingProducts sets only ProductCode and REMOVE for the removal, and the Upgrade table has no column for a property. Nothing of ours may run before it (Error 2613), and moving it before InstallInitialize takes the old product's removal out of the transaction (#1629). An immediate action runs unelevated under UAC. DisableAutomaticApplicationShutdown is a machine policy. The restart also comes after the last action, so nothing of ours can follow it. So it is answered where it lands. FastCachedDisableForNode leaves FastCached DISABLED beside the node (`sc config FastCached start= disabled`, Return="check"), where it was manual. A start of it then fails at the service control manager: no process, no recovery restart, and no race for 6674 with the leg that has the node stopped. The upgrade already exited 0 with that 352. The binary keeps its policy: a bind conflict is an I/O arm, Failed and restarted by design, so a start that may not run must not be startable. Removing the node re-applies fastcached auto, and a failed transaction puts back the start type it found. The upgrade leg now also asserts the nested shutdown line, so its "no start of FastCached" judgement cannot pass because no restart was attempted. The NodeSelected row reads Disabled, and wix-service-table pins the action, its condition and its place. Both directions were checked: the action set to demand, and the action unscheduled, each go red. install.md says what an operator upgrading from 0.3.0 sees: the 352 line, harmless. The residual is fastcached WITHOUT the node: FASTCACHED_START_SERVICE=0 on an upgrade from 0.3.0 leaves a FastCached that was running still running, so stop it first. It also says how to run FastCached beside the node on purpose. The other pages saying "manual" now say disabled. And from the review of the judgement itself: I-A. Moving the judgement into Invoke-Msiexec made it run at msiexec's EXIT. The transactions with no Assert-ServiceTable after them lost the window past the exit that the old placement gave them. Among them is the failed upgrade, whose rollback restarts fastcached with an unwaited `sc.exe start`, so a bad restore crashes after the read. Assert-TransactionSettled now judges the last transaction once more, over a window reaching at least 10 s past the exit, and waits for the remainder, measured on a monotonic clock. Assert-ServiceTable runs it after its stability interval. The failed upgrade and every control, the successful upgrade beside them and the two repairs call it themselves. It is self-enforcing: Invoke-Msiexec refuses to BEGIN while the previous transaction has not settled, before anything is started. Only a step's LAST transaction relies on its call site, and every step ends in an Assert-ServiceTable. An AST walk over build.yml finds all 31 transactions settled before the next one, and it flags the one transaction it is shown with the settle removed. m-1. The refused-pin repair names its row, NodeSelected, rather than RollsBack, so the start witness watches fastcached during its rollback. Each named expectation is MustFail: a transaction stating one that returns 0 is refused. m-2. The 7036 control has four states. A node start whose 7036 is missing while other services' 7036s were read is refused, because the reader or its matcher failed. NOT SEEN (no 7036 at all) names both causes, and it is refused once an earlier transaction of the process read the node's 7036. m-3. A 7036 "stopped" past the one stop the starting process allows is a start. A start that dies before RUNNING still ends in a stop, and PR 1634's stray starts logged three stops and no "running". m-5. The refusal of a call naming neither expectation asserts that the start seam was never called. The comment gives the real reason the parameters are not Mandatory. m-6. NodeCannotBind's reason no longer cites a measurement CI never made. m-7 is done rather than filed. The diagnostics run showed Windows Installer's wording ("RESTART MANAGER: Disabled by MSIRESTARTMANAGERCONTROL property", on the Server 2025 runner). A transaction whose package sets the property to Disable now must log that line, or it is refused. The window around a finding reached 30 s back, and its cap of 200 cut the end of the transaction the findings followed. That is MEASURED on the diagnostics run: it stopped 27 s early. The window is now 10 s back, and the cap keeps the LATEST lines. Self-test: 151 cases become 168. Neutered, each goes red at its own case: - the unsettled refusal; - the settle wait; - MustFail; - the stops witness; - the unmatched state; - the latch; - the Restart Manager verdict; - the cap keeping the earliest lines; - the expectation resolved after the start. AGENT.md's service-table tripwire names fastcached disabled beside the node, within the line budget. Signed-off-by: Christian Parpart <christian@parpart.family>
… is registered, and this build's package must promise Restart Manager off Review round of the nested Restart Manager fix. I-1. FastCachedDisableForNode was Return="check" on a registration that FastCachedInstallService makes Return="ignore" on purpose. A refused or absent FastCached registration therefore failed the node's whole install with 1603 and rolled it back, and the log named the disable rather than the refusal. An absent service is no more startable than a disabled one, so only a registration that EXISTS is disabled now. sc.exe answers 1060 for a service that does not exist (MEASURED on this host); that one answer ends the action with 0, and every other answer goes on to the checked disable. The guard stays fail-closed for a disable that fails on a service that is there. msi-custom-action-commands step 14 runs the command for real, with every service name it spells swapped for one nothing registers: - with that name everywhere it must exit 0; - with the query naming EventLog and the disable naming the absent service, it must exit 1060. Neither run can change a service. wix-service-table pins the guard. Neutered, each goes red at its own place: - an unconditional exit 0 fails the 1060 control; - the 1060 test inverted fails the absent case; - the old unguarded command fails both checks. m-A. install.md no longer claims what a DISABLED start logs. It quotes the 352 line as MEASURED for the case where FastCached could still be started, and names the client's misleading "Previously shut down applications have been restarted.". m-B. A package's Restart Manager promise is no longer skipped silently. This build's package must set MSIRESTARTMANAGERCONTROL=Disable, or the transaction is refused. A released package is named with -Released (0.3.0 and the latest release); it is judged only if it sets the property, and a skip is printed. m-C. The comments in check-msi-custom-actions, check-wix-service-table and the fragment now say what was MEASURED: the start came from the OLD package's nested session, so the property is necessary but not sufficient. m-D. The start-type search comment names #4 (disabled). The rule says the binary's re-registration twin degrades disabled to manual, and why that is accepted. m-E. install.md now says the next transaction stops an overridden FastCached, as well as disabling it. It also states the old removal's forced close of a program holding 0.3.0's files. Self-test: 168 cases become 171. Neutered, the released switch goes red at "this build's package without the property". install.md states the refused restart's log lines as MEASURED with FastCached disabled: CI's upgrade from 0.3.0 logged "Failed while restarting applications. Error: 352", exited 0, and FastCached was never started. Signed-off-by: Christian Parpart <christian@parpart.family>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #1624. Closes #1629.
Ten commits. The first three are the concerns below: storage, the MSI rollback-state defect, and the CI step. The rest are what CI and review found along the way:
1. storage: a reopened store reclaims every page it leaked (#1624)
A leaked page counts against
--cache-disk/--storage-max-diskforever. Each leak is fixed where it happens:CommitMetaLocked, which writes the list, the meta, the tail cut and the release of pending frees. The startup refusal of a disk budget with a non-batched durability is deleted.How it was checked:
PagesInUse()is asserted exactly. Each fix was switched off in turn, and its own cases went red.2. msi: discard the rollback state only once the install has succeeded
The bug:
FastCacheDiscardRollbackStatewas a deferred action. A failure after it rolled back with the saved registrations already deleted, so the exact restores did nothing.The fix:
FastCacheFailForTest, a late failure gated on the secure propertyFASTCACHE_FAIL_FOR_TEST=1, so CI tests the package that ships.check-msi-custom-actions.ps1step 11 allows a rollback-state delete only in the commit phase, requires a scheduled commit discard to exist, and states its blind spot.3. ci: a deliberately failed upgrade restores the previous installation exactly (#1629)
A new step in
Package (Windows .msi):The scenario: N is installed fastcached-only into a custom root, then upgraded to N+1 with the node added and the failure switched on.
What it checks: everything must equal N afterwards: both services' full registry keys, their running state, the installer values, both firewall groups, the installed product, the absence of the rollback key, and the binary.
Controls: patched copies of the MSI, each read back before it is trusted:
The registration carries an extra flag, so a re-registration from defaults cannot pass for an exact restore.
Discrimination: a successful run of the same upgrade must change every field the failed one had to keep.
4. msi: a maintenance transaction finds the product's own install root
CPack resolves
INSTALL_ROOTfrom the previous product only during an upgrade. A repair, a modify or an uninstall of a product installed into a custom folder therefore fell back to the default folder. The uninstall then ran actions against binaries that were not there, and left FastCached registered. The package now reads its ownInstallLocationand restates it asINSTALL_ROOTbefore costing, in maintenance only.check-msi-custom-actions.ps1step 12 pins it.5. msi, ci: Restart Manager cannot restart fastcached into the node's port
CI's first run failed an older step, "an upgrade deselecting fastcached", with 1603: the node could not bind 6674 because FastCached held it.
MSIRESTARTMANAGERCONTROL=Disable. It cannot reach the old package's removal: nothing of ours may run before RemoveExistingProducts (Error 2613).disabled, notmanual, through a checked action that applies only to a registered service.net startcannot cause one either.manualis a one-row change.RESTART MANAGER: Failed while restarting applications. Error: 352, exits 0, and FastCached is never started.FASTCACHED_START_SERVICE=0.Invoke-Msiexecjudges every one of build.yml's 31 transactions twice, while msiexec runs and again once it has settled (at least 10 s after exit), and the next transaction refuses to begin until it has.check-msi-custom-actions.ps1, andcheck-wix-service-table.cmake.doc-subject-checks.shnow makes--source-dirabsolute (a relative one failed three of its seventeen checks).Verification (local)
The gate and MSVC runs below covered the C++ tree. No commit after the first touches
src/; those commits change only packaging, scripts, the workflow and the rules.scripts/local-gate.sh(WSL): PASSED. clang-debug ran 6631 tests with 0 failed, and gcc-release ran 6630 tests with 0 failed. The pinned clang-format and clang-tidy (22.1.8) report no findings.cl-release(Windows): the build is clean (warnings are errors), and the fullctestrun passes 6571 tests with 0 failed.check-msi-custom-actions.ps1(14 steps),msi-custom-action-commandsandcheck-wix-service-tableall pass.d1c1cc0e4,Package (Windows .msi)passes. That job runs the failed-upgrade step with its controls, the upgrade from 0.3.0, and all 31 judged transactions.d1c1cc0e4; the two skips are release-only.Windows-cl-releasewas re-run once because GitHub's Actions cache was over its account egress limit (HTTP 503 at sccache startup), not for anything in this PR.