Skip to content

Releases: nanoDBA/boot-upd

v2.5.86 - Progress-aware reboots

Choose a tag to compare

@nanoDBA nanoDBA released this 25 Oct 20:40
f24175b

The updater used to stop after five total reboots, even when providers were still making verified progress. It now limits consecutive reboot barriers without verified progress and keeps going when completed phases or verified update counts change. Repeated no-progress states still stop for attention.

649 Pester tests passed. The quality, User/SYSTEM exclusion, and published launcher upgrade gates passed. The live README bootstrap also passed both clean Windows PowerShell 5.1 scenarios: fresh install and legacy repair (workflow run).

The headless VM row parked correctly at the saved user's sign-in task; the saved-user continuation ran and cleaned up its tasks and checkpoint. The VM still returned deferred inventory because Windows Update offered KB5007651 again after two successful installs. The test guest also lacked Winget, so user-app upgrades were not tested. Overall VM result: PARTIAL. VM A: NOT RUN because its checkpoint did not reach a desktop.

Full results and limits: https://github.com/nanoDBA/boot-upd/blob/master/docs/validation-2.5.86.md

v2.5.85 - Resume saved-user updates automatically

Choose a tag to compare

@nanoDBA nanoDBA released this 25 Oct 02:34

User-only updates now stay queued after headless servicing finishes. Sign in as the saved user and the cycle resumes automatically. No manual rerun required.

  • Stops the SYSTEM fallback once only saved-user work remains. An already signed-in saved user gets an automatic retry too.
  • Fixes a launcher race that could overwrite the record of a completed legacy upgrade.
  • Gives delayed-start Windows Time a bounded chance to start before reporting an early health failure. The health check still does not start or reconfigure services.
  • Corrects deployment messages about which account runs updates and when user updates resume.

Validation: 645 tests passed, along with the User/SYSTEM exclusion and published launcher upgrade gates. Windows 11 VM rows A, G and N passed, including killed-process recovery and protection against duplicate runs. Automatic saved-user sign-in resumed the cycle and cleaned up tasks, state and temporary autologon settings.

Known limitation: VM B remains PARTIAL because Windows Update offered KB5007651 again after successful installation. It is reported as deferred inventory, not counted as a completed update. Follow-up: #96.

The VM matrix did not cover Windows 10, RDP, simultaneous interactive sessions or real upgrades of user-installed apps. Detailed evidence and coverage limits: https://github.com/nanoDBA/boot-upd/blob/master/docs/validation-user-continuation.md. These VM runs tested the implementation before the release-only version bump.

Published-asset verification: all ten SHA256 sidecars and expected line endings passed. The exact README bootstrap passed on hosted Windows VMs from Windows PowerShell 5.1 for both fresh installation and legacy repair: https://github.com/nanoDBA/boot-upd/actions/runs/40154970065.

v2.5.84 - Targeted Winget parsing and pre-flight watchdog

Choose a tag to compare

@nanoDBA nanoDBA released this 24 Oct 01:58
6295b3f

Fixes two ways a run could stall or misreport: single-package Winget upgrades, and a pass that dies before it starts updating.

Changes

  • Targeted Winget upgrades are read correctly. When the updater upgrades one package at a time (winget upgrade --id ...), Winget prints a different transcript than a bulk upgrade: no (1/3) counter, and its own wording for "no applicable upgrade" and "installed with a different technology". Those runs are now counted as attempted, and their exit codes reconcile with what Winget actually did. Packages Winget can't upgrade in place (pinned, unknown version, different installer technology) are now recorded in the deferred inventory for targeted runs too, as they already were for bulk runs.
  • A pass that crashes during pre-flight now resumes. The repeating watchdog task used to be armed only after the pre-flight checks. If PowerShell died in between, for example because Windows Update replaced pwsh.exe underneath it, nothing restarted the run until the next reboot. Task Scheduler records a crashed action as "completed", so the gap stayed invisible. The watchdog is now armed before pre-flight.

Testing

619 tests passed, along with the User/SYSTEM and launcher checks. The published installer also passed fresh-install and legacy-repair tests.

The interactive VM passed after four update passes and three reboots. The headless VM completed five passes and two reboots, but deferred user-only updates (no one was signed in), a Windows update that stayed offered after installing, and some delete-only file cleanup. Both cleaned up their resume tasks and saved state. The overall VM result is PARTIAL, not a claim that everything was patched.

A separate VM row killed a resumed pass during pre-flight on purpose; the watchdog restarted it about 18 minutes later and the run finished.

Full results are in the validation report.

v2.5.83 - AWS CLI repair fixes

Choose a tag to compare

@nanoDBA nanoDBA released this 26 Sep 18:00

Fixes the AWS CLI repair instructions for machines where Winget reports a successful update but Chocolatey still fails.

Changes

  • For the specific Amazon.AWSCLI / awscli case, the repair plan asks you to confirm the installed product, then offers choco uninstall awscli -y --skip-autouninstaller --skip-powershell to remove Chocolatey's package record. You decide whether to run it.
  • Other AWS CLI failures point to upd aws. Package IDs are checked before they appear in commands, and checksum failures require hash verification before a repair command is offered.
  • The updater now recognizes ordinary Winget success messages and remembers them after a reboot. Similar package names and preview editions are kept separate.
  • The README starts with installation and the commands you'll actually use. Longer explanations are linked from there.

Testing

608 tests passed, along with the User/SYSTEM and launcher checks. The published installer also passed fresh-install and legacy-repair tests.

The interactive VM passed its checks after four update passes and three reboots. The headless VM completed five passes and three reboots, but deferred user-only updates and a Windows update that remained offered after installation. Both cleaned up their resume tasks and saved state. The overall VM result is PARTIAL, not a claim that everything was patched.

