Repository navigation
Conversation
|
🦞👀 Pull request received. I will update this pull request when review starts. ClawSweeper review completeClawSweeper finished reviewing this revision. The review result is being finalized. |
|
Codex review: blocked before merge. Reviewed September 29, 2026, 4:49 PM ET / 20:49 UTC (Revision 16). ClawSweeper reviewWhat this changesAdds a guided handoff from the Windows installer app to the Store app, including consent, recovery, production build enablement, and migration test artifacts. Merge readiness⛔ Blocked before merge - 9 items remain Keep this PR open. Current main has the migration foundation, while this branch supplies the remaining user flow. A source-backed alpha-release defect and unresolved upgrade and release acceptance work block merge. Priority: P1 Review scores
Verification
How this fits togetherThe Windows tray app reads the existing Inno installation, protected migration records, and Store package identity before starting normal services. Its migration flow can stop the old app, preserve shared data and gateway state, and resume the Store app after verified removal. flowchart LR
A[Inno installation] --> B[Installation and record checks]
C[Protected consent] --> B
B --> D[Migration window]
D --> E[Preserve shared state]
E --> F[Verify old app removal]
F --> G[Store app startup]
Decision needed
Why: The branch's tag-time plan conflicts with the repository's scheduled alpha tagging and release pipeline; choosing when to expose migration requires release-owner intent. Before merge
Findings
Agent review detailsSecurityNone. Review metrics
Root-cause clusterRelationship: Members:
Proposal only: this assessment does not dispatch repair, suppress jobs, mutate sibling items, close, or merge anything. Merge-risk optionsMaintainer options:
Technical reviewBest possible solution: Keep migration off in automatically published alpha builds, prove foundation-record upgrade compatibility, and activate the coordinated release only after its exact artifacts pass the documented acceptance gates. Do we have a high-confidence way to reproduce the issue? Yes for the review finding: the scheduled alpha workflow supplies a prerelease Inno version to a Release build whose new migration action requires a stable version. This was established from source, not by running an alpha release. Is this the best way to solve the issue? No. The migration flow has substantial runtime proof, but its default enablement needs an alpha-release gate and direct old-record upgrade coverage. Full review comments:
Overall correctness: patch is incorrect AGENTS.md: found and applied where relevant. Codex review notes: model internal, reasoning medium; reviewed against 24263d3b06c4. LabelsLabel changes:
Label justifications:
EvidenceWhat I checked:
Likely related people:
Rating scale
Overall follows the weaker of proof and patch quality. Workflow
HistoryReview history (15 earlier review cycles; latest 8 shown)
|
2735a63 to
dd85663
Compare
|
Note on the red X from the Connection Tests job: it is a known flake in The case deliberately holds This branch cannot reach that code path. Its only change to I added the test and its 30 second budget in #1461, so it is mine to fix. Doing that separately in #1536 rather than here, to keep this PR's diff scoped. |
Adds the consent store, recovery discard, and the startup/finalization decisions the Store handoff needs on top of the migration foundation. Records are judged one file at a time and on their own contents rather than on the exception raised while reading them, because decoding is pure in-memory work: a malformed length surfaces as IOException and must read as corruption, not as contention. Corruption routes to recovery, which is the only stage that offers a way out, so a damaged record can no longer block startup with no in-app escape. The completion receipt is planned last so an interrupted discard still looks like a handoff that needs recovery. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: e51d7c52-25e3-43f8-a83a-31c105ba5b52
Adds the migration window, Settings card, and consent copy to all six locales, including the discard and failure states used by recovery. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: e51d7c52-25e3-43f8-a83a-31c105ba5b52
Replaces the native dialog loop with a WinUI window offering directly labeled Migrate, Not now, and Retry actions, plus an entry point on the Settings page. StoreMigrationWorkflow owns serial interaction, StoreMigrationOperations adapts the migration coordinators, and StoreMigrationWindow applies the UI, so the startup guard keeps only the compile-time gate and pre-services window lifetime. Recovery surfaces a discard action, and a discard that could not run is reported instead of failing silently. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: e51d7c52-25e3-43f8-a83a-31c105ba5b52
Resolves the effective migration switch from Migration.Build.props so disabling it produces a release without migration instead of making release impossible, and compiles installer.iss on every PR rather than only at tag time. The uninstall cleanup walks every ancestor before deleting, stopping at a UNC share root, so a junction above the target cannot redirect a recursive delete. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: e51d7c52-25e3-43f8-a83a-31c105ba5b52
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: e51d7c52-25e3-43f8-a83a-31c105ba5b52
Adds engine, workflow, localization, build-configuration, and mounted UI proof. The reparse-ordering assertions are replaced with a behavioral test that runs the shipped guard text against a real junction, because the source-text version passed while the bug was live: it asserted that markup existed, not that it ran. Each recovery fix is covered by a test confirmed red against the code it fixes. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: e51d7c52-25e3-43f8-a83a-31c105ba5b52
Addresses two review findings against the Store migration startup policy. Keep the Store app inactive when migration is declined. Issue openclaw#1374 specifies that Not now "leaves Inno unchanged and MSIX inactive", but declining left BlocksStartup false, so the Store app started anyway. StoreMigrationWorkflow now blocks whenever the window sits at the consent stage, which covers both Not now and dismissing the window without answering. That stage is reached only with a detected installation, so the block cannot strand a user whose previous app is already gone. The informational stages stay non-blocking: the issue does not ask for them, and an unsupported source, a failed inspection, or an undecodable record can each occur after removal, where refusing to launch would leave no working app at all. Release the startup block once source removal is proven. Finalization verifies removal partway through, but every failure path after that point still refused launch while the retained completion receipt blocked the previous app, which was by then uninstalled. Neither app was usable, permanently, with no in-app escape. StoreMigrationFinalizationDecision now carries SourceRemoved, set on every return after verification and left false on every return before it. The workflow latches it and drops the block. The failing stage stays visible and the receipt is still retained, so the user sees what went wrong and the failed step is retried on the next launch, but it is no longer a dead end. SourceRemoved is kept separate from AllowsNormalStartup on purpose: AllowsNormalStartup also selects the visible stage, so folding the two together would have reported a failed finalization as Ready. Migration2_ConsentHint is updated in all six locales, since Not now now carries a consequence the consent screen has to state. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: e51d7c52-25e3-43f8-a83a-31c105ba5b52
Two documented behaviors no longer matched the code. RELEASING.md still described Not now as closing the window after which "the Store app then starts normally", which is the behavior the companion change inverts. Release and QA operators reading it would have validated against the wrong contract and could have filed the intended block as a regression. ONBOARDING_WIZARD.md and RELEASING.md both claimed production defaults remain disabled, while Migration.Build.props checks MigrationProductionEnabled in as true for non-Dev Release builds. RELEASING.md also contradicted itself, since its own build-contract table already documented the default as true. Both sites now state the real gate: the switch ships enabled, and publication is held by tag-time acceptance rather than by the checked-in default. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: e51d7c52-25e3-43f8-a83a-31c105ba5b52
An unsupported or out-of-date Inno registration reached an informational stage without a completion receipt, so dismissing that window let the Store app start its normal services while the previous installation was still on disk. Issue openclaw#1374 permits one active production client, and a source the user must update or replace is still a client. Block on positive evidence rather than on the registration alone. The detector now reports SourcePayloadPresent, and startup admission splits its decision into two halves with different durability: - HoldsDurableBlock latches. Data has moved, or is about to, so a later inspection that fails must not drop the block. - SourceBlocksStartup never latches. It asserts a source is installed right now, and removing that source is how the block is meant to end. Latching both together would strand a user whose uninstall succeeded but whose next inspection threw, leaving no working client at all. Requiring payload evidence keeps an interrupted uninstall, which can leave a registration with no payload behind, from blocking on nothing. Payload evidence is held outside the inspection's try block so a failure raised while validating the rest of the installation cannot report an app that is really on disk as absent, and the probe treats an unreadable registration as contributing no evidence rather than abandoning the whole inspection, so one bad registration cannot mask a live sibling. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: e51d7c52-25e3-43f8-a83a-31c105ba5b52
The unsupported and update-required notices described the previous installation but not the consequence, so a user reading them had no way to know why the Store app had not started or what would end that state. Phrase the sentence conditionally. These same stages appear when a registration is left behind with no payload, and there the app does start, so an absolute claim of inactivity would be false in that case. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: e51d7c52-25e3-43f8-a83a-31c105ba5b52
…y point Consent now blocks startup by design, so the three StoreMigrationWindow proof tests that dismissed consent must expect an inactive Store app. The settings status InfoBar was a visible StackPanel child even when closed, so it consumed layout spacing and pushed the bottom settings row past the scroll viewport clip, zeroing two checkbox bounding rectangles. Collapse it until there is a status to show. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 16c59d10-1f82-4ce4-9c06-995ce44038ee
…ders Collapsing the bar while idle removed its automation peer, and InfoBar announces through the existing peer only, so the open-time announcement was dropped. Realize the peer before opening, re-open a bar the user never dismissed so a second result is announced instead of silently swapping text, and only re-collapse on a close that stuck. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 16c59d10-1f82-4ce4-9c06-995ce44038ee
A prerelease Inno DisplayVersion such as 2026.9.5-alpha.19 never parses as a stable release version, so the detector refused it with no RegisteredVersion to compare against the minimum. Startup policy only recognized a version problem when a version had parsed, so a prerelease fell through to UnsupportedInstallation, which tells the user to check the install location, Windows user, and architecture. None of those is the problem, and the obvious way out is removing the source app, which forfeits the migration. Carry the version refusal explicitly so admission can tell it apart from a location, user, or architecture refusal, and route it to the update guidance that already exists and is already localized. Admission is unchanged: both states block startup, and only the message the user reads differs. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: b5aa05ec-0a5a-461e-aef9-52c6a9c92ba2
The prerelease routing fix changed what a prerelease source is told, but the release guide still described both a prerelease suffix and a mismatched name as producing the unsupported-installation message. Release QA would look for the wrong message when validating the tag. Describe the two refusals separately. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: b5aa05ec-0a5a-461e-aef9-52c6a9c92ba2
Hanselman dual-model reviewClaude Opus 5 and GPT-5.3-Codex independently reviewed this PR's migration, UI, installer, and build changes. No issue was flagged by both models. Both models agree - HIGH consensusNone. One-model findings - LOW consensus
Assessment: No dual-model consensus blocker. The concurrency impact in the Codex finding is contradicted by the shared mutex and secondary-instance forwarding path; the pre-migration startup behavior is intentionally being preserved. No production changes were made as part of this review. Validation reported by Opus: migration-filtered |
1937c76 to
dde0a9b
Compare
rebased to get the test fix |
|
One recovery case worries me here: what happens if uninstall removes the old app but leaves its registry entry? With a valid migration receipt, we still classify that as Could we distinguish a verified orphaned canonical HKCU registration from a generally unsupported install, and let it reach finalization? Both admission and the finalizer's status guard need the change. Keep the registration identity checks, reject machine-wide/conflicting registrations, and independently verify that both executables and the running source are gone. Use strict absence checks in the final verifier, not Keep the existing locking, receipt/inventory checks, and receipt-last cleanup. After successful recovery, the leftover registry entry shouldn't trigger the migration window on every launch. I'd want regression tests for that recovery and subsequent launch, plus surviving executable/uninstaller, running process, and access-denied cases that must still block. 92% confidence in this design with those safeguards, not a claim that the fix has been implemented or runtime-validated. |
Uninstalling the Inno app can leave its HKCU uninstall key behind. The detector classified that leftover key as Unsupported, so the Store app refused to start while the old executable was already gone, leaving the user with no working app at all. Add an OrphanedRegistration status for a canonical HKCU registration whose identity checks pass but which has neither an executable nor an uninstaller payload, and admit it to finalization instead of blocking. Machine-wide and conflicting registrations are still rejected before the orphan check runs, and finalization independently re-verifies removal rather than trusting the earlier detection. Replace File.Exists with a tri-state probe so absence is proven rather than assumed: an unreadable or access-denied probe now fails closed and does not count as removed. A single surviving remnant stays Unsupported. After a successful recovery the leftover registry entry no longer triggers the migration window on later launches, because the receipt is cleared. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: b5aa05ec-0a5a-461e-aef9-52c6a9c92ba2
Finalization can run inside OnLaunched, before the app's XAML content is composed. Calling StartupTask.GetAsync on the UI thread there fail-fasts through Microsoft.UI.Xaml as a stowed exception (0xc000027b), which kills the process past every managed catch, so no crash log and no .NET Runtime event is produced. Because the process died before the receipt was cleared, the receipt stayed on disk and every subsequent launch crashed the same way, permanently locking the user out of the Store app. Marshal the WinRT call off the UI thread so the failure stays catchable and reaches the existing finalization error handling. Verified on a real machine with a signed MSIX: finalization now completes, auto-start is applied, and the receipt is cleared. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: b5aa05ec-0a5a-461e-aef9-52c6a9c92ba2
@bkudiess Addressed in the latest 2 commits! |
Closes #1374
PR 2 of 2: the complete migration experience, plus production enablement. PR 1, #1461 (feat(migration): gate Inno-to-Store migration preview and preserve the gateway on uninstall), has merged, so this targets
mainand the diff is PR 2's work alone. There is no planned PR 3.Production migration is enabled by default for Release non-Dev builds, so the coordinated
v2026.9.5Inno and Store artifacts both carry migration, and CI publishes a package developers can actually install to exercise it.Merging this does not ship migration to users; the release tag does. Real Store, ARM64, and live-gateway acceptance are tag-time gates.
Since this writeup was first drafted the branch has been through seven rounds of independent dual-model adversarial review. They found four fail-open defects, then a class of misclassified-corruption defects that could lock a user out of the app with no in-app escape, then two paths that missed a previous app that was actually installed. All are fixed and covered by tests. See Correctness hardening and Lockout defects.
Head
2fcf1577, onmainat5a595352. This is a cross-repository PR from a contributor fork, so Allow edits from maintainers is enabled.What Problem This Solves
The migration foundation needs a clear, accessible handoff that obtains consent, stops the previous app safely, and guides users through removing it before the Store app becomes active.
User Impact
Explicit Debug previews and opted-in production-identity Release builds provide a wizard-style migration window with Migrate / Not now, progress, manual close / Retry recovery, and Open Installed apps. Supported settings, gateway data, identities, and models remain in place. Users must uninstall the previous Inno app themselves; migration does not force-kill or automatically uninstall it.
Release non-Dev builds at the current head enable this flow by default. The shared build policy pins the source floor and product ID and now sets
MigrationProductionEnabled=true; Debug and Dev builds still cannot inherit production activation. The confirmed Store listing is OpenClaw, product ID9NFPR3BGDRR5. Actual Store-distributed handoff and release acceptance are still pending, and are now tag-time gates rather than merge-time gates.Enablement and the developer migration test package
Neither existing MSIX artifact can exercise migration. The unsigned Store package is a submission asset and cannot be installed, and the Dev package is refused by
StoreMigrationStartupGuardbecause its identity is not the production one. Enabling the flag alone would therefore have produced artifacts nobody could test.CI now also publishes
openclaw-msix-dev-migration-test-<arch>fromscripts\Export-MigrationTestMsix.ps1: the production-identity Store package, test-signed with a disposable certificate whose subject matches the production publisher. "Dev" in that name means for developers; the package identity is production. ItsINSTALL.txtstates plainly that it shares a package family name with the Store release and is therefore not side-by-side, that migration runs against real data directories because the guard rejects path redirection, and that it will adopt and uninstall an Inno installation. The exporter refuses an already-signed package, and refuses any package whose assembly lacks the migration metadata, so the artifact cannot silently ship disabled. Measured export results are under Real behavior proof.Because every unhappy Store-side path stops launch rather than degrading,
docs/RELEASING.mdnow also requires confirming before the tag that the released Inno installer registersDisplayVersion2026.9.5andDisplayNameOpenClaw Companion version 2026.9.5. A prerelease suffix or mismatched name is rejected byInnoInstallationDetector, which blocks startup for that user. The already-publishedv2026.9.5-alpha.*builds carry a prerelease suffix and fall into that category, and are routed to the update guidance rather than the generic unsupported message.Why This Change Was Made
Ownership:
StoreMigrationStartupGuardpreviously hosted the native dialog loop;StoreMigrationWorkflownow owns serial interaction,StoreMigrationOperationsadapts existing migration coordinators, andStoreMigrationWindowapplies the UI. The startup guard retains the compile-time gate and pre-services window lifetime.InnoMigrationHandoffowns the gated consent/listing/shutdown integration.Startup while a previous app is installed
#1374 states under Release validation: "Without valid intent, MSIX requires confirmation; Not now leaves Inno unchanged and MSIX inactive."
Not now now conforms.
StoreMigrationWorkflow.BlocksStartupblocks whenever the window is at the consent stage, so declining, or dismissing the window without answering, leaves the Store app inactive while the previous app is still installed. That stage is only ever reached with a detected installation, so the block cannot strand a user whose previous app is already gone. Covered byDecliningConsent_LeavesTheStoreAppInactive.UnsupportedInstallationandUpdateInnoalso block, but only while the source payload is actually present on disk, and that half is never latched because removing the source is how the block is meant to end. An interrupted uninstall can leave a registration with no payload, and blocking on that alone would leave no working app at all. Both blocking paths are captured on a real machine under Unsupported-source startup block.Two states deliberately remain non-blocking, and maintainer acknowledgement is requested for these two only:
InspectionFailed,RecoveryRequiredSupporting context:
main. Shipped builds evaluateDisabled, whoseAllowsNormalStartupistrue, so the Store app already starts normally beside an installed Inno app today. This PR adds the first startup block that has ever existed; it does not remove one.AppIdentity.MutexBaseName(OpenClawTray), whichinstaller.issalso uses asAppMutex. A second launch forwards its activation to the running instance and exits, so shared settings, gateway records, and identity are never written by two live apps. The single-active-client property does not depend on this startup policy.Rebasing onto main after PR 1 merged
PR 1 landed on
mainas squash commit5a595352, so its individual commits are not ancestors ofmainand a plain rebase replayed work already merged, conflicting on the first commit. The branch was instead rebuilt frommainand PR 2's net change applied on top. The rebuilt tree was verified to be identical to the previously reviewed headd2c8e3faexcept formain's own LF fix (#1518), so nothing was lost or silently altered in the transfer:PR 2 retains its WinUI workflow and directly labeled Migrate / Not now / Retry buttons, not PR 1's obsolete native dialog loop. All six locales retain the legacy templates and concise composed PR 2 disclosures. Production/native XAML metadata delegation remains intact.
Selected release plan
The selected target is the next Inno release after the current published
v2026.9.4:v2026.9.5, withMigrationMinimumSourceVersion=2026.9.5.0pinned inMigration.Build.props. Both PR 1's safeguards and PR 2's complete migration flow must be included in that coordinated release. No separate preparatory Inno release is required.Users on older Inno versions must update to
v2026.9.5or newer before migrating to Store. The minimum stays fixed in subsequent releases; it must not track each later app version or the Store package version.This resolves release-version selection and configuration pinning, not artifact verification or production activation. If the next release receives a different version or does not contain both PRs, revise the pinned minimum before shipping.
Evidence
About the commit hashes below. Short hashes such as
f0017f4dand5714c8aaare this branch's pre-rebase history (see Rebasing onto main). They no longer resolve in this repository, but remain reachable in the fork at natalie-aguinaldo#2. They are kept so each piece of proof still names the exact source it was captured against.Everything in the screenshot sections below still describes the shipped flow; the later commits change error-path behavior, uninstall safety, CI gating, and tests, not the workflow the screenshots show.
Measured results are in Validation; captured runs are in Real behavior proof. Failed automation attempts are not counted as passing proof.
Correctness hardening after adversarial review
The migration flow was put through an independent read-only review after the UI work settled. It refuted most
of my own earlier self-assessment and found four HIGH-severity defects that shared one root cause: the
completion receipt was authoritative only where a receipt was successfully read, so every error path fell
through to "start normally." On a machine that had already migrated, that means the old Inno app launches again
against data the Store app now owns.
File.Existsfails open. It returnsfalsefor a directory of the same name, a missing parent, or invalid characters, and never throws, so thecatch { return true; }fallback was dead code and the doc comment promised the opposite of the behavior.MigrationStartupRecordReader.CompletionFileExistsnow usesFile.GetAttributes, which throws distinguishably. OnlyFileNotFoundExceptionandDirectoryNotFoundExceptionreturnfalse; everything else returnstrue.gatewaysredirects the whole subtree whilegateways\<id>still reports as an ordinary directory, so a recursive delete followed it. The reviewer reproduced real data loss under PowerShell 5.1.Uninstall-LocalGateway.ps1now walks every ancestor up to$DataDirbefore deleting, and stops at a UNC share root so folder-redirected AppData cannot block uninstall.StoreMigrationStartupGuardinspected twice and discarded the first result, so a successful first pass followed by a failing second pass lost the receipt._holdsCompletedHandofffrom it.catchin the workflow could not tell "no receipt" from "could not tell," andStoreMigrationStartupCoordinatorthrewInvalidOperationExceptionwhen a completed migration had no receipt.IStoreMigrationOperationsgainedHoldsCompletionReceipt(), deliberately independent ofInspect(), with a non-throwing variant that fails safe totrue. The coordinator returns aRecoveryRequireddecision instead of throwing.Source-text contract tests gave false assurance here. The reparse-ordering assertions passed while the bug
was live, because they asserted that markup existed, not that it ran. They were replaced with a behavioral
test that extracts the shipped guard text from
Uninstall-LocalGateway.ps1, wraps it in a harness, and runs itunder
powershell.exeagainst a real junction. It was confirmed to fail against the pre-fix code and passafter, and the junction case it catches is exactly the one the leaf-only guard missed.
Two further release-path gaps were closed in the same round:
Export-MigrationTestMsix.ps1throws when the packagedassembly carries no
MigrationMinimumSourceVersion, and that step ran unconditionally inbuild-msix, whichreleasedepends on. SettingMigrationProductionEnabled=falsetherefore made every release impossiblerather than producing a release without migration. CI now resolves the effective switch from
Migration.Build.props(honoring an environment override) and gates both the export and its upload on it.installer.isswas only compiled for real in the tag-only release job, so a Pascal Script error wouldhave been found at release time. CI now runs
scripts/Test-InstallerScriptCompiles.ps1 -RequireCompileronevery PR.
Lockout defects found in later review rounds
Five further dual-model rounds targeted the recovery path specifically. They converged on one bug class appearing in three places: migration records are DPAPI envelopes, and decoding is pure in-memory work, but
BinaryReaderreports a malformed 7-bit string length as a plainSystem.IO.IOException. Any code that lumps the decode together with file I/O therefore reads corruption as an operational failure.That distinction decides whether a user can recover.
StoreMigrationStartupCoordinatormapsUnavailabletoInspectionFailedandInvalidtoRecoveryRequired, and the discard button renders only in Recovery. A malformed receipt was classifiedUnavailable, which blocked startup while offering no way out.StoreMigrationRecoveryDiscardclassifies each file on its own contents, and plans the receipt last so an interrupted discard still looks like a handoff needing recovery.IOExceptionand was read as lock contention.IOExceptionis allowlisted in the decode block only, matching the existingInnoMigrationConsentStore.ReadConsentpattern.MigrationStartupRecordReader.ReadFilemade the escape hatch unreachable for exactly the corruption it exists to clear.IOExceptionis wrapped asInvalidDataException, so the record reaches Recovery rather than InspectionFailed.FileMode.Open, so a missing lease file failed instead of being created.FileMode.OpenOrCreate, with a regression test asserting a missing lease does not fake success.CreateProtectedDirectoryreuses the drift checkMigrationOperationLockalready applies for the same reason instead of duplicating it. An already-hardened directory is left alone; real drift is still corrected.Each fix was proved by reverting it and confirming the new test went red.
Three pre-existing lockouts are deliberately out of scope here, because they are not introduced by this PR and each needs its own guidance text: a directory named
completed.dpapi, a reparse point above the records folder, and an access-denied ACL on the receipt. All three yieldUnavailablewith a receipt present.Detection hardening in the final review round
Because
UnsupportedInstallationandUpdateInnoblock only when a payload is present, a false negative inInnoInstallationDetectorsilently downgrades that block to "start anyway". Two paths did exactly that:Detect's outerInvalidDataExceptionandBadImageFormatExceptionhandlers returnedUnsupported()with the payload flag left at its defaultfalse, discarding evidence the probe had already gathered (reachable by appending bytes toapp-identity.txt), andHasTrayExecutablecaught too narrow a set of exceptions, so one unreadable registration short-circuited the check toInspectionFailedand masked a live sibling. The flag is now hoisted above thetryand threaded through every return, and the probe's catch coversIOException,UnauthorizedAccessException,SecurityException, andInvalidDataException. Both verified red-to-green.Merging PR 1 into this branch
PR 1's branch gained ten commits while this one was in review. They were merged at
f284c258. Six filesconflicted. Each was resolved in favor of whichever side made the later, narrower decision, and PR 1's
behavioral fixes were ported into this branch's architecture rather than reverting the architecture to match
them.
StoreMigrationStartupGuard.csStoreMigrationAutoStartRefusedException, and the workflow mapsStartupPreferenceRefusedto a newStartupRefusedstage before the allows-normal-startup fold. It reuses the existingMigration_StoreStartupRefusedstring and stays outsideBlocksStartup, so the user is told that only Windows can re-enable the startup task without being trapped.MigrationOperationLock.csScrubExistingEntries, which this branch'sMigrationRecordStorageextraction had silently weakened.InnoMigrationStartupGuard.csStoreMigrationFinalizationCoordinator.csAllowsNormalStartup, so a transientStartupPreferenceFailedno longer strands the user on a failure card. Only a durable refusal notifies.MigrationRecordTests.csconsentkind.InnoMigrationContractTests.csStorePreviewChoices_UseNativeYesNoWithoutActionLegendswas deliberately dropped because it asserts theMessageBoxWflow this branch deleted, and itsTask.Run(...).ConfigureAwait(false)assertion no longer applies to an applier that plain-awaits.docs/RELEASING.mdThe merge surfaced four test failures that were genuine semantic collisions rather than noise, all from PR 1's
broadened
AllowsNormalStartupand this branch's newStartupRefusedstage. They were resolved by followingPR 1's semantics and adding behavioral coverage for the durable-refusal path.
Change Type
Scope
winnodeRequired proof pools
windows-winui-interactive: migration and Inno consent UX, focus/accessibility, localization, and activation behavior.windows-clean-installer-upgrade: exact supported Inno to Store package handoff, protected uninstall, and finalization.windows-11-arm64: architecture-matched ARM64 package/runtime acceptance.windows-wsl-gateway-e2e: existing managed WSL gateway preservation and continued management after handoff.These declarations request proof; they do not claim the complete pools passed.
Validation
Commands ran in the isolated PR 2 worktree with
OPENCLAW_REPO_ROOTset to that worktree. UI and Tray checks used isolatedOPENCLAW_TRAY_DATA_DIRdirectories. The first four rows were rerun at current head2fcf1577, that is, after the branch was rebuilt on mergedmain..\build.ps12fcf1577dotnet test .\tests\OpenClaw.Shared.Tests\OpenClaw.Shared.Tests.csproj --no-restore2fcf1577dotnet test .\tests\OpenClaw.Tray.Tests\OpenClaw.Tray.Tests.csproj --no-restore2fcf1577dotnet test .\tests\OpenClaw.Connection.Tests\OpenClaw.Connection.Tests.csproj --no-restore2fcf1577.\scripts\Test-InstallerScriptCompiles.ps15714c8aaMigration2LocalizationTestsandInnoMigrationContractTests0651b419MigrationBuildConfigurationTests5714c8aa.\scripts\validate-docs.ps12fcf1577CleanupScript_DoesNotDeleteThroughAJunction5714c8aa.\scripts\test-ci-workflow-contract.ps15714c8aa.\scripts\test-migration-test-msix.ps15714c8aaMigration2LocalizationTests,InnoMigrationContractTests, andStoreMigrationWorkflowTestsf0017f4dd2c8e3fa.\build.ps159e06bcddotnet test .\tests\OpenClaw.Shared.Tests\OpenClaw.Shared.Tests.csproj --no-restore59e06bcddotnet test .\tests\OpenClaw.Tray.Tests\OpenClaw.Tray.Tests.csproj --no-restore59e06bcdFullyQualifiedName~Migration59e06bcdThe fourteenth test is new: it mounts the production window, drives finalization to
StartupPreferenceRefused, and asserts the refusal copy, the collapsed Primary button, the polite live region, and that dismissing resumes startup because no receipt blocks it.Mounted tests cover light/dark rendering, native small/large window icons, native button styles and spacing, stage headings, safety notices, and compact scrolling without hiding footer actions. The compact test enlarges heading/status fonts; it is not OS-wide text-scaling proof.
Historical regression coverage at the preceding published merge
3fd9358d: Connection 1,123 passed / 1 skipped; combined migration/native-chat/markdown/tool UI 97 passed / 1 skipped. Those broader suites were not rerun for this presentation-only follow-up. The prior UI skip wasReactorChatLayoutProofTests.ReasoningPicker_PointerReleaseCommitsButCaptureLossDoesNot.Adversarial dual-model review of the full
ac5ff781+59e06bcddiff (Claude Opus and GPT Codex, independent passes) returned no CRITICAL or HIGH findings. Both models independently confirmed that machine-wide and conflicting-registration checks run before the orphan check, that every absence probe fails closed, and that finalization re-verifies removal rather than trusting detection. The LOW findings that survived cross-referencing are listed as residuals under Real behavior proof.Real behavior proof
Enablement and the migration test package (current head)
The flag flip was verified against a real Release binary rather than only the MSBuild probe. A plain
dotnet build -c Release -r win-x64of the tray project produced an assembly carrying the pinned policy:scripts\Build-StoreMsix.ps1 -Architecture x64then produced a real unsigned Store package (Identity: OpenClawFoundation.OpenClaw 2026.9.5.0 x64), andscripts\Export-MigrationTestMsix.ps1was run against it end to end with the real Windows SDKSignTool.exe:OpenClaw-MigrationTest-x64.msix(157,079,494 bytes),OpenClaw-MigrationTest.cer,msix-metadata.json,INSTALL.txtOpenClawFoundation.OpenClaw,CN=4BA40A7A-B719-4C40-BF91-84AF4F1136FC,2026.9.5.0CN=4BA40A7A-B719-4C40-BF91-84AF4F1136FC(matches the manifest publisher)signing=migration-test-only,migrationEnabled=True,sideBySideWithStore=FalseCert:\CurrentUser\Myafter the runGet-AuthenticodeSignaturereportsUnknownErroron the signing machine because that disposable certificate is deliberately not trusted there; trusting it is step 3 of the generatedINSTALL.txt. The exporter verifies the signer thumbprint rather than machine trust, so it does not modify machine trust in CI.Not verified / blocked: this package was not installed or launched. Installing it replaces the production package family, so it belongs on a disposable VM, and the Store-side migration UI proof below is still the
f0017f4drun.Store migration entry point on the Inno Settings page (current head)
This is the complete path a user takes, from the installed Inno app to the Store app finishing the move. Steps 1 and 2 are current-head proof. Steps 4 through 7 reuse the
f0017f4dmanual VM run, because the Store listing is not live, so a Store-distributed install cannot be exercised yet.C4D2F8E4BD98B91E4C692A21DA2078301E43AE4333EA242E26D8C0A27D1A0A17, 972/972 payload hashes matched, measured at 985.6 client DIPs.In context with the settings rows below it, showing matching row height and text alignment:
Consent and handoff. Choosing the primary action records protected consent bound to the source version, user, architecture, canonical paths, and target package identity, then opens the Store listing.
InnoMigrationHandoff.GrantAndLaunchAsyncgrants before launching, so a user who abandons the Store has still granted. Choosing the neutral action records nothing.Install from Microsoft Store. The user installs the Store package themselves. Not exercised from a live listing; the
f0017f4drun used a locally test-signed MSIX.Store app requests shutdown. With a valid grant, the Store app asks the Inno app to exit through the current-user activation pipe. There is no force-kill. If the source does not exit, the user closes it manually and clicks Retry.
Layout bug found by VM proof, not by tests
The card action was specified to sit inline beside the text. VM capture showed it stacked below the text at 1085.6 and 1517.6 client DIPs. The
VisualStateManager.VisualStateGroupsblock was declared on a Grid nested inside the card, and WinUI only evaluates state triggers declared on the page root, so theAdaptiveTriggernever activated at any width. The full Tray suite passed throughout, because the contract test only asserted that the trigger existed somewhere in the card markup.Re-proofing after moving the group to the page root then showed the fallback was unreachable in principle:
HubWindowdeclaresMinWidth="1000", so a 720 trigger is permanently satisfied and the stacked state can never render for any user. Repeated attempts to size the window to 680 DIPs produced 985.6. The states, trigger, and setters were therefore removed and the card is a single row. The contract test now asserts that noAdaptiveTriggerorVisualStateManagergroup exists in the file, and names the window minimum as the reason.Deliberate reversal of the disclosure contract added in
f0017f4dMigration2LocalizationTests.InnoDialog_DisclosesSafetyBoundariesInEveryLocalewas added inf0017f4dto require the long-form consent copy verbatim in every locale, including the 30-day term and the neutral/primary button labels. That test has been rewritten to assert a reduced set of disclosures.This is an intentional policy change, not an accidental weakening. The dialog previously restated the uninstall and Retry steps, which
Migration2_AwaitingRemovalalready delivers at the moment they apply, so the same user read them twice, the first time out of context. The dialog now discloses what the user is agreeing to at that moment: the Store opens, the Store app can close this app and finish the move without asking again, and supported data stays in place. The 30-day expiry is still enforced byInnoMigrationConsentStore; it is no longer recited in the dialog.docs/RELEASING.mdacceptance disclosures for the Store preview window cover different keys and are unchanged.Current-head native icon diagnostics
StoreMigrationWindownow calls the existing WinUIExSetIcon("Assets\\openclaw.ico")helper, matching other application windows. Its custom XAML title-bar image alone did not populate native HWND icons.The new
Consent_AssignsNativeIconsForTaskbarAndWindowSwitchertest mounts the production window with in-memory migration operations and queriesWM_GETICONon its live HWND. It failed before the production fix because an icon handle was zero. Current-head output:Types 0 and 1 are the native small and large icons. This directly verifies native icon assignment. A developer-captured Explorer taskbar-hover screenshot from an isolated current-head VM preview confirms that the OpenClaw icon is visibly rendered. The restored VM originals were not modified for this follow-up.
Current-head taskbar preview
The screenshot was captured from the isolated 72e6940 preview with in-memory operations. It is visual icon proof, not another migration lifecycle run.
Startup-refusal window state (current head)
StartupRefusedarrived with the PR 1 merge and is the newest visible state. It is reached when migrationfinishes but Windows refuses to enable the startup task, which is durable and cannot be retried from inside
the app. The window therefore states what happened, tells the user where to change it, and offers only Close.
Captured at
5714c8aafrom the mounted production window with in-memory operations, the same harness used forthe native icon proof. It is visual proof of the rendered state, not a migration lifecycle run: no real
finalization, package, or startup-task change is exercised.
Unsupported-source startup block on a real machine (current head
2fcf1577)The startup block added in
d3a1e96dhad unit and mounted-window coverage only. It was captured on a real guest in the two states that reach it from opposite directions: a supported source the user declines, and an outdated source they cannot migrate from yet.OC-AUTO-A91896, userMigrationTest. No cloud lease ID or hosted run URL.msix-metadata.jsonrecordssourceCommit 2fcf1577...,sourceTreeDirty false,migrationEnabled true. The package isOpenClawFoundation.OpenClaw_2026.9.5.0_x64, locally test-signed, not Store-distributed. An older package on this guest carried the same version, so the installed tray executable was hashed to confirm which build was running (8CB8FEE9...D367F454).2026.9.5, because no stable2026.9.5exists publicly and the pinned floor is2026.9.5.0. Scenario C used the real publicv2026.9.4release, which is genuinely below that floor.2026.9.52026.9.4ConsentRequiredUpdateInnoFalse(see caveat)TrueScenario A: supported source, user declines
Scenario C: source below the supported floor
Caveat on the logged
source payloadfield. Scenario A logsFalsealthough the payload was present. The flag is threaded only through the paths that consume it,UnsupportedInstallationandUpdateInno, which is why scenario C reportsTrue;ConsentRequiredblocks through the consent stage and never reads it. That is a diagnostics inaccuracy with no behavioral effect, deliberately not fixed here. Builds predating this work logged nosource payloadterm at all, so its presence is what dates these captures, as does scenario C's notice text, added in2fcf1577.Not verified: the package is locally signed rather than Store-distributed, both runs are x64, and neither completes a migration. They prove refusal to start while a source is present.
Manual VM migration at
f0017f4dOpenClaw-Migration-Auto-x64-a91896be, VM IDd0cd56dc-6a26-4493-8bc6-89f6cf2de790. No cloud lease ID or hosted run URL.f0017f4d035d70093f2ffe7e8b2fd3373c2bfaf1, tested before commit with no later source edits. Release x64, production package identity,DevBuild=false, migration enabled only by proof-time CLI override. Checked-in source floor2026.9.5.0was not overridden; the checked-in production default was still disabled at that time and is enabled at the current head.2026.9.5.0and locally test-signed MSIX2026.9.21.0. These are not official Store/release artifacts.gateway.example.testconfiguration and credential, with startup/node/MCP disabled. Connection/authentication errors in the normal-app screenshot are expected for that non-live gateway; they do not prove gateway connectivity.Test-InnoMigration.ps1returned exit code 10 for the canonical current-user source, confirming migration-preservation eligibility. The source app was no longer running.Store migration finalized:at 21:18:39.499, followed byApplication started (WinUI 3)at 21:18:40.755, after the pre-Retry verification gate. The same verified Store process continued. Consent, intent, and completion records were removed by finalization; settings, gateway, and generated identity hashes remained unchanged.Store-side consent at
f0017f4dSteps 5 through 7 of the walkthrough above are the rest of this run's screenshots; they are not repeated here. The one view the walkthrough does not cover is the Store-side consent window, whose Inno-side counterpart has since been rewritten.
All are developer-captured from the real VM and the exact
f0017f4dtested build, not mounted fake-operations captures. Every embedded URL returned HTTP 200 with image/png, and downloaded SHA-256 hashes match the inspected developer screenshots.Live migration lifecycle at
5714c8aaA second manual VM run exercised the whole lifecycle at
5714c8aainstead off0017f4d, using the checked-in production default rather than a proof-time CLI override.OC-AUTO-A91896, userMigrationTest. No cloud lease ID or hosted run URL.5714c8aa88a62fec8e339ba1215fda432eee6b58, clean tree (sourceTreeDirty: falseinmsix-metadata.json).OpenClawCompanion-Setup-x64.exe(ProductVersion2026.9.5, payload FileVersion2026.9.5.0) and locally test-signed MSIXOpenClawFoundation.OpenClaw 2026.9.5.0. Neither is an official Store or release artifact.InnoInstallationDetectorcondition (canonical per-user path,DisplayNamematchingDisplayVersion, publisher, exact quotedunins000.exeuninstall string, complete payload,releaseidentity, matching executable FileVersion), with no machine-wide registration present.Test-InnoMigration.ps1exit 0startup admission: ConsentRequired (receipt: False)preparation failed: Device identity is malformed or unreadable.preparation completed/completion recordedTest-InnoMigration.ps1all absentFinalizationRequired (receipt: True)/finalizedPreservation result. Comparing SHA-256 of every tracked file across the real uninstall,
gateways.json,gateways\<id>\device-key-ed25519.json, andsettings.jsonwere unchanged. Onlyconsent.dpapi,consent.lock, andintent.dpapidisappeared, which is finalization consuming its own records.The refusal below and the checker's exit-11 branch are proven here for the first time against a live migration rather than a mounted window. The card is deliberately generic.
What this run does not establish.
wss://gateway.example.test/with a shared token) and was never paired, so this is not live gateway continuity. The authentication error in the normal-startup capture is expected. Continuity against a real paired gateway is proven separately in the current-head lifecycle run.gateways.jsonwas hand-authored mid-run after an operator seeding mistake overwrote the pre-existing fixture. The preservation assertion still holds, because it compares hashes across the real uninstall, but that file is not an organically produced artifact.%APPDATA%\OpenClawTraywas not pristine: a stale zero-byteprepare.lockand the generated identity directory predate this run. The source install was fresh and the baseline checker returned0.Full migration lifecycle and gateway continuity at current head (
2fcf1577)The same developer workstation and the same pre-paired gateway as the
5714c8aarun above, re-run end toend at current head. This run adds the two things that one lacked: a tracked-file hash manifest taken
before and after, and the tray log for every admission decision.
CPC-nagui-AVM65, Windows10.0.26691.0, x64. Not a VM, and no cloud lease ID or hosted run URL.2026.9.5as the source, locally test-signed MSIX2026.9.5.0as the target. Neither is a Store or release artifact.ws://127.0.0.1:18790, added and paired via device token before migration started.source payload:term, which only exists from2fcf1577.2026.9.5running and Connected; 9 tracked files hashed16:08:07 ConsentRequired (receipt: False, source payload: False)16:08:07 WARN Migration shutdown request unavailable: Access to the path is denied.consent,intent, andcompletedrecords all present16:09:01 preparation completed/completion recorded16:10:07 FinalizationRequired (receipt: True, source payload: False)/finalized2026.9.5.0, gateway still Connected16:10:08 Application started (WinUI 3)Retained state. Of the six tracked non-record files, five were byte-identical across the real
uninstall:
settings.json, the rootdevice-key-ed25519.json, and all threegateways\<id>\device-key-ed25519.jsonidentity files.consent.dpapiandconsent.lockdisappeared,which is finalization consuming its own records. The WSL distro
OpenClawGateway-Devstayed registered,because the uninstaller's optional gateway-cleanup prompt was declined as the removal card instructs.
gateways.jsonis the one tracked file whose hash changed. Its only write is stamped16:10:08, the samesecond the migrated app started and reconnected, and its post-migration contents keep the same gateway
id,url,identityDirName, shared token, andrequiresV2Signature. That is consistent with alastConnectedrefresh on reconnect rather than drift, but the snapshot stores hashes only, so afield-level before/after diff is not available for this run.
What this run does not establish.
up 3m, so this is continuity of the gateway record and its device credential, not an unbroken socket across the handoff.ConsentRequiredlogssource payload: Falseeven though the payload was present. That is a logging-only defect: the value is passed correctly for the two states that gate startup, and it is documented under Outstanding acceptance and disclosures.Unpaired-gateway block found by VM proof
The live run also exposed a user state with no way forward. A gateway that was added but never paired
has no device, shared, or bootstrap token, so completion refused and directed the user to restore a
configuration that was never damaged. Retrying could not clear it, and deleting the gateway record
reached the same stage through
NoActiveGateway.The refusal protected nothing. Migration leaves gateway state in place, both installs read the same
roaming directory, and the resolved credential was only null-checked, never written to the receipt.
Proving the damaged-credential half showed that branch was unreachable:
MigrationInventory.CaptureIdentitiesrejects an unreadable or corrupt identity during preparation, so completion was only ever reachable when no
credential was configured. The old gate was also weaker than it looked, because a corrupt identity carrying
any fallback token passed it.
The check was removed in
71509850, together with the unreachableNoActiveGatewayandCredentialUnavailablestates, theCredentialUnavailablestage and its localized strings, and theICredentialResolverdependency, so the coordinator structurally cannot resolve credentials. Coverage wasadded for completing without a credential, for an unpaired gateway, and for damage introduced both before
preparation and between preparation and completion.
Store migration status InfoBar: accessibility realization (captured at
2735a636, before the rebase)Covers the review request to capture the current-head status announcement with accessibility diagnostics.
Build under test. Two builds were used. Items 1 through 3 use an unpackaged Debug preview so the Settings entry point is reachable without an installed package. Item 5 uses a real Release Inno installation, which is the shipping configuration.
This is the preview switch that
Migration.Build.propsrestricts to Debug unpackaged non-Dev builds. NoOPENCLAW_TRAY_*_DIRoverride was set, becauseMigrationEnvironment.HasPathOverridewould disable the entry point.The
Move to the Microsoft Store versioncard is visible, and theStoreMigrationStatusInfoBar is rendered inline at full width withErrorseverity and theMigration2_InnoFailedstring. Before this change the bar was opened while stillCollapsed, so it occupied no layout space and never appeared.The
StructureChangeType_ChildAddedrecords show the InfoBar, its severity icon, its message text, and its close button entering the accessibility tree in response to the real button invocation. That realization is whatFrameworkElementAutomationPeer.CreatePeerForElementprovides, and it is the precondition forInfoBarraising its status announcement, sinceInfoBarannounces through an existing automation peer only andFrameworkElementAutomationPeer.FromElementnever creates one.Scope of this evidence. This shows the InfoBar becoming present and readable in the accessibility tree. It is not a capture of the
UIA_Notificationevent itself. Accessibility Insights Event Recorder did not surface aNotificationrow even withListen to All Eventsenabled, andNotificationis not offered in its expected-event list for theWindowcontrol type, so the tool appears not to register forUIA_Notification_EventId. Reported here as observed rather than inferred.Why the error path. The preview binary runs from
bin\Debug\..., soInnoMigrationHandoff.InspectOwnInstallation()correctly rejects it, becauseEnvironment.ProcessPathdoes not match<InstallDir>\OpenClaw.Tray.WinUI.exe. That throw happens beforeInnoMigrationConsentStore.Grant(...)and beforeLauncher.LaunchUriAsync(...), so no consent was written and the Store listing was not opened. The failure is the guard working as intended, and it exercises exactly the sameShowStoreMigrationStatuscode path as the success case, which differs only inInfoBarSeverityand the resource key.Migration behavior is unchanged. Both accessibility commits touch UI only:
b76c2599Pages/SettingsPage.xaml,Pages/SettingsPage.xaml.cs,StoreMigrationWindowProofTests.cs2735a636Pages/SettingsPage.xaml,Pages/SettingsPage.xaml.csNo change to
InnoMigrationHandoff,InnoMigrationConsentStore,MigrationRecordCodec,MigrationEnvironment,InnoMigrationStartupGuard, orStoreMigrationStartupGuard.ShowStoreMigrationStatusruns only on the string returned byawait InnoMigrationHandoff.GrantAndLaunchAsync(), which means it runs strictly after consent persistence and Store launch have already completed, and it only setsMessage,Severity,IsOpen, andVisibility.End to end Inno handoff re-verified on current head. A local Release Inno installer was built from this head and the Inno side of the migration was run for real, to confirm the accessibility change did not regress the functional path.
Release plus non-Dev plus
win-x64, soMigration.Build.propsdefinesPRODUCTION_MIGRATION. Verified in the produced assembly:MigrationStoreProductId9NFPR3BGDRR5presentMigrationMinimumSourceVersion2026.9.5.0presentMigrationPreviewStoreProductIdProductVersion2026.9.5.0+2735a63624c1c2c1b900c5b951c0627b9cede797The build version equals
MigrationMinimumSourceVersion, so it qualifies as a supported source. Installed per user to%LOCALAPPDATA%\OpenClawTray, which is the pathMigrationEnvironment.CreateBinding()requires.Clicking the card and confirming the consent dialog produced the
SuccessseverityMigration2_InnoGrantedInfoBar and opened the Microsoft Store listing. The consent record was written at the same moment:This exercises the same
ShowStoreMigrationStatusmethod as the error case above, on the success branch, and confirms thatInnoInstallationDetectordetection, the supported source check inInspectOwnInstallation(),InnoMigrationConsentStore.Grant(...), andLauncher.LaunchUriAsync(...)all still work with both accessibility commits applied.mainand the whole lifecycle was re-run end to end on the resulting head. See Full migration lifecycle after rebasing onto main (dd856634) below.Automated coverage. The
UI, functional, and accessibility testsjob passed on this head in 13m39s, with the Axe scan executing rather than being skipped as on the prior run.Full migration lifecycle after rebasing onto main (
dd856634)The branch was rebased onto
mainat373887bbto pick up the setup E2E flake fixes (5f300cf5,60246220,373887bb). All 12 commits replayed with no conflicts. Required validation on the rebased head:.\build.ps1dotnet test .\tests\OpenClaw.Shared.Tests\OpenClaw.Shared.Tests.csprojdotnet test .\tests\OpenClaw.Tray.Tests\OpenClaw.Tray.Tests.csprojBoth artifacts were then rebuilt from
dd856634and the migration was run end to end on a real machine, with no VM:scripts\build-inno-local.ps1 -Arch x64 -Fast -Version 2026.9.5.0, installed per user to%LOCALAPPDATA%\OpenClawTray, registeringDisplayVersion 2026.9.5.0.Name="OpenClawFoundation.OpenClaw"with the generated manifest reporting an emptyDevBuild, signed locally and installed asSignatureKind: Developer.Every startup admission logged across the run, in order:
DisplayVersionwas the branch-suffixed GitVersion string2026.9.5-user-natalie-aguinaldo-inno-to-store-shipping-main.1.InnoInstallationDetectorrejected it throughMigrationVersionPolicy.TryParseReleaseVersionand loggedThe Inno DisplayVersion is not a stable numeric release version.The Store app refused to take over and left the source untouched.2026.9.5.0version, the same build presented the consent screen, including theBefore you continuewarning that a successful validation stops the previous app from starting normally.Preparation and the removal handoff. Choosing
Migrateclosed the source, captured the inventory, and wrote the protected records. Nothing was uninstalled automatically.consent.dpapiintent.dpapicompleted.dpapiWith the source still installed,
scripts\Test-InnoMigration.ps1 -AppRoot "%LOCALAPPDATA%\OpenClawTray" -Architecture x64reportedValidated completed Store migration. Preserve generated state and gateway.and exited10, which is the documented retain contract.Finalization and continuity. After uninstalling only the previous app and choosing
Retry, the Store build verified removal, applied the startup preference, and finalized. The packaged app then started normally and reconnected to the existing gateway.OpenClaw.Tray.WinUI.exeunder%LOCALAPPDATA%\OpenClawTraygateways.json,settings.json,device-key-ed25519.json,models\Packaged (MSIX), version2026.9.5.0ConnectedThe three protected records are absent afterwards by design.
MigrationFinalizationRecordCleaner.ClearCompletedremoves the consent writer lock, consent, and intent, and deletes the completion receipt last, only after the Store startup preference has been applied, so the receipt stays available as the recovery anchor until everything else has succeeded.Test-InnoMigration.ps1reports2once the source is gone. That is a harness limit rather than product state: the script ships inside the Inno payload, so the-AppRootit needs no longer exists after the uninstall it is meant to follow. The meaningful reading is the10captured above while the source was still present.Found by this run, and fixed here. The prerelease refusal in step 1 was correct, but its guidance was not.
InnoInstallationDetectorcannot parse2026.9.5-alpha.19as a stable release version, so it refused the source with noRegisteredVersionto compare against the minimum. Startup policy only recognized a version problem when a version had parsed, so a prerelease fell through toUnsupportedInstallation, whose message explains only install location, Windows user, and architecture. None of those was the cause, and the obvious way out of that message is removing the source app, which forfeits the migration.The detector now carries the version refusal explicitly, and admission routes it to the update guidance that already exists and is already translated in every shipped locale. Admission itself is unchanged:
UpdateInnoandUnsupportedInstallationboth block startup, so only the message the user reads differs.Reproduced at current head with Inno
2026.9.5-alpha.19installed alongside the signed Store build2026.9.5.0, after clearingstore-migrationstate. The refusal reason logged is identical on both sides, and only the admission changed:The Inno DisplayVersion is not a stable numeric release version.->Store migration startup admission: UnsupportedInstallation (receipt: False, source payload: True)The Inno DisplayVersion is not a stable numeric release version.->Store migration startup admission: UpdateInno (receipt: False, source payload: True)Two limits worth stating.
MigrationMinimumSourceVersionis pinned to2026.9.5.0and no stable2026.9.5has shipped yet, so the update this message asks for only becomes followable once the coordinated Inno release lands. An absent or emptyDisplayVersion, which an interrupted uninstall can leave behind, also reaches this same guidance.Covered by 8 new cases across
InnoInstallationDetectorTestsandStoreMigrationStartupCoordinatorTests, including a negative case asserting a non-version refusal is not reported as a version refusal.Screen reader announcement on the current head (
dd856634)The earlier accessibility capture predates the rebase and recorded tree insertion only, which shows the bar exists but not that it speaks. This captures the announcement itself, on the current head, including a repeated result.
Taken out of process by a separate UIA client subscribed to
AutomationElement.NotificationEventand scoped to the app's process ID. The source was the current-head Inno build (2026.9.5.0, non-Dev Release, soPRODUCTION_MIGRATION), driven twice throughSettings->Install Store version and migrateand its consent dialog. The capture harness is throwaway and is not part of this PR.dd856634#1carriesInfoBarOpenedActivityIdand the full message text, from a bar that started collapsed#2then#3, 2 ms apart, are the close and re-open fromIsOpen = falsefollowed byIsOpen = trueDisplayStringis the severity plus the resolvedMigration2_InnoGrantedstring, not a control nameautomationId='StoreMigrationStatus'matchesx:NameinSettingsPage.xaml, so these events come from this PR's bar.CurrentThenMostRecentis what makes a screen reader interrupt and speak the newest result instead of queueing it behind the first.This is a UIA notification trace, which is what a screen reader consumes; it is not an audio recording of Narrator. No redaction was required, since the trace contains no endpoints, addresses, credentials, or user paths.
Outstanding acceptance and disclosures
Tag-time gates. The switch is checked in as
trueso the coordinated release carries migration, but the tag must not be published until these pass against that tag's real artifacts. If acceptance fails, setMigrationProductionEnabledtofalseand retag rather than shipping an unverified launch gate.v2026.9.5artifacts contain both PRs' safeguards and register the requiredDisplayVersionandDisplayName.What the existing proof does not establish.
StartupRefusedcapture is a mounted-window render, not a real refusal by Windows. That path is covered by unit tests only.f0017f4devidence, not current-head captures. Those Store-side screenshots predate the Settings card redesign, the Inno consent rewrite, and the icon fix. The lifecycle they depict is re-proven at the current head above.Known and accepted.
FinalizationFailureAfterTheSourceIsGone_IsVisibleButNotADeadEnd). Shipping as is; an escape-after-N-failures counter would require a record-schema change in the PowerShell 5.1-compatible codec.InspectionFailedand the Store app is allowed to start, where it previously failed closed. The failure stays on screen and the receipt is retained, so the user is told and finalization retries, but they can dismiss it and run against user state that drifted since the handoff was recorded. Accepted deliberately: a permanent two-app lockout is worse than a visible warning, and the check still fails closed before removal is proven (UncertainSourceRemoval_FailsClosedAndKeepsReceipt).Incident disclosure.
Rubber-duck review
Seven rounds of independent adversarial review, most of them dual-model, produced the hardening above. The first round refuted five of six self-reported claims and found the four fail-open defects; later rounds targeted the recovery path and found the lockout class. Every fix carries a regression test, and each was verified red-to-green against the actual pre-fix code. The final round cleared the branch, with both models independently agreeing there was no regression and no remaining lockout in the changed code.
Narrower follow-ups (the 11-file UI change and the three-file icon change) had single-model or direct source review only, with no actionable findings. No review claims release, Store, or gateway acceptance.
Orphaned Inno registration lockout, and its recovery on a real machine (current head
59e06bcd)Review round feedback asked what happens when the Inno uninstall key survives but the payload does not. On a real machine this was a hard lockout: the detector classified the leftover
HKCUregistration asUnsupportedInstallation, so the Store app refused to start, while the Inno executable it was deferring to no longer existed. The user was left with no working app and no in-product way out.ac5ff781adds anOrphanedRegistrationstatus for a canonical registration whose identity checks pass but which has neither an executable nor an uninstaller payload, and admits it to finalization. Machine-wide and conflicting registrations are still rejected before the orphan check runs, so nothing can be laundered through the new branch, and finalization re-verifies removal itself rather than trusting the earlier detection.Captured on a clean signed MSIX built from this branch, staging a real leftover registry key with no payload.
1. Orphaned registration is detected, admitted, and finalized
The app then continued into normal startup and reconnected to the gateway with its existing device token, so the recovery preserves pairing rather than resetting it.
2. The leftover key does not re-trigger migration on later launches
The registry key is deliberately not deleted, so the next launch still sees it. Because the receipt was cleared, admission is now
NotRequiredand no migration window appears.3. A real surviving installation still blocks, and the receipt is preserved
With the executable present, the orphan
WARNis correctly absent and the existing refusal path is unchanged. The receipt is not cleared, so no state is lost.Disclosure: finalization fail-fast crash found while capturing the proof above
Proving the orphan path exercised startup-time finalization on this machine for the first time, and it crashed the process every launch. This defect was pre-existing in this PR and is not caused by the orphan work; it reproduced on the ordinary
NotInstalledpath too. Earlier successful migrations had all been driven from the in-window Retry button, which runs after the app's XAML content is composed, so the startup path had never actually executed.Finalization can run inside
OnLaunched, before content is composed. CallingStartupTask.GetAsyncon the UI thread there fail-fasts throughMicrosoft.UI.Xamlas a stowed exception (0xc000027b, innerE_UNEXPECTED). That kills the process past every managedcatch,App.UnhandledException, andAppDomain.UnhandledException, and produces nocrash.logand no.NET Runtimeevent, which is why no existing diagnostic surfaced it.The consequence was worse than a single crash: the process died before the receipt was cleared, so the receipt survived and every subsequent launch failed identically. That is a permanent lockout with no in-product recovery.
59e06bcdmarshals the WinRT call off the UI thread so the failure stays catchable and reaches the existing finalization error handling. Verified on a signed MSIX: finalization completes, auto-start is applied, the receipt is cleared, and the app continues into normal startup.InnoMigrationContractTestsgains an assertion pinning the marshalling so it cannot silently regress.Separable behavior changes and residual gaps in
ac5ff781Called out explicitly so they get reviewed on their own merits rather than riding along with the orphan fix.
File.Existsis replaced with a tri-state probe, so an unreadable or access-denied path no longer reads as "removed". This is a deliberate tightening of an existing check and affects paths beyond the orphan case.OrphanedRegistrationis inserted mid-enum. Safe here because the enum is never serialized, never numerically cast, and every consumer uses explicit equality or membership checks, but it is a real source-compatibility consideration.Unsupported. If only the executable or only the uninstaller survives, for example because antivirus quarantinedunins000.exe, the user stays blocked. This is the deliberate conservative choice, since a remnant may indicate a live install, and it is pinned byARegistrationWithASurvivingRemnant_IsNotOrphaned. Accepting it as a documented residual rather than loosening an absence check.Unknownbranch is not covered by a test. It fails closed by construction.Security Impact
Compatibility and Migration
DisplayVersionandDisplayNamecheck above is required.Migration.Build.propscarries build-onlyMigrationProductionEnabled=true, pinnedMigrationMinimumSourceVersion=2026.9.5.0, andMigrationStoreProductId=9NFPR3BGDRR5. Explicit Debug preview properties remain separate. No runtime or environment-variable production-enablement setting. CI gains one staging step, one artifact, and one contract-test step.SetupWindow, provision a replacement gateway, or bypass the pre-services startup guard.Review Conversations