The Chocolatey test checked that uninstall scripts stayed suppressed and an external application file remained. It was not a live AWS publisher-rollover test. Full results, the initial launcher-exit caveat, and remaining coverage gaps are in the validation report.

v2.5.82 - Native SSMS updates and reboot verification

Choose a tag to compare

@nanoDBA nanoDBA released this 20 Sep 17:49

Running upd.cmd now invokes native servicing for existing SSMS 22 installations through Visual Studio Installer, using each instance's configured update channel. Fresh inventory must verify the target build; busy installers, open SSMS, and unavailable metadata remain retryable. Reboot evidence and update accounting survive checkpoints.

Use upd --skip-ssms (PowerShell: -SkipSsms) to skip native SSMS servicing. The choice survives deployment, self-update, and reboot resume, including explicit false values. Winget and Chocolatey retain their existing package selection.

Final Windows Update scans now receive a fresh settled reboot check before completion. The VM collector also requires active-state cleanup before reporting completion.

Validation:

  • 577 local tests passed; user/SYSTEM exclusion and published-launcher upgrade gates passed.
  • All 43 PowerShell files parsed with no error-severity analyzer findings; GitHub CI passed.
  • Native SSMS VM upgrade: 22.9.2 to 22.10.1, followed by a verified no-op.
  • Default upd.cmd VM: PASS, three independently confirmed reboots, native SSMS phase executed, and no remaining tasks, active state, or CBS reboot marker.
  • Headless -SkipSsms VM: skip/resume/cleanup verified across two OS-confirmed reboots. Overall PARTIAL because user-scoped work and the KB5007651 re-offer remained explicitly deferred.

The native installer did not request a reboot during its upgrade; native 3010/1641 recovery has regression coverage but was not exercised by that live install.

Detailed validation. Issues: #73, #74, #75.

v2.5.81 - engine upgrade

Choose a tag to compare

@nanoDBA nanoDBA released this 15 Sep 18:55

v2.5.81 (2026-09-15)

Five defects from the 2026-09-14 laptop diagnostics and the September lab runs, each
implemented and unit-tested in an isolated worktree and merged on 2026-09-15.

Fixed

  • A laptop sleeping mid-phase no longer has its healthy provider killed on wake. Provider
    timeouts measured elapsed time with the wall clock, so a suspend counted against the idle
    and hard limits; on 2026-09-13/14 one laptop saw four "hard timeout" kills 1-2 s after wake,
    each preceded by a 50-158 minute heartbeat gap. A poll-to-poll gap far beyond the cadence is
    now treated as suspension and walked out of both clocks, logged once as "System was
    suspended for N min during ; not charged to the timeout."
    (-xglj).
  • Boot identity alone no longer declares a new boot when the monotonic reading says the
    same boot.
    The same laptop twice logged "new Windows boot session (identity)" within
    two minutes of the previous pass, resetting the same-boot retry budget each time. When a
    prior and current monotonic reading both exist and agree, an identity move is logged as
    clock jitter with both deltas; legacy state without a monotonic reading behaves as before.
    The observation line now carries Δidentity and Δmonotonic in seconds (-vhcm).
  • A portable package Winget refuses to replace because it was modified is deferred
    inventory, not a terminal failure.
    "Unable to remove Portable package as it has been
    modified"
    stopped a cycle for manual attention twice in one afternoon. It is now recorded
    as PortableModified with the override command, does not enter the failure signature, and
    the cycle converges qualified. An empty machine-scope inventory ("No installed package
    found matching input criteria"
    ) after a user-scope success is no longer read as
    unparseable (-qjkh).
  • A trusted per-phase hook is no longer rejected because BUILTIN\Users can read the trust
    root.
    The write mask OR-ed composite rights that include read bits; it now tests the
    specific mutation rights only, with real-ACL tests for read-only and writable broad grants
    (-egwn).
  • A pass that dies during final verification after the cohort is now charged and named.
    The state carries a FinalVerification intent after CohortDone, so a crash there counts
    as an unobserved stop, re-runs only the final checks, and leaves provider flags and the
    reboot budget untouched (-z72k).

Added

  • upd bootstrap now upgrades an already-installed PowerShell 7 to the latest stable
    release, not just verifies it exists.
    Machines without Winget — Windows Server 2016
    chief among them — had no path to a newer engine short of a manual MSI. upd bootstrap
    compares the running pwsh.exe against the latest non-prerelease GitHub release, upgrades
    through Winget when present or the same signed-MSI route the fresh install already uses
    otherwise, and never downgrades. An unreachable GitHub API is a warning, not a failure:
    bootstrap still exits 0 so the launcher keeps working offline.
    The upgrade itself runs detached from the launcher, under Windows PowerShell 5.1 in a hidden
    host that logs to %TEMP%\boot-upd-pwsh-upgrade.log, because the installer closes every
    running pwsh.exe, the launcher's own included. The first attempt hosted the MSI inside the
    launcher and lost the source package mid-transaction when Restart Manager's shutdown unwound
    a finally that cleaned the temp directory; the installer then failed with 1316/1603 and
    left the guest with no pwsh.exe. The package is now removed only after a successful exit
    code, and Restart Manager is kept out of the install (MSIRESTARTMANAGERCONTROL=Disable).
    Verified on lab-b (no Winget): upd bootstrap took 7.6.5 to 7.6.6, MSI event 11707
    "Installation completed successfully", pwsh -v 7.6.6 afterwards.

Validation

Unit/process behavior:       PASS     (537 tests, 0 failed)
User/SYSTEM boundary:        PASS
Published launcher upgrade:  PASS     (from v2.5.43)
Live bootstrap:              NOT RUN  (README one-liner on a hosted VM)
PowerShell 7 engine upgrade: PASS     (lab-b, no Winget, 7.6.5 -> 7.6.6; see below)
Provider integration:        NOT RUN
Multi-reboot convergence:    PARTIAL  (A PASS, B PARTIAL, F PASS; watchdog rows NOT RUN)
Release assets:              PASS     (20 assets with SHA256 sidecars)

Gates were run from an elevated PowerShell 7 console on 2026-09-15 against the final tree, and the three rows below ran against the same tree.
The unit suite's six trusted-file ACL tests run only from an elevated console; from a
non-elevated one they skip and the count is lower.

  • PowerShell 7 engine upgrade - lab-b, no Winget, PowerShell 7.6.5 installed. PASS.
    upd bootstrap reported 7.6.5 behind 7.6.6, handed off, and the detached host upgraded it:
    MSI event 11707 "Installation completed successfully", pwsh -v 7.6.6, no restart needed.
    Manual check, not a harness row; the harness has no row shape for it yet.

  • Watchdog rows (kill-and-resume, short-interval no-double-run): NOT RUN on this
    build. They ran on 2026-09-12 for v2.5.80 and nothing in this release touches the trigger
    or the probe.

  • Row A - interactive user, three armed reboots, lab-a. PASS.
    A-v2581-release-boot-upd-matrix-20260915-143619. Four passes, converged in 11.8 minutes
    with 2 verified updates, RebootsClaimed 3 = RebootsObservedOS 3, no state file, no
    continuation tasks, no pending CBS. Two of the three boots fell inside the 120-second
    identity window and were caught by the monotonic signal; the observation line shows both
    deltas ("identity delta=108s, monotonic delta=107s"). No jitter suppression fired, as
    expected on a guest that does not suspend.

  • Row B - headless, SYSTEM fallback, one armed reboot, lab-b. PARTIAL.
    B-v2581-final-lab-b-20260915-134435. Five passes, converged in 31.2 minutes with a
    qualified claim (user-scope work deferred after two rediscovery attempts, one re-offered
    Windows Update), RebootsClaimed 2 = RebootsObservedOS 2, no state file, no continuation
    tasks. PARTIAL rather than PASS because the harness read the CBS RebootPending key at
    collection after the updater's own two-probe settle was clean; on 2026-09-12 the same
    post-completion reading was traced to Windows' own update agent staging a package, but that
    was not re-established for this run.

  • Row F - PowerShell 5.1-only bootstrap through upd.cmd, lab-b. PASS.
    F-v2581-final-lab-b-20260915-142045. The launcher installed PowerShell 7.6.6 from the
    signed MSI through the refactored install function and the cycle converged in one pass
    with a cleanup advisory, no state file, no continuation tasks, no pending CBS.

Not run for this release: row D (failed restart command) and the hosted-VM live bootstrap.
Treat them as NOT RUN.

v2.5.80 - watchdog recovery and diagnostics corrections

Choose a tag to compare

@nanoDBA nanoDBA released this 12 Sep 17:55

v2.5.80 (2026-09-12)

Closes the gap v2.5.79's "Known limitations" named: a cycle killed mid-pass on a machine that
neither reboots nor logs on again was not resumed until the next logon or boot (-35qb.10,
found by matrix row G on 2026-09-09).

Fixed

  • A cycle killed mid-pass on a machine that neither reboots nor logs on again was never
    resumed.
    Every trigger in the resume chain was boot- or logon-scoped, so a pass that died
    in between sat idle indefinitely. While a cycle is in flight — armed at the resume checkpoint
    before the first update phase — both continuation tasks now also carry a repeating trigger
    that starts a watchdog probe every WatchdogIntervalMinutes (default 15, floor 2, no off
    switch). A probe that finds the cycle's mutex held exits at once, unchanged; one that finds it
    free or abandoned becomes the recovery pass. This closes #69 (Beads -35qb.10).

Changed

  • New -WatchdogIntervalMinutes parameter on Invoke-BootUpdateCycle.ps1 and
    Deploy-BootUpdateCycle.ps1 (default 15, floor 2 with a logged clamp), passed through in
    continuation-task arguments.
  • A pass resumed after an unobserved stop is now a recovery pass, charging the retry budget
    once and recorded in state as UnobservedStops. It is disclosed in the completion banner/log
    and in the repair plan, without changing the convergence claim.
  • Registration verification fails closed in both directions: a deliberate stop (retry-pending,
    waiting on a user logon, the restart path) must never carry the repeating trigger, and an
    in-flight checkpoint must carry it — either mismatch throws.
  • The mutex-held log line changed from a Warn "Exiting" to an Info line describing a watchdog
    probe exiting without changes, since a probe finding the mutex held is now an expected, routine
    outcome rather than a collision worth a warning.
  • A staged-rollout withhold is now marked as deliberate. Before this, the pass after a staged
    target left incomplete logged "Previous run crashed" although nothing had; with the new
    accounting it would also have disclosed a false unobserved stop and charged a second retry.
  • An operator resume after a raised limit is not charged. The pass that follows raising
    -MaxRetryPasses or -MaxIterations re-runs the unfinished phase without spending the retry
    it was just granted.
  • WatchdogIntervalMinutes is accepted from remote configuration, with the same floor and a
    1440-minute ceiling applied on every path.
  • Downgrade note. Continuation tasks now pass -WatchdogIntervalMinutes; an older orchestrator
    placed at the install directory by hand would reject that argument and the resume chain would
    not run. Roll back with Deploy-BootUpdateCycle.ps1, which re-registers the tasks.

Additional fixes

  • Empty cleanup is no longer labeled persistent when both observations are empty. #53 records the exporter defect; only equal nonempty fingerprints establish persistence. Empty-to-empty is false; missing or skipped observations remain unknown.
  • The lab CI credential boundary is explicit. #66 records the analyzer correction: required plaintext-to-secure-string conversion is centralized with narrow function-scoped compatibility exceptions.
  • Parallel-phase interruptions consume the watchdog retry budget. Release review found that the parallel-cohort sentinel bypassed crash accounting, allowing repeated same-boot interruptions to retry indefinitely. #70 tracks the correction: charge one recovery for the interrupted pass, retain completed provider flags, disclose the stop, and use the existing retry-limit handoff.

Known limitations

  • Interruption during final health and convergence verification, after the parallel cohort has finished, does not yet charge the recovery budget. #71 tracks a durable checkpoint for that window; the parallel-cohort fix does not cover it.
  • Optional phase hooks may be rejected by the trust check under the standard directory permissions, even when broad groups have only read access. #72 tracks this permission-mask false positive.

Validation

Unit/process behavior:       PASS     (502 tests, 0 failures or skips; clean CI for 98f72e2)
User/SYSTEM boundary:         PASS
Published launcher upgrade:   PASS
Live bootstrap:               PASS     (fresh install and legacy repair from the public release)
Parallel-cohort recovery:     PASS     (two interruptions; bounded safety stop, not convergence)
Provider integration:         NOT RUN
Multi-reboot convergence:     PARTIAL  (rows A and G PASS; row B PARTIAL with deferred inventory; wider rows NOT RUN)
Release assets:               PASS     (10 scripts + 10 SHA256 sidecars; uploaded bytes verified)

Candidate CI passed 502 tests, PowerShell parsing, analyzer checks, user/SYSTEM exclusion, and the published-launcher upgrade gate at commit 98f72e2. Four new behavioral regressions cover the parallel-cohort accounting correction. The A/B/G rows below ran before that correction and the version bump; they establish their tested paths, not the new cohort edge case. Live bootstrap VM checks passed for fresh install and legacy repair against the public release. All 20 release assets were downloaded and verified before publication, and v2.5.80 was confirmed as the latest public release. Broader provider coverage and the wider VM matrix remain NOT RUN.

  • Row A — interactive continuation, September 12. PASS. Four user-context passes across three observed reboots; reboot accounting matched the OS evidence, the final Windows Update scope was empty, health checks passed, and state, continuation tasks and cleanup evidence were clear. Evidence: vm-runs-retry2/A-sep12-boot-upd-matrix-20260912-111434.

  • Row B — headless SYSTEM continuation, September 12. PARTIAL. Five SYSTEM passes and two observed reboots completed the cycle with no interactive user. Winget, Scoop and VS Code user work remained deferred, and the Windows Update re-offer was retained as deferred inventory rather than retried; the row therefore does not claim full provider convergence. This is a truthful partial result.

  • Row G — killed-process recovery, September 12. PASS. The intended Deploy process was killed after promotion; the scheduler records recovery PID 5148 and a separate SYSTEM fallback PID 4784 that respected the mutex. The recovered cycle completed with zero reboots, empty Windows Update assessment, passing health checks, and no remaining state or continuation tasks.

  • Row H — bounded parallel-cohort recovery, September 12. PASS for recovery; convergence intentionally stopped. The exact candidate orchestrator ran directly from its installed path. An admin-only fixture directory avoided the separate hook-permission defect (#72); a trusted hook held the cohort entry before provider jobs started. Two distinct processes were killed at persisted cohort checkpoints with retry counts zero and one. Time-triggered continuations charged exactly two recoveries; the third pass retained RetryLimitReached state, exited with code 3, and removed both continuation tasks. Boot timestamps normalized to the same UTC instant and the reboot count remained zero. Scheduler exit-code encoding was independently calibrated. This tests cohort-entry interruption, not termination inside an active provider. Details and fixture limitations: September 12 evidence report.

v2.5.79 - truthful claims

Choose a tag to compare

@nanoDBA nanoDBA released this 09 Sep 19:51

v2.5.79 (2026-09-09)

Truthfulness release. v2.5.78 fixed the headless servicing path and shipped a note claiming a capability the binary did not have; running the matrix against two guests found that claim was one of several. Six of the fixes below are cases where the updater said something that was not so — a dead preference described as active, a deliberate withhold logged as a crash, a reboot announced that never happened, a watchdog documented in a comment and never armed — and one is a fix that reopened an old defect the moment it started working.

Four of these were found on a machine after the unit suite had agreed with the code. Two more — a crash on the ordinary Windows Update path, and this release's own false claim about -WhatIf — were found by the two-axis review, before they shipped.

Fixed

  • The resume account's SID preference was dead code. $state.ResumeUserSid was assigned inside an empty catch while neither the state constructor nor the add-if-missing normaliser declared the property. Assigning an undeclared property on a [pscustomobject] throws, the empty catch swallowed it, and Resolve-BootUpdateResumeAccount was therefore always called with an empty -PreferredSid. AzureAD\ and MicrosoftAccount\ machines degraded to a SYSTEM-only continuation — exactly the case the SID existed to cover. The property is now declared in both places, and the discovery moved into Update-BootUpdateResumeIdentity so a test can walk constructor → discovery → resolver: every existing test called the resolver directly with a SID it supplied itself, which is why this survived a release. The discovery also records the SID and the name together, so a SID left by an earlier pass for a different account can no longer outrank the fresh name at a resolver that prefers SIDs absolutely. v2.5.78's note that the SID "is now preferred" is corrected below.
  • Fixing that reopened the unbounded wait for an interactive user. On a machine nobody signs into, LogonUI still records a past session. With the SID discovery working, ResumeUser became populated for the first time, the cycle concluded a user was known, waited on a logon trigger that will never fire, armed no dated retry, and took no further pass — sitting at UserContextPending with both tasks armed and nothing able to fire them. That is the unbounded wait -MaxUserIdentityWaits was added to prevent, walked back in through the door the fix opened. The bound now asks whether a user-context pass can still happen (Test-BootUpdateInteractiveUserPresent) rather than whether a name is known: a cycle running as the user is the session; a SYSTEM pass looks for a console user. Behaviour change worth stating: a machine whose user has signed out is now bounded too, where v2.5.78 would wait indefinitely. After -MaxUserIdentityWaits rediscovery attempts its user-scope work is recorded as deferred inventory and the cycle completes with a qualified claim, rather than withholding forever. The probe reads Win32_ComputerSystem.UserName, which is the console session, so an RDP-only server is treated as having no interactive user — the safe direction, but not the same question.
  • An update the machine installs and is then offered again was treated as retry fuel forever. KB5007651, the Windows Security platform update, installs, genuinely changes the machine, is recorded by Windows Update with result code 2, and is offered again by the very next scan. Measured on a cold guest across one run: AMProductVersion advanced 4.18.23110.3 → 4.18.26080.3 and a Platform directory appeared where the guest had none, while Windows Update history gained five KB5007651 entries, every one result code 2 — and the update stayed applicable. Row B ran six passes still looping when the row's 45-minute budget ran out. An update that Windows Update itself records as installed successfully DURING THIS RUN and still offers on the final scan is now recorded as deferred inventory with kind ReofferedAfterSuccess, is not counted as a verified update and not counted as retry fuel, and the completion claim is qualified. Only result code 2 qualifies, so a failed install remains a real failure and stays retryable — which is the whole reason for reading the agent's result code rather than PSWindowsUpdate's Installed [n] Updates prose. Convergence is withheld on the count of unexplained updates, and a clean pass retracts the previous pass's observation. The window is the run rather than the boot: a first attempt at this scoped it to the current boot, and lab row B showed the cycle then loses the evidence across the very restart the classification exists to stop repeating.
  • The dated watchdog the restart path documents had never been armed. Start-BootUpdateRestart registers a watchdog before every restart for the case its own comment names: if the user takes the documented shutdown /a escape hatch, neither startup nor logon fires, so a time trigger is what keeps the resume chain alive. The parameter is declared [Nullable[datetime]], but PowerShell's parameter binder converts that to a plain System.DateTime, which has no HasValue member — so $RetryAt.HasValue evaluated to $null, never $true, and every -RetryAt caller registered a task with no time trigger. -RetrySoon is a [switch] and was unaffected, which is why the two-minute retries everywhere else kept working and hid this. Matrix row C proved it from the machine: after cancelling the countdown, Export-ScheduledTask on the guest returned a single LogonTrigger, the task never ran, and the cycle sat untouched for the whole 40-minute row with Resume chain verified printed beside it.
  • A timeout kill orphaned the installer it had started, and the next passes collided with it. Remove-ProcessTree kills the provider and its descendants, but msiexec.exe is parented to services.exe and is not in that tree. On 2026-09-06 a killed Acrobat Reader install left msiexec alive for 28 minutes and six machine-scope packages failed 0x652 = 1618 against the updater's own orphan — three iterations and 39.6 minutes spent colliding with itself. Killing harder is the wrong fix; tearing down an MSI transaction mid-write is how a machine ends up with a half-installed product. After either timeout kill the cycle now asks the question Windows Installer actually arbitrates — is Global\_MSIExecute held — and waits up to 30 minutes for the transaction to finish, a bound chosen to cover the 28-minute case that motivated it. Asking the mutex rather than looking for a running msiexec.exe matters: msiexec runs as a service process for reasons unrelated to an active transaction, so a process-name wait would block on nothing. A mutex that cannot be examined — an ACL denial, say — is treated as held rather than absent, and the outcome travels out with the timeout result so a later 1618 can be attributed rather than guessed at.
  • A deliberately withheld phase was logged as a crash. An unfinished phase leaves one footprint — LastPhaseStarted set, the Done flag false — for three different histories, and only one is a crash. Row B emitted "Previous run crashed during [WindowsUpdate]" on three of its seven passes while nothing had crashed, which makes a healthy withhold-and-retry cycle read as repeated crashes in the log and in every diagnostics bundle built from it. The message now uses the previous pass's own terminal disposition: a withheld verification and a planned restart each say so, and a real crash keeps its warning.
  • A pass that followed no reboot announced that it had resumed after one. Matrix row D drove five passes with the restart command rejected every time, and every pass after the first announced RESUMED (after reboot) while the same line reported Reboots: 0/5. The banner verb now comes from the boot observation the cycle already computes, and a same-boot retry says RESUMED (same boot).
  • The diagnostics manifest inferred pending-file cleanup state from the absence of a log line. The cycle declines to emit that line for four unrelated reasons — an empty advisory set, duplicate suppression, the phase not running, and the explicit 3010/1641 short-circuit — and all four collapsed to Persistent=null. A completed, converged run whose truthful answer was Persistent=false reported null. The cycle now writes BootUpdateCycle.pending-cleanup.json on an evidence lifecycle — never TTL'd, never invalidated on a fingerprint, written even under -WhatIf, appended per probe and keyed by session and pass, because whether a fingerprint survived a restart is only answerable across passes. A damaged file is moved aside rather than overwritten. The duplicate guard stays in the log; it no longer decides what is known. The exporter prefers the sidecar and keeps log parsing only for bundles that predate it, which now report unknown-legacy-log rather than a bare null. See ADR-0004.
  • The explicit-reboot short-circuit skipped that recording entirely. Get-ConfirmedPendingReboot returns early on a 3010/1641 request, correctly bypassing the settle-and-recheck wait — but it also bypassed the recording, and it fires precisely when real servicing has just happened and the pending-file evidence is most worth having. One registry read now refreshes the snapshot before it returns, with the record marked explicit-reboot so a reader can see it did not come from a full two-probe confirmation. No settle wait was added to that path.
  • The beads export wrote a real name and email address into a public repository. The identity appeared in created_by (274), owner (211), assignee (113) and author (2) — 600 fields across 216 rows. The export path now sanitises all four (bd's own unknown author is preserved, because replacing it would invent attribution rather than remove it), the existing rows were rewritten once, and the one remaining occurrence in tic...
Read more

v2.5.78 - Headless servicing

Choose a tag to compare

@nanoDBA nanoDBA released this 09 Sep 08:03

Boot Update Cycle - Release Notes

Current Version: v2.5.78
Release Date: 2026-09-09
Status: STABLE


v2.5.78 (2026-09-09)

Headless-servicing release. v2.5.77 shipped the lab that found its own defects; running that lab against a machine nobody signs into found four more, and one of them was introduced by v2.5.77 itself.

Microsoft documents a machine with no signed-in user as the unblocked servicing path. The updater could not converge there at all. Every fix below is on that path.

Fixed

  • A same-boot recovery pass reset the retry budget, so a permanently applicable update retried forever. The monotonic boot instant v2.5.77 added is persisted, and ConvertFrom-Json rehydrates an ISO-8601 string as a [datetime]; coercing that back to text for the comparison uses the current culture, which emits no offset, so a UTC instant parses as local and the delta picks up the machine's entire UTC offset rather than the seconds of jitter the tolerance was sized for. Measured on an Eastern host: 14,398 seconds of phantom skew where the true delta was 1 second. Every same-boot pass therefore read as a fresh boot, which zeroes ConsecutiveRetryCount, so the same-boot recovery limit never fired. Lab row B ran seven passes across two real reboots and stopped only when the harness timed out. This is the defect class the boot-session rule already documents; it recurred because a second persisted timestamp was added without routing it through ConvertTo-BootUpdateTimestampString. The comparison now lives in one helper that normalises both sides and types neither as [string], so the two call sites cannot drift apart again. The reboot count stayed truthful only by luck - its increment is gated on the phase being Rebooting, which these retries were not.
  • The wait for an interactive user was unbounded, so a machine nobody signs into could never finish. User-scope work legitimately defers until a user context exists; with no user ever appearing, that condition never cleared. The wait is now bounded by -MaxUserIdentityWaits (default 10). On exhaustion the outstanding user-scope work becomes deferred inventory, which produces a qualified convergence claim rather than an unqualified one. A headless machine now reaches a terminal state and says truthfully what it could not do.
  • The resume account preferred a name over the SID sitting next to it. LastLoggedOnSAMUser returns the .\name form, which LookupAccountName cannot map (ERROR_NONE_MAPPED). LastLoggedOnUserSID is recorded beside it and is unambiguous, so it is now preferred; it also covers the AzureAD\ and MicrosoftAccount\ forms that no string expansion would fix.
  • Resume-chain verification compared a SID against a name and failed. A direct regression from the fix above: with the expected principal now a SID, [NTAccount]::Translate throws on SID input and the fallback compared a leaf name against a full SID. The cycle registered both tasks and then killed itself one line later - indistinguishable from the original no-user death it was meant to fix. Both sides are now normalised to SIDs before comparison, and the failure message quotes both values.
  • Pre-flight warned about a healthy Windows Update service. A Stopped wuauserv was reported as a problem. It is demand-start: stopped is the normal resting state of essentially every idle Windows machine, and the WU COM client starts it on the first method call. Only Disabled is a warning now; the resting state is logged as normal.

Corrections to v2.5.77's notes

  • v2.5.77 attributed row B's failure to converge to wuauserv being stopped on the guest image, framed as an environmental artefact. That was wrong. A stopped wuauserv is the documented, normal resting state, not a broken image, and the updater started it on demand in every subsequent run. Row B did not converge because of defects in the updater: the unbounded user wait and the retry-budget reset above.
  • v2.5.77 reported multi-reboot convergence as "four of seven rows". Those seven rows are this project's own decomposition of the gate, not a bar docs/TESTING.md sets - the gate is defined there as reported, not blocking, and states no completion criterion. "Four of seven" reads as a score against an external standard, and should not have.
  • v2.5.77's README framed the Microsoft-Windows-International-Core requirement in the oobeSystem pass as a discovery. It is documented: Microsoft describes the -WinPE variant as applying only in the windowsPE pass and directs you to the non-WinPE component for oobeSystem.

Validation

Unit/process behavior:       PASS  (392 tests, 0 failed)
User/SYSTEM boundary:        PASS
Published launcher upgrade:  PASS
Live bootstrap:              NOT RUN
Provider integration:        NOT RUN
Multi-reboot convergence:    PASS  (rows A and B, see below)
Release assets:              PASS

Both rows ran on a Windows 11 Enterprise LTSC 2024 guest rebuilt during this cycle, so neither inherited state from the v2.5.77 lab.

  • Row A, interactive-user continuation across real reboots: PASS. Four passes, three reboots, converged with 2 verified updates; both continuation tasks removed and no state file left. The reboot count the updater claimed equals the OS boot record. The reboots were 67 and 58 seconds apart - inside the 120-second boot-session tolerance - and each was observed exactly once, so the fix above did not buy same-boot stability by going blind to the fast restarts that motivated the monotonic signal in the first place.
  • Row B, SYSTEM continuation with nobody signed in: PASS, against a guest verified headless before the row started (no Explorer, active LogonUI, empty console user). The new-boot observation fired exactly once for the one real reboot, where before the fix it fired five times across two. The retry budget therefore accumulated, and the cycle stopped itself: "Same-boot recovery limit 3 reached; incomplete phases: WindowsUpdate. Continuation tasks were removed and verified absent." No continuation tasks remained, and diagnostic state was retained deliberately, as the limit path documents.

Two limits on what row B establishes, stated plainly:

  • It reached a truthful terminal state, not convergence. Update for Windows Security platform - KB5007651 reports as installed on every pass and is still applicable on the next scan, so this image cannot converge. Whether that is an environmental re-offer or the phase counting an install that did not happen is filed separately and is not claimed either way here.
  • The bounded user wait was not exercised by the lab. -MaxUserIdentityWaits never fired, because the recovery limit is reached first on this image. It has unit cover; it does not yet have lab cover.

Rows C, D, E, F and G were NOT RUN for this release. C and E passed under v2.5.77 and were not re-run against the rebuilt guest.

No live update cycle was run on the maintainer's own machine during validation; every reboot was performed on a disposable guest.

v2.5.77 (2026-09-08)

Multi-reboot gate release. The gate defined in docs/TESTING.md was executed on a disposable VM lab for the first time, and it found two P1 defects that every prior release shipped with, including v2.5.76. Neither was visible to the unit suite.

Fixed

  • The idle timeout never measured idleness. Get-ProcessTreeActivity walked Win32_Process from the provider PID, but ProcessId/ParentProcessId are UInt32 while the traversal queue is Queue[int], and a Hashtable keyed by UInt32 never matches an Int32 probe. Every lookup missed, so the function reported an empty tree with zero CPU for every process it was ever asked about. Zero never exceeds the previous zero, so the idle clock never reset: the idle timeout was a fixed wall-clock kill at IdleTimeoutMinutes, applied to healthy and wedged packages alike. Shipped logs carry the fingerprint plainly — heartbeat: CPU=0s procs=0 beside phases that completed normally. On 2026-09-06 it killed an Acrobat Reader upgrade at exactly five minutes, orphaning an msiexec that then failed the next six packages with 1618, at a cost of three iterations and 39.6 minutes.
  • An unobservable process tree is no longer treated as an idle one. msiexec is parented to services.exe, so an MSI-backed package legitimately does its work outside the tree being watched; killing on that reading is how a healthy install dies. The hard timeout remains the bound. See docs/adr/0005.
  • Parent edges are rejected when the claimed child predates the claimed parent. A reused PID adopted every orphan still naming it; one reading moved from 71 minutes of stranger CPU to 2 seconds of real CPU between consecutive samples, and a total that falls can never exceed the previous high-water mark, so the idle clock advances and kills working software.
  • Fast reboots were absorbed by the boot-session tolerance, letting the reboot limit be evaded. Two boots inside the 120-second window counted as one, so the completed-reboot count under-reported and the claim said "2 reboot(s)" where Windows event 6005 recorded three. That count gates the reboot limit, so a loop faster than the tolerance never increments toward the cap. Shrinking the window cannot fix it: jitter is seconds, fast reboots are tens of seconds, and the ranges overlap. The boot instant is now reconstructed by subtracting monotonic uptime from the current time, which is stable within a boot and jumps by the downtime across one.
  • A cycle started as SYSTEM with nobody signed in died at task registration. Resume-user discovery falls through to LastLoggedOnSAMUser, which returns the .\name form; Task Scheduler cannot map that to a SID and Register-ScheduledTask throws a terminating CimException. The cycle died immediately after pre-flight leaving a state file and no continuation task at all,...
Read more

v2.5.77 - multi-reboot gate

Choose a tag to compare

@nanoDBA nanoDBA released this 09 Sep 01:53

Boot Update Cycle - Release Notes

Current Version: v2.5.77
Release Date: 2026-09-08
Status: STABLE


v2.5.77 (2026-09-08)

Multi-reboot gate release. The gate defined in docs/TESTING.md was executed on a disposable VM lab for the first time, and it found two P1 defects that every prior release shipped with, including v2.5.76. Neither was visible to the unit suite.

Fixed

  • The idle timeout never measured idleness. Get-ProcessTreeActivity walked Win32_Process from the provider PID, but ProcessId/ParentProcessId are UInt32 while the traversal queue is Queue[int], and a Hashtable keyed by UInt32 never matches an Int32 probe. Every lookup missed, so the function reported an empty tree with zero CPU for every process it was ever asked about. Zero never exceeds the previous zero, so the idle clock never reset: the idle timeout was a fixed wall-clock kill at IdleTimeoutMinutes, applied to healthy and wedged packages alike. Shipped logs carry the fingerprint plainly — heartbeat: CPU=0s procs=0 beside phases that completed normally. On 2026-09-06 it killed an Acrobat Reader upgrade at exactly five minutes, orphaning an msiexec that then failed the next six packages with 1618, at a cost of three iterations and 39.6 minutes.
  • An unobservable process tree is no longer treated as an idle one. msiexec is parented to services.exe, so an MSI-backed package legitimately does its work outside the tree being watched; killing on that reading is how a healthy install dies. The hard timeout remains the bound. See docs/adr/0005.
  • Parent edges are rejected when the claimed child predates the claimed parent. A reused PID adopted every orphan still naming it; one reading moved from 71 minutes of stranger CPU to 2 seconds of real CPU between consecutive samples, and a total that falls can never exceed the previous high-water mark, so the idle clock advances and kills working software.
  • Fast reboots were absorbed by the boot-session tolerance, letting the reboot limit be evaded. Two boots inside the 120-second window counted as one, so the completed-reboot count under-reported and the claim said "2 reboot(s)" where Windows event 6005 recorded three. That count gates the reboot limit, so a loop faster than the tolerance never increments toward the cap. Shrinking the window cannot fix it: jitter is seconds, fast reboots are tens of seconds, and the ranges overlap. The boot instant is now reconstructed by subtracting monotonic uptime from the current time, which is stable within a boot and jumps by the downtime across one.
  • A cycle started as SYSTEM with nobody signed in died at task registration. Resume-user discovery falls through to LastLoggedOnSAMUser, which returns the .\name form; Task Scheduler cannot map that to a SID and Register-ScheduledTask throws a terminating CimException. The cycle died immediately after pre-flight leaving a state file and no continuation task at all, so a machine where nobody logs in could never resume. Discovered names are now expanded and SID-checked, and an unresolvable account degrades to the SYSTEM-only registration branch rather than terminating.
  • Three progress assertions in tests/TuiExperience.Tests.ps1 measured something other than what they claimed and had been failing on real hardware since before v2.5.76. Two demanded an exact count of distinct rendered status lines, which varies with rounding boundaries; the third set a 1.2-second deadline for output that takes a measured 4.0 seconds to appear.

Added

  • tests/integration/lab/ builds and drives the disposable Hyper-V lab docs/TESTING.md requires: one call per guest, rows runnable on parallel VMs, and a README recording the traps each script encodes. Guest credentials come from BOOTUPD_LAB_PASSWORD and are never committed.
  • Every row records Windows event 6005 beside the updater's own log and reports whether the two agree about how many restarts happened. That comparison found the reboot-accounting defect, and later caught the first fix for it being wrong.
  • docs/adr/0005-an-unobservable-process-tree-is-not-an-idle-one.md.

Validation

Unit/process behavior:       PASS  (375 tests, 0 failed)
User/SYSTEM boundary:        PASS
Published launcher upgrade:  PASS
Live bootstrap:              NOT RUN
Provider integration:        NOT RUN
Multi-reboot convergence:    PARTIAL - four of seven rows, see below
Release assets:              PASS

Multi-reboot convergence was executed for the first time, on Windows 11 Enterprise LTSC 2024 guests under Hyper-V. Four of seven rows ran:

  • Row A, two or more real reboots with interactive-user continuation: PASS. Four passes across three real reboots; the claimed reboot count equals the OS boot record; both continuation tasks removed; no state file left.

  • Row B, SYSTEM-fallback continuation with no user logged on: PARTIAL. The mechanism is verified — tasks register, the cycle resumed across four real reboots as SYSTEM with nobody signed in, and user-scope work was deferred rather than claimed. It did not reach a completion claim, because wuauserv is stopped on the guest image and the Windows Update phase cannot finish. The updater withheld the claim rather than asserting convergence, which is the correct behaviour, but the row demonstrates truthful incompleteness rather than truthful completion.

  • Row C, canceled delayed restart: PASS. With the restart cancelled 13 seconds into a 60-second countdown, the updater did not record a reboot that never happened, did not advance its pass number, and left the resume chain armed. A later reboot resumed and completed it.

  • Row E, delayed reboot signal: PASS, for the after-window case only. The signal was injected 10 seconds after the 20-second settle window closed rather than inside it, so the in-window detection path was not exercised. What the row does establish is the property that matters most: a reboot signal arriving after the probe was not silently ignored. It was caught at the after-updates check, the cycle rebooted, resumed, and completed with a claimed reboot count matching the OS boot record and no state or task residue.

Rows D (failed restart command), F (PowerShell 5.1-only bootstrap) and G (killed-process recovery) are NOT RUN. Row G was attempted and is recorded as not run rather than failed: the injected kill did not match the orchestrator's process, so the run proved nothing about crash recovery. Each remaining row is filed individually and names the checkpoint it starts from.

No live update cycle was run on the maintainer's own machine during validation; every reboot was performed on a disposable guest.

v2.5.76 (2026-08-25)

Truthful-convergence release.

Fixed

  • A cycle that finished carrying work it could not attempt claimed unqualified convergence and sent a Success notification. CONTEXT.md has always held that deferred work "does not make a phase fail, but it does prevent an unqualified claim of convergence"; only the first half was implemented. $deferredCount — pinned packages, packages whose installed version cannot be determined, and packages Winget refuses to upgrade in place — existed solely to reconcile the Winget exit code and reached nothing downstream. Observed on real hardware 2026-08-24 17:00: the run logged Winget machine: deferred inventory - 1 install-technology blocked for Microsoft Edge, then BOOT UPDATE CYCLE COMPLETE and a Success toast. Deferred inventory now persists in cycle state and qualifies the completion claim, the notification severity, the completion banner, and the manual repair plan.
  • The observation is read from the checkpoint rather than from the finishing pass's own provider results. A phase already flagged done does not run again after a reboot or a recovery pass, so a per-run value would be empty at exactly the moment the claim is made.
  • Deferred inventory is recorded only for the canonical user and machine scopes. Write-WingetScopeSummary is also called with <scope>-retry and <scope>/<PackageId> labels, which are sub-observations of a scope rather than scopes of their own; recording them double-reported one deferral under a scope name that does not exist and stranded a retry entry that nothing would clear for the rest of the cycle.

Changed

  • The completion claim is composed rather than enumerated. Get-BootUpdateCompletionClaim replaces a nested if/elseif that produced four branches for two qualifiers and would have produced eight for three. This normalises the inconsistent COMPLETE WITH QUARANTINE AND CLEANUP ADVISORY to COMPLETE WITH WINGET QUARANTINE AND CLEANUP ADVISORY; nothing parses past the COMPLETE prefix, which Export-BootUpdateDiagnostics matches and the composer guarantees stays first.
  • The exit code is deliberately unchanged and remains 0. upd.cmd and both scheduled tasks read a non-zero exit as failure, and nothing failed; encoding the qualification there would assert the one thing "deferred" forbids. The qualification is carried only by surfaces a human reads.
  • A cleanup advisory still sends a Success notification, as before. It reports housekeeping that does not block convergence — "updates converged and no restart is required" — so it does not contradict a success claim the way outstanding update work does. It continues to qualify the log claim.
  • deferred was overloaded and is now two terms in CONTEXT.md. UserCompletionDeferred is a scope deferral: the machine-scope pass ran and the user-scope pass must run later in user context, which already drives the completion disposition. Deferred inventory is work that cannot be attempted at all. Adds qualified convergence.

Added

  • docs/adr/0003-deferred-inventory-qualifies-convergence.md, recording why the code was fixed to match the glossary rather than the glossary relaxed to match the code, why blocking completi...
Read more