Skip to content

feat(migration): ship the Inno-to-Store migration - #1519

Merged
bkudiess merged 16 commits into
openclaw:mainfrom
natalie-aguinaldo:user/natalie-aguinaldo/inno-to-store-shipping-main
Sep 29, 2026
Merged

bkudiess merged 16 commits into
openclaw:mainfrom
natalie-aguinaldo:user/natalie-aguinaldo/inno-to-store-shipping-main

Conversation

@natalie-aguinaldo

@natalie-aguinaldo natalie-aguinaldo commented Sep 25, 2026 •

Copy link
Copy Markdown
Contributor

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 main and 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.5 Inno 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, on main at 5a595352. 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 ID 9NFPR3BGDRR5. 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 StoreMigrationStartupGuard because 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> from scripts\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. Its INSTALL.txt states 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.md now also requires confirming before the tag that the released Inno installer registers DisplayVersion 2026.9.5 and DisplayName OpenClaw Companion version 2026.9.5. A prerelease suffix or mismatched name is rejected by InnoInstallationDetector, which blocks startup for that user. The already-published v2026.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

  • Replace the preview's native dialog loop with a serial workflow and a localized WinUI surface.
  • Reuse the setup wizard's mascot, Mica backdrop, typography, themed cards, non-dismissible safety InfoBars, and neutral-left/accent-right footer with 100-pixel minimum action widths. Keep consent concise and show only the immediate action at each stage, without entering the gateway-installation pipeline.
  • Record protected explicit consent before opening the Store listing or requesting graceful shutdown. An inventory intent is not consent.
  • Use the existing current-user activation pipe for a bounded, fixed shutdown request, with manual close / Retry if the source does not exit.
  • Keep admission, preparation, completion, and finalization in PR 1's coordinators. Normal startup remains blocked until verified finalization.
  • Preserve all six locales, accessible control names, polite status announcements, and initial focus on Not now.

Ownership: StoreMigrationStartupGuard previously hosted the native dialog loop; StoreMigrationWorkflow now owns serial interaction, StoreMigrationOperations adapts existing migration coordinators, and StoreMigrationWindow applies the UI. The startup guard retains the compile-time gate and pre-services window lifetime. InnoMigrationHandoff owns 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.BlocksStartup blocks 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 by DecliningConsent_LeavesTheStoreAppInactive.

UnsupportedInstallation and UpdateInno also 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:

State Behavior Reason
InspectionFailed, RecoveryRequired Informs, then starts Both can occur when the previous app is already uninstalled. Refusing startup there leaves no working app and no in-app escape, which is the defect class the later review rounds were spent removing.

Supporting context:

  • This is strictly more restrictive than main. Shipped builds evaluate Disabled, whose AllowsNormalStartup is true, 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.
  • Two live production copies are already impossible regardless. Both Release binaries compile the same AppIdentity.MutexBaseName (OpenClawTray), which installer.iss also uses as AppMutex. 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 main as squash commit 5a595352, so its individual commits are not ancestors of main and a plain rebase replayed work already merged, conflicting on the first commit. The branch was instead rebuilt from main and PR 2's net change applied on top. The rebuilt tree was verified to be identical to the previously reviewed head d2c8e3fa except for main's own LF fix (#1518), so nothing was lost or silently altered in the transfer:

git diff --stat d2c8e3fa   # 4 files, only the #1518 LF-normalization changes

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, with MigrationMinimumSourceVersion=2026.9.5.0 pinned in Migration.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.5 or 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 f0017f4d and 5714c8aa are 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.

Defect Fix
File.Exists fails open. It returns false for a directory of the same name, a missing parent, or invalid characters, and never throws, so the catch { return true; } fallback was dead code and the doc comment promised the opposite of the behavior. MigrationStartupRecordReader.CompletionFileExists now uses File.GetAttributes, which throws distinguishably. Only FileNotFoundException and DirectoryNotFoundException return false; everything else returns true.
The uninstall reparse guard checked only the leaf directory. A junction at gateways redirects the whole subtree while gateways\<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.ps1 now walks every ancestor up to $DataDir before deleting, and stops at a UNC share root so folder-redirected AppData cannot block uninstall.
StoreMigrationStartupGuard inspected twice and discarded the first result, so a successful first pass followed by a failing second pass lost the receipt. The guard captures the admission and passes it into the workflow, which seeds _holdsCompletedHandoff from it.
The general catch in the workflow could not tell "no receipt" from "could not tell," and StoreMigrationStartupCoordinator threw InvalidOperationException when a completed migration had no receipt. IStoreMigrationOperations gained HoldsCompletionReceipt(), deliberately independent of Inspect(), with a non-throwing variant that fails safe to true. The coordinator returns a RecoveryRequired decision 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 it
under powershell.exe against a real junction. It was confirmed to fail against the pre-fix code and pass
after, 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:

  • The migration switch was not really a switch. Export-MigrationTestMsix.ps1 throws when the packaged
    assembly carries no MigrationMinimumSourceVersion, and that step ran unconditionally in build-msix, which
    release depends on. Setting MigrationProductionEnabled=false therefore made every release impossible
    rather 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.iss was only compiled for real in the tag-only release job, so a Pascal Script error would
    have been found at release time. CI now runs scripts/Test-InstallerScriptCompiles.ps1 -RequireCompiler on
    every 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 BinaryReader reports a malformed 7-bit string length as a plain System.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. StoreMigrationStartupCoordinator maps Unavailable to InspectionFailed and Invalid to RecoveryRequired, and the discard button renders only in Recovery. A malformed receipt was classified Unavailable, which blocked startup while offering no way out.

Defect Fix
The whole record set was judged by the first exception raised, so one unreadable file made the others undiagnosable. StoreMigrationRecoveryDiscard classifies each file on its own contents, and plans the receipt last so an interrupted discard still looks like a handoff needing recovery.
A malformed record length surfaced as IOException and was read as lock contention. The byte read is split from the decode, and IOException is allowlisted in the decode block only, matching the existing InnoMigrationConsentStore.ReadConsent pattern.
The same bug in MigrationStartupRecordReader.ReadFile made the escape hatch unreachable for exactly the corruption it exists to clear. Same split; a decode IOException is wrapped as InvalidDataException, so the record reaches Recovery rather than InspectionFailed.
The discard lock used 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.
Granting consent rewrote the directory owner and DACL unconditionally. That rewrite needs WRITE_DAC and WRITE_OWNER, which a roaming or administratively hardened profile can withhold, and the refusal is not an invalid record, so it surfaced as a generic inspection failure and blocked a migration whose state was already correct. CreateProtectedDirectory reuses the drift check MigrationOperationLock already 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 yield Unavailable with a receipt present.

Detection hardening in the final review round

Because UnsupportedInstallation and UpdateInno block only when a payload is present, a false negative in InnoInstallationDetector silently downgrades that block to "start anyway". Two paths did exactly that: Detect's outer InvalidDataException and BadImageFormatException handlers returned Unsupported() with the payload flag left at its default false, discarding evidence the probe had already gathered (reachable by appending bytes to app-identity.txt), and HasTrayExecutable caught too narrow a set of exceptions, so one unreadable registration short-circuited the check to InspectionFailed and masked a live sibling. The flag is now hoisted above the try and threaded through every return, and the probe's catch covers IOException, UnauthorizedAccessException, SecurityException, and InvalidDataException. 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 files
conflicted. 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.

File Resolution
StoreMigrationStartupGuard.cs Kept this branch's windowed flow. PR 1's durable autostart refusal was ported instead: the applier rethrows StoreMigrationAutoStartRefusedException, and the workflow maps StartupPreferenceRefused to a new StartupRefused stage before the allows-normal-startup fold. It reuses the existing Migration_StoreStartupRefused string and stays outside BlocksStartup, so the user is told that only Windows can re-enable the startup task without being trapped.
MigrationOperationLock.cs Took PR 1's inline DACL hardening and ScrubExistingEntries, which this branch's MigrationRecordStorage extraction had silently weakened.
InnoMigrationStartupGuard.cs Both branches independently fixed the same launch-bricking bug. Took PR 1's narrower version: only a contended lock blocks Inno startup, an unreadable one degrades to the receipt check. This branch's blanket "never block" had lost the in-progress interlock.
StoreMigrationFinalizationCoordinator.cs Kept PR 1's broadened AllowsNormalStartup, so a transient StartupPreferenceFailed no longer strands the user on a failure card. Only a durable refusal notifies.
MigrationRecordTests.cs Took PR 1's exit-code semantics, where a receipt that exists but does not validate is exit 2 and never exit 0, alongside this branch's new consent kind.
InnoMigrationContractTests.cs Unioned both suites. PR 1's StorePreviewChoices_UseNativeYesNoWithoutActionLegends was deliberately dropped because it asserts the MessageBoxW flow this branch deleted, and its Task.Run(...).ConfigureAwait(false) assertion no longer applies to an applier that plain-awaits.
docs/RELEASING.md Kept this branch's full shipping journey.

The merge surfaced four test failures that were genuine semantic collisions rather than noise, all from PR 1's
broadened AllowsNormalStartup and this branch's new StartupRefused stage. They were resolved by following
PR 1's semantics and adding behavioral coverage for the durable-refusal path.

Change Type

  • Bug fix
  • Feature
  • Refactor
  • Docs or instructions
  • Tests or validation
  • Security hardening
  • Chore or infrastructure

Scope

  • Tray or WinUI UX
  • Windows node capability
  • Local MCP or winnode
  • Gateway, connection, or pairing
  • Setup or onboarding
  • Permissions, privacy, or security
  • Tests, CI, or docs

Required 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_ROOT set to that worktree. UI and Tray checks used isolated OPENCLAW_TRAY_DATA_DIR directories. The first four rows were rerun at current head 2fcf1577, that is, after the branch was rebuilt on merged main.

Command Result Source tested
.\build.ps1 Passed 2fcf1577
dotnet test .\tests\OpenClaw.Shared.Tests\OpenClaw.Shared.Tests.csproj --no-restore 4,106 passed, 33 skipped, 0 failed 2fcf1577
dotnet test .\tests\OpenClaw.Tray.Tests\OpenClaw.Tray.Tests.csproj --no-restore 3,262 passed, 0 failed 2fcf1577
dotnet test .\tests\OpenClaw.Connection.Tests\OpenClaw.Connection.Tests.csproj --no-restore 1,247 passed, 1 skipped, 0 failed 2fcf1577
.\scripts\Test-InstallerScriptCompiles.ps1 Passed for x64, arm64, and DevBuild 5714c8aa
Focused Migration2LocalizationTests and InnoMigrationContractTests 62 passed, 0 failed 0651b419
Focused MigrationBuildConfigurationTests 23 passed, 0 failed 5714c8aa
.\scripts\validate-docs.ps1 Passed, 50 Markdown files checked 2fcf1577
Behavioral junction test CleanupScript_DoesNotDeleteThroughAJunction Passed, and confirmed red against the pre-fix leaf-only guard 5714c8aa
.\scripts\test-ci-workflow-contract.ps1 Passed 5714c8aa
.\scripts\test-migration-test-msix.ps1 Passed 5714c8aa
Focused Migration2LocalizationTests, InnoMigrationContractTests, and StoreMigrationWorkflowTests 89 passed, 0 failed f0017f4d
Mounted migration UI command below 16 passed, 0 skipped, 0 failed d2c8e3fa
.\build.ps1 Passed Current head 59e06bcd
dotnet test .\tests\OpenClaw.Shared.Tests\OpenClaw.Shared.Tests.csproj --no-restore 4,145 passed, 34 skipped, 0 failed Current head 59e06bcd
dotnet test .\tests\OpenClaw.Tray.Tests\OpenClaw.Tray.Tests.csproj --no-restore 3,327 passed, 0 failed Current head 59e06bcd
Focused Connection FullyQualifiedName~Migration 404 passed, 1 skipped, 0 failed Current head 59e06bcd
dotnet test .\tests\OpenClaw.Tray.UITests\OpenClaw.Tray.UITests.csproj `
  -c Debug -r win-x64 --no-restore `
  --filter 'FullyQualifiedName~StoreMigrationWindowProofTests'

The 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 was ReactorChatLayoutProofTests.ReasoningPicker_PointerReleaseCommitsButCaptureLossDoesNot.

Adversarial dual-model review of the full ac5ff781 + 59e06bcd diff (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-x64 of the tray project produced an assembly carrying the pinned policy:

MigrationStoreProductId=9NFPR3BGDRR5
MigrationMinimumSourceVersion=2026.9.5.0

scripts\Build-StoreMsix.ps1 -Architecture x64 then produced a real unsigned Store package (Identity: OpenClawFoundation.OpenClaw 2026.9.5.0 x64), and scripts\Export-MigrationTestMsix.ps1 was run against it end to end with the real Windows SDK SignTool.exe:

Check Result
Exported files OpenClaw-MigrationTest-x64.msix (157,079,494 bytes), OpenClaw-MigrationTest.cer, msix-metadata.json, INSTALL.txt
Package identity OpenClawFoundation.OpenClaw, CN=4BA40A7A-B719-4C40-BF91-84AF4F1136FC, 2026.9.5.0
Signer subject CN=4BA40A7A-B719-4C40-BF91-84AF4F1136FC (matches the manifest publisher)
Metadata signing=migration-test-only, migrationEnabled=True, sideBySideWithStore=False
Disposable private key Absent from Cert:\CurrentUser\My after the run

Get-AuthenticodeSignature reports UnknownError on the signing machine because that disposable certificate is deliberately not trusted there; trusting it is step 3 of the generated INSTALL.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 f0017f4d run.

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 f0017f4d manual VM run, because the Store listing is not live, so a Store-distributed install cannot be exercised yet.

  1. Inno app, Settings page. A migration-capable Inno build shows a recommended card above the General section. Current head, installer C4D2F8E4BD98B91E4C692A21DA2078301E43AE4333EA242E26D8C0A27D1A0A17, 972/972 payload hashes matched, measured at 985.6 client DIPs.

Current-head VM proof: Settings promotion card with badge beside the title and the action inline on the right

In context with the settings rows below it, showing matching row height and text alignment:

Current-head VM proof: promotion card above the General settings rows

  1. Confirmation dialog. Selecting the action opens a confirmation before anything is recorded. Opening the dialog grants nothing; consent is written only on the primary path.

Current-head VM proof: confirmation dialog asking Move to the Store version?

  1. 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.GrantAndLaunchAsync grants before launching, so a user who abandons the Store has still granted. Choosing the neutral action records nothing.

  2. Install from Microsoft Store. The user installs the Store package themselves. Not exercised from a live listing; the f0017f4d run used a locally test-signed MSIX.

  3. 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.

Manual VM proof: close guidance with separate safety warning and Retry

  1. Migration and guided removal. After migration is recorded, the Inno app no longer starts normally, and the Store app guides the user through uninstalling it. Nothing is removed automatically.

Manual VM proof: remove the previous app, preserve setup, and return to Retry

  1. Finalization and normal startup. Settings, gateway, identities, approvals, and models stay in place.

Manual VM proof: normal Store interface with the expected synthetic gateway connection error

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.VisualStateGroups block was declared on a Grid nested inside the card, and WinUI only evaluates state triggers declared on the page root, so the AdaptiveTrigger never 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: HubWindow declares MinWidth="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 no AdaptiveTrigger or VisualStateManager group exists in the file, and names the window minimum as the reason.

Deliberate reversal of the disclosure contract added in f0017f4d

Migration2LocalizationTests.InnoDialog_DisclosesSafetyBoundariesInEveryLocale was added in f0017f4d to 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_AwaitingRemoval already 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 by InnoMigrationConsentStore; it is no longer recited in the dialog. docs/RELEASING.md acceptance disclosures for the Store preview window cover different keys and are unchanged.

Current-head native icon diagnostics

StoreMigrationWindow now calls the existing WinUIEx SetIcon("Assets\\openclaw.ico") helper, matching other application windows. Its custom XAML title-bar image alone did not populate native HWND icons.

The new Consent_AssignsNativeIconsForTaskbarAndWindowSwitcher test mounts the production window with in-memory migration operations and queries WM_GETICON on its live HWND. It failed before the production fix because an icon handle was zero. Current-head output:

Native window icon type 0: nonzero handle.
Native window icon type 1: nonzero handle.
Test Run Successful.
Total tests: 13
     Passed: 13

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

Current-head VM proof: OpenClaw icon visible in the migration window taskbar thumbnail

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)

StartupRefused arrived with the PR 1 merge and is the newest visible state. It is reached when migration
finishes 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.

Current-head mounted proof: startup-refusal notice with Close as the only action

Captured at 5714c8aa from the mounted production window with in-memory operations, the same harness used for
the 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 d3a1e96d had 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.

  • Provider: local Hyper-V guest OC-AUTO-A91896, user MigrationTest. No cloud lease ID or hosted run URL.
  • Build: msix-metadata.json records sourceCommit 2fcf1577..., sourceTreeDirty false, migrationEnabled true. The package is OpenClawFoundation.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).
  • Sources: scenario A used a locally built Inno 2026.9.5, because no stable 2026.9.5 exists publicly and the pinned floor is 2026.9.5.0. Scenario C used the real public v2026.9.4 release, which is genuinely below that floor.
  • The tray log was deleted before each run, so every admission line shown was written by that run.
A: supported source declined C: source below the floor
Previous app 2026.9.5 2026.9.4
User action Not now Close
Admission decision ConsentRequired UpdateInno
Logged source payload False (see caveat) True
App started anyway No No

Scenario A: supported source, user declines

Scenario A preflight: supported 2026.9.5 source staged, no migration records, no tray log

Scenario A: consent window stating the Store version stays closed while the previous app is installed

Scenario A after Not now: no window and no tray icon

Scenario A evidence: no OpenClaw process, admission logged as ConsentRequired

Scenario C: source below the supported floor

Scenario C preflight: 2026.9.4 staged below the 2026.9.5 floor, clean records, no tray log

Scenario C: update-required notice stating this app stays inactive while the previous app is installed

Scenario C after Close: no window and no tray icon

Scenario C evidence: no OpenClaw process, admission logged as UpdateInno with source payload True

Caveat on the logged source payload field. Scenario A logs False although the payload was present. The flag is threaded only through the paths that consume it, UnsupportedInstallation and UpdateInno, which is why scenario C reports True; ConsentRequired blocks 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 no source payload term at all, so its presence is what dates these captures, as does scenario C's notice text, added in 2fcf1577.

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 f0017f4d

  • Provider: existing local IXPTools / Hyper-V, VM OpenClaw-Migration-Auto-x64-a91896be, VM ID d0cd56dc-6a26-4493-8bc6-89f6cf2de790. No cloud lease ID or hosted run URL.
  • Source: 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 floor 2026.9.5.0 was not overridden; the checked-in production default was still disabled at that time and is enabled at the current head.
  • Artifacts: actual installed Inno fixture 2026.9.5.0 and locally test-signed MSIX 2026.9.21.0. These are not official Store/release artifacts.
  • Fixture: synthetic gateway.example.test configuration 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.
  • Actions: developer launched the previous app, opened the Store migration app, confirmed Migrate, exercised close/Retry, opened Installed apps, manually uninstalled only the previous app, then clicked Retry in the existing migration window.
  • Pre-uninstall check: the installed Test-InnoMigration.ps1 returned exit code 10 for the canonical current-user source, confirming migration-preservation eligibility. The source app was no longer running.
  • After uninstall: source registration, executable, and uninstaller were absent; no uninstall/cleanup worker remained. All eight tracked preservation entries, including settings, gateway, generated identity, and all three protected records, retained their hashes.
  • Finalization: guest logs on 2026-09-22 showed Store migration finalized: at 21:18:39.499, followed by Application 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.
  • Original fixture safety: the seven originals were backed up and hash-verified separately. Safe closeout passed: all seven originals were restored and SHA-256 verified after terminal process checks. No app, worker, installer, cleanup process, or proof task remained. The final synthetic state and original backups were retained in the VM. No credentials or private identity files were uploaded.

Store-side consent at f0017f4d

Steps 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.

Manual VM proof: concise consent with three sections, warning, Not now and Migrate

All are developer-captured from the real VM and the exact f0017f4d tested 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 5714c8aa

A second manual VM run exercised the whole lifecycle at 5714c8aa instead of f0017f4d, using the checked-in production default rather than a proof-time CLI override.

  • Provider: local Hyper-V guest OC-AUTO-A91896, user MigrationTest. No cloud lease ID or hosted run URL.
  • Source: 5714c8aa88a62fec8e339ba1215fda432eee6b58, clean tree (sourceTreeDirty: false in msix-metadata.json).
  • Artifacts: locally built Inno OpenClawCompanion-Setup-x64.exe (ProductVersion 2026.9.5, payload FileVersion 2026.9.5.0) and locally test-signed MSIX OpenClawFoundation.OpenClaw 2026.9.5.0. Neither is an official Store or release artifact.
  • Admission pre-check: the registration satisfied every InnoInstallationDetector condition (canonical per-user path, DisplayName matching DisplayVersion, publisher, exact quoted unins000.exe uninstall string, complete payload, release identity, matching executable FileVersion), with no machine-wide registration present.
Step Observed Guest log
Baseline Test-InnoMigration.ps1 exit 0 n/a
Admission real Inno install detected, consent shown startup admission: ConsentRequired (receipt: False)
Fail closed on corrupt identity preparation refused, no receipt, source untouched preparation failed: Device identity is malformed or unreadable.
Receipt absent while packaged checker exit 11 plus its preserve warning n/a
Prepare and complete validated receipt written, checker exit 10 preparation completed / completion recorded
Real uninstall registration, executable, and Test-InnoMigration.ps1 all absent n/a
Finalization receipt consumed, consent and intent records removed FinalizationRequired (receipt: True) / finalized
Restart normal startup, no migration window, gateway present n/a

Preservation result. Comparing SHA-256 of every tracked file across the real uninstall, gateways.json, gateways\<id>\device-key-ed25519.json, and settings.json were unchanged. Only consent.dpapi, consent.lock, and intent.dpapi disappeared, which is finalization consuming its own records.

Live current-head admission against a real Inno installation

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.

Live fail-closed refusal after a corrupt device identity

Live removal stage after a validated receipt was written

Normal startup after finalization, with the migrated gateway intact

What this run does not establish.

  • The MSIX is locally test-signed, so the Store-distributed identity and handoff gate is unchanged.
  • The gateway fixture is synthetic (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.
  • The preserved gateways.json was 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.
  • The guest had already run migrations on 2026-09-22, so %APPDATA%\OpenClawTray was not pristine: a stale zero-byte prepare.lock and the generated identity directory predate this run. The source install was fresh and the baseline checker returned 0.

Full migration lifecycle and gateway continuity at current head (2fcf1577)

The same developer workstation and the same pre-paired gateway as the 5714c8aa run above, re-run end to
end 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.

  • Provider: developer workstation CPC-nagui-AVM65, Windows 10.0.26691.0, x64. Not a VM, and no cloud lease ID or hosted run URL.
  • Artifacts: locally built Inno 2026.9.5 as the source, locally test-signed MSIX 2026.9.5.0 as the target. Neither is a Store or release artifact.
  • Gateway: ws://127.0.0.1:18790, added and paired via device token before migration started.
  • Current head: every admission line carries the source payload: term, which only exists from 2fcf1577.
Step Observed Tray log
Baseline Inno 2026.9.5 running and Connected; 9 tracked files hashed n/a
Admission real Inno install detected, gated consent shown 16:08:07 ConsentRequired (receipt: False, source payload: False)
Close the previous app handoff blocked on the running Inno app, with an explicit "don't uninstall yet" warning 16:08:07 WARN Migration shutdown request unavailable: Access to the path is denied.
Prepare and complete validated receipt written before any uninstall; consent, intent, and completed records all present 16:09:01 preparation completed / completion recorded
Real uninstall Inno reported successful removal; registration and tray executable both absent afterwards n/a
Finalization receipt consumed on the next start 16:10:07 FinalizationRequired (receipt: True, source payload: False) / finalized
Migrated app Install type: Packaged (MSIX), version 2026.9.5.0, gateway still Connected 16: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 root device-key-ed25519.json, and all three
gateways\<id>\device-key-ed25519.json identity files. consent.dpapi and consent.lock disappeared,
which is finalization consuming its own records. The WSL distro OpenClawGateway-Dev stayed registered,
because the uninstaller's optional gateway-cleanup prompt was declined as the removal card instructs.

gateways.json is the one tracked file whose hash changed. Its only write is stamped 16:10:08, the same
second the migrated app started and reconnected, and its post-migration contents keep the same gateway
id, url, identityDirName, shared token, and requiresV2Signature. That is consistent with a
lastConnected refresh on reconnect rather than drift, but the snapshot stores hashes only, so a
field-level before/after diff is not available for this run.

Before migration: the Inno app connected to the pre-paired gateway

Gated consent before migration starts

Handoff blocked while the previous app is still running

Removal stage after validation passed and completion was recorded

Real Inno uninstall reporting successful removal

Previously paired gateway still connected after migration

Migrated app reporting Packaged (MSIX) while connected

What this run does not establish.

  • The MSIX is locally test-signed and the source installer is locally built, so Store distribution and its handoff gate remain unproven. That stays a tag-time gate.
  • x64 on one Windows build only. ARM64 is still unproven.
  • The gateway reports up 3m, so this is continuity of the gateway record and its device credential, not an unbroken socket across the handoff.
  • The automatic shutdown request failed with an access-denied warning, so closing the previous app was manual. The flow degraded correctly by showing the Close the previous app card, but the automated path is unproven here.
  • ConsentRequired logs source payload: False even 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.CaptureIdentities
rejects 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 unreachable NoActiveGateway and
CredentialUnavailable states, the CredentialUnavailable stage and its localized strings, and the
ICredentialResolver dependency, so the coordinator structurally cannot resolve credentials. Coverage was
added 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.

dotnet build src\OpenClaw.Tray.WinUI\OpenClaw.Tray.WinUI.csproj -c Debug -r win-x64 `
  -p:InnoMigrationPreview=true -p:MigrationPreviewStoreProductId=9NFPR3BGDRR5

This is the preview switch that Migration.Build.props restricts to Debug unpackaged non-Dev builds. No OPENCLAW_TRAY_*_DIR override was set, because MigrationEnvironment.HasPathOverride would disable the entry point.

  1. Entry point and InfoBar render correctly.

Settings page showing the migration card and the status InfoBar

The Move to the Microsoft Store version card is visible, and the StoreMigrationStatus InfoBar is rendered inline at full width with Error severity and the Migration2_InnoFailed string. Before this change the bar was opened while still Collapsed, so it occupied no layout space and never appeared.

  1. The InfoBar is realized in the UIA accessibility tree. Captured with Accessibility Insights for Windows v1.1.2924.01 Event Recorder, scoped to the OpenClaw Companion window.

Accessibility Insights event recording

12:11:40.656  AutomationFocusChanged  button 'Install Store version and migrate'
12:11:41.963  StructureChanged        status bar ''
12:11:42.340  StructureChanged        text 'Error icon'
12:11:42.492  StructureChanged        text 'OpenClaw could not save migration consent or open the Store listin...'
12:11:42.641  StructureChanged        button 'Close'

The StructureChangeType_ChildAdded records 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 what FrameworkElementAutomationPeer.CreatePeerForElement provides, and it is the precondition for InfoBar raising its status announcement, since InfoBar announces through an existing automation peer only and FrameworkElementAutomationPeer.FromElement never 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_Notification event itself. Accessibility Insights Event Recorder did not surface a Notification row even with Listen to All Events enabled, and Notification is not offered in its expected-event list for the Window control type, so the tool appears not to register for UIA_Notification_EventId. Reported here as observed rather than inferred.

  1. Why the error path. The preview binary runs from bin\Debug\..., so InnoMigrationHandoff.InspectOwnInstallation() correctly rejects it, because Environment.ProcessPath does not match <InstallDir>\OpenClaw.Tray.WinUI.exe. That throw happens before InnoMigrationConsentStore.Grant(...) and before Launcher.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 same ShowStoreMigrationStatus code path as the success case, which differs only in InfoBarSeverity and the resource key.

  2. Migration behavior is unchanged. Both accessibility commits touch UI only:

    Commit Files
    b76c2599 Pages/SettingsPage.xaml, Pages/SettingsPage.xaml.cs, StoreMigrationWindowProofTests.cs
    2735a636 Pages/SettingsPage.xaml, Pages/SettingsPage.xaml.cs

    No change to InnoMigrationHandoff, InnoMigrationConsentStore, MigrationRecordCodec, MigrationEnvironment, InnoMigrationStartupGuard, or StoreMigrationStartupGuard. ShowStoreMigrationStatus runs only on the string returned by await InnoMigrationHandoff.GrantAndLaunchAsync(), which means it runs strictly after consent persistence and Store launch have already completed, and it only sets Message, Severity, IsOpen, and Visibility.

  3. 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.

    .\scripts\build-inno-local.ps1 -Arch x64 -Version 2026.9.5.0 -Fast

    Release plus non-Dev plus win-x64, so Migration.Build.props defines PRODUCTION_MIGRATION. Verified in the produced assembly:

    Attribute Value
    MigrationStoreProductId 9NFPR3BGDRR5 present
    MigrationMinimumSourceVersion 2026.9.5.0 present
    MigrationPreviewStoreProductId absent, so this is the production path and not a preview build
    Installed ProductVersion 2026.9.5.0+2735a63624c1c2c1b900c5b951c0627b9cede797

    The build version equals MigrationMinimumSourceVersion, so it qualifies as a supported source. Installed per user to %LOCALAPPDATA%\OpenClawTray, which is the path MigrationEnvironment.CreateBinding() requires.

Success InfoBar and the Store listing opening

Clicking the card and confirming the consent dialog produced the Success severity Migration2_InnoGranted InfoBar and opened the Microsoft Store listing. The consent record was written at the same moment:

%APPDATA%\OpenClawTray\store-migration\
  consent.dpapi    678 bytes   12:27:35
  consent.lock       0 bytes   12:27:35

This exercises the same ShowStoreMigrationStatus method as the error case above, on the success branch, and confirms that InnoInstallationDetector detection, the supported source check in InspectOwnInstallation(), InnoMigrationConsentStore.Grant(...), and Launcher.LaunchUriAsync(...) all still work with both accessibility commits applied.

  1. Store-side finalization is proven directly, not by traceability. This item previously argued that the Store-side path did not need re-capturing because it was unchanged. That argument has been replaced with a direct capture: the branch was rebased onto main and 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 tests job 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 main at 373887bb to pick up the setup E2E flake fixes (5f300cf5, 60246220, 373887bb). All 12 commits replayed with no conflicts. Required validation on the rebased head:

Command Result
.\build.ps1 all five projects succeeded
dotnet test .\tests\OpenClaw.Shared.Tests\OpenClaw.Shared.Tests.csproj 4145 passed, 0 failed, 34 skipped
dotnet test .\tests\OpenClaw.Tray.Tests\OpenClaw.Tray.Tests.csproj 3327 passed, 0 failed

Both artifacts were then rebuilt from dd856634 and the migration was run end to end on a real machine, with no VM:

  • Source. scripts\build-inno-local.ps1 -Arch x64 -Fast -Version 2026.9.5.0, installed per user to %LOCALAPPDATA%\OpenClawTray, registering DisplayVersion 2026.9.5.0.
  • Target. Production-identity MSIX, Name="OpenClawFoundation.OpenClaw" with the generated manifest reporting an empty DevBuild, signed locally and installed as SignatureKind: Developer.

Every startup admission logged across the run, in order:

13:04:05  UnsupportedInstallation  (receipt: False, source payload: True)
13:09:45  ConsentRequired          (receipt: False, source payload: False)
13:10:44  FinalizationRequired     (receipt: True,  source payload: False)
13:10:44  Store migration finalized: <migration id>
  1. Unsupported source is refused before consent. The first launch was made against an Inno build whose DisplayVersion was the branch-suffixed GitVersion string 2026.9.5-user-natalie-aguinaldo-inno-to-store-shipping-main.1. InnoInstallationDetector rejected it through MigrationVersionPolicy.TryParseReleaseVersion and logged The Inno DisplayVersion is not a stable numeric release version. The Store app refused to take over and left the source untouched.

Unsupported source refusal at current head

  1. Consent on a supported source. After reinstalling the source with a stable 2026.9.5.0 version, the same build presented the consent screen, including the Before you continue warning that a successful validation stops the previous app from starting normally.

Consent screen at current head

  1. Preparation and the removal handoff. Choosing Migrate closed the source, captured the inventory, and wrote the protected records. Nothing was uninstalled automatically.

    Record Size Written
    consent.dpapi 678 bytes 13:09:59
    intent.dpapi 6550 bytes 13:10:06
    completed.dpapi 694 bytes 13:10:06

    With the source still installed, scripts\Test-InnoMigration.ps1 -AppRoot "%LOCALAPPDATA%\OpenClawTray" -Architecture x64 reported Validated completed Store migration. Preserve generated state and gateway. and exited 10, which is the documented retain contract.

Removal handoff at current head

  1. 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.

    Check Result
    Inno uninstall registration 0 entries remaining
    OpenClaw.Tray.WinUI.exe under %LOCALAPPDATA%\OpenClawTray absent
    gateways.json, settings.json, device-key-ed25519.json, models\ all preserved
    Reported install type Packaged (MSIX), version 2026.9.5.0
    Gateway status Connected

Migrated Store build reporting Packaged (MSIX) and Connected

The three protected records are absent afterwards by design. MigrationFinalizationRecordCleaner.ClearCompleted removes 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.ps1 reports 2 once the source is gone. That is a harness limit rather than product state: the script ships inside the Inno payload, so the -AppRoot it needs no longer exists after the uninstall it is meant to follow. The meaningful reading is the 10 captured 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. InnoInstallationDetector cannot parse 2026.9.5-alpha.19 as a stable release version, so it refused the source 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, 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: UpdateInno and UnsupportedInstallation both block startup, so only the message the user reads differs.

Reproduced at current head with Inno 2026.9.5-alpha.19 installed alongside the signed Store build 2026.9.5.0, after clearing store-migration state. The refusal reason logged is identical on both sides, and only the admission changed:

Logged
Before The Inno DisplayVersion is not a stable numeric release version. -> Store migration startup admission: UnsupportedInstallation (receipt: False, source payload: True)
After The Inno DisplayVersion is not a stable numeric release version. -> Store migration startup admission: UpdateInno (receipt: False, source payload: True)

Prerelease source routed to update guidance at current head

Two limits worth stating. MigrationMinimumSourceVersion is pinned to 2026.9.5.0 and no stable 2026.9.5 has shipped yet, so the update this message asks for only becomes followable once the coordinated Inno release lands. An absent or empty DisplayVersion, which an interrupted uninstall can leave behind, also reaches this same guidance.

Covered by 8 new cases across InnoInstallationDetectorTests and StoreMigrationStartupCoordinatorTests, 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.NotificationEvent and scoped to the app's process ID. The source was the current-head Inno build (2026.9.5.0, non-Dev Release, so PRODUCTION_MIGRATION), driven twice through Settings -> Install Store version and migrate and its consent dialog. The capture harness is throwaway and is not part of this PR.

OpenClaw Store migration status InfoBar - UIA notification trace
target pid     : 22664
os build       : 10.0.26699.0
event filter   : UIA NotificationEvent, scoped to the target process
------------------------------------------------------------------------------

[13:48:03.056] UIA Notification #1
    source           : controlType=ControlType.StatusBar automationId='StoreMigrationStatus'
    NotificationKind : Other
    Processing       : CurrentThenMostRecent
    ActivityId       : InfoBarOpenedActivityId
    DisplayString    : Success icon  Migration consent was saved. Install and open the Store
                       version to continue. Keep this app installed until migration completion
                       is confirmed.

[13:48:10.457] UIA Notification #2
    source           : controlType=ControlType.StatusBar automationId='StoreMigrationStatus'
    ActivityId       : InfoBarClosedActivityId
    DisplayString    : InfoBar dismissed

[13:48:10.459] UIA Notification #3
    source           : controlType=ControlType.StatusBar automationId='StoreMigrationStatus'
    NotificationKind : Other
    Processing       : CurrentThenMostRecent
    ActivityId       : InfoBarOpenedActivityId
    DisplayString    : Success icon  Migration consent was saved. Install and open the Store
                       version to continue. Keep this app installed until migration completion
                       is confirmed.
------------------------------------------------------------------------------
notifications  : 3
Claim in dd856634 Evidence
The collapsed bar is realized, so the open-time announcement is not dropped #1 carries InfoBarOpenedActivityId and the full message text, from a bar that started collapsed
A repeated result is announced again rather than silently swapping text #2 then #3, 2 ms apart, are the close and re-open from IsOpen = false followed by IsOpen = true
The announced text is what the user sees DisplayString is the severity plus the resolved Migration2_InnoGranted string, not a control name

automationId='StoreMigrationStatus' matches x:Name in SettingsPage.xaml, so these events come from this PR's bar. CurrentThenMostRecent is 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 true so the coordinated release carries migration, but the tag must not be published until these pass against that tag's real artifacts. If acceptance fails, set MigrationProductionEnabled to false and retag rather than shipping an unverified launch gate.

  • Store-distributed package identity, signature, and handoff. No Store-distributed install has been exercised; every Store-side capture uses a locally test-signed package.
  • Native ARM64 acceptance. Earlier ARM64 cross-builds are not native-runtime proof.
  • Live gateway acceptance: local WSL gateway continuity and management including its Node.js, remote-gateway migration without local WSL, no changes to Windows-installed Node.js, and real settings, credential, identity, approval, and Local AI preservation. Partly closed: a real paired local gateway is now proven to survive migration and reconnect from the migrated MSIX (developer-machine run). Node.js handling, remote-gateway migration, and Local AI preservation remain unproven.
  • Signed x64 and ARM64 coverage of both consent entry paths, shutdown and manual retry, interruption and recovery, uninstall preservation, finalization, startup preference, and protocol activation.
  • Confirmation that the actual v2026.9.5 artifacts contain both PRs' safeguards and register the required DisplayVersion and DisplayName.

What the existing proof does not establish.

  • The manual run proves recovery through Retry, not uninterrupted automatic graceful-shutdown progression.
  • Startup and protocol continuation, Narrator, OS-wide scaling and high-DPI, and translated-locale visual acceptance.
  • The StartupRefused capture is a mounted-window render, not a real refusal by Windows. That path is covered by unit tests only.
  • Steps 4 through 7 of the entry-point walkthrough are f0017f4d evidence, 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.
  • Source-text contract assertions confirm markup, not rendering. Both the card layout regression and the reparse-guard bug passed a fully green suite while live; each is now covered by a test verified red-to-green.
  • Proof-pool declarations request work. They are not evidence that the pools ran.

Known and accepted.

  • Finalization has no completion escape hatch. If finalization can never succeed on a given machine, the receipt keeps blocking the previous app's startup indefinitely and the records are never cleaned. The user is not locked out of the product: once removal of the previous app is verified, no finalization outcome may refuse launch, so the Store app starts and the failure is shown rather than trapping the user (FinalizationFailureAfterTheSourceIsGone_IsVisibleButNotADeadEnd). Shipping as is; an escape-after-N-failures counter would require a record-schema change in the PowerShell 5.1-compatible codec.
  • Releasing the block after removal can start against drifted state. That no-lockout rule has a cost. If the inventory fingerprint no longer matches the completion receipt after the previous app is removed, finalization returns InspectionFailed and 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.

  • Automated lifecycle attempts failed on harness process tracking, log serialization, or timeout handling. None is counted as full lifecycle acceptance.
  • One earlier harness restored fixture data while an uninstaller was still pending. When subsequently approved, that uninstall entered ordinary gateway cleanup. Its verified process tree was contained, the exact pre-proof checkpoint was restored, and all seven original hashes were verified before the successful manual run. Evidence was preserved. The replacement harness adds fail-closed process and receipt lifetime barriers, but the accepted lifecycle proof here is manual.

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 HKCU registration as UnsupportedInstallation, 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.

ac5ff781 adds an OrphanedRegistration status 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

[WARN] The canonical Inno registration has neither an executable nor an uninstaller payload.
[INFO] Store migration startup admission: FinalizationRequired (receipt: True, source payload: False).
[INFO] Packaged auto-start enabled
[INFO] Store migration finalized: [REDACTED_ID].

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.

proof-orphan-recovery-app-running.png

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 NotRequired and no migration window appears.

[WARN] The canonical Inno registration has neither an executable nor an uninstaller payload.
[INFO] Store migration startup admission: NotRequired (receipt: False, source payload: False).

3. A real surviving installation still blocks, and the receipt is preserved

With the executable present, the orphan WARN is correctly absent and the existing refusal path is unchanged. The receipt is not cleared, so no state is lost.

[INFO] Store migration startup admission: UnsupportedInstallation (receipt: True, source payload: True).

proof-surviving-exe-blocks.png

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 NotInstalled path 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. Calling StartupTask.GetAsync on the UI thread there fail-fasts through Microsoft.UI.Xaml as a stowed exception (0xc000027b, inner E_UNEXPECTED). That kills the process past every managed catch, App.UnhandledException, and AppDomain.UnhandledException, and produces no crash.log and no .NET Runtime event, 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.

59e06bcd marshals 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. InnoMigrationContractTests gains an assertion pinning the marshalling so it cannot silently regress.

Separable behavior changes and residual gaps in ac5ff781

Called out explicitly so they get reviewed on their own merits rather than riding along with the orphan fix.

  • Absence probes now fail closed. File.Exists is 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.
  • OrphanedRegistration is 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.
  • A single surviving remnant is still Unsupported. If only the executable or only the uninstaller survives, for example because antivirus quarantined unins000.exe, the user stays blocked. This is the deliberate conservative choice, since a remnant may indicate a live install, and it is pinned by ARegistrationWithASurvivingRemnant_IsNotOrphaned. Accepting it as a documented residual rather than loosening an absence check.
  • The verifier-level Unknown branch is not covered by a test. It fails closed by construction.

Security Impact

  • New permissions or capabilities? No new node/MCP capability or elevation requirement.
  • Secrets or tokens handling changed? Yes: a separate same-user DPAPI consent record binds source version, identity, paths, architecture, and package. It contains no exported credential payload, expires, and never authorizes destructive uninstall. Invalid records are preserved.
  • New or changed network calls? A configured migration action opens the pinned Windows Store listing via its URI handler. This is not an arbitrary endpoint or a new credential-bearing request.
  • Command or tool execution surface changed? A fixed private current-user pipe message requests canonical Inno shutdown after consent/source validation. It is not a public deep link or arbitrary command. Timeouts fall back to manual close; no force-kill.
  • Data access scope changed? No expansion beyond the supported current-user migration roots. Reads do not create missing consent records or lock files.

Compatibility and Migration

  • Backward compatible? Release non-Dev builds now enable migration. Unsupported source installations fail closed in enabled migration builds, which stops launch rather than degrading, so the pre-tag DisplayVersion and DisplayName check above is required.
  • Config or environment changes? Central Migration.Build.props carries build-only MigrationProductionEnabled=true, pinned MigrationMinimumSourceVersion=2026.9.5.0, and MigrationStoreProductId=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.
  • Migration needed? For explicitly enabled proof builds until release acceptance. Confirm migration, allow Inno to exit (or close it manually and Retry), wait for successful validation, open Installed apps, uninstall only the previous Inno app, then Retry to finalize.
  • The redesigned window does not construct SetupWindow, provision a replacement gateway, or bypass the pre-services startup guard.

Review Conversations

  • I replied to or resolved every bot review conversation addressed by this PR.
  • I left unresolved only conversations that still need maintainer judgment.

@clawsweeper

clawsweeper Bot commented Sep 25, 2026 •

Copy link
Copy Markdown

🦞👀
ClawSweeper picked this up.

Pull request received. I will update this pull request when review starts.

ClawSweeper review complete

ClawSweeper finished reviewing this revision. The review result is being finalized.

View the workflow run.

@clawsweeper

clawsweeper Bot commented Sep 25, 2026 •

Copy link
Copy Markdown

Codex review: blocked before merge. Reviewed September 29, 2026, 4:49 PM ET / 20:49 UTC (Revision 16).

ClawSweeper review

What this changes

Adds 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
Reviewed head: 59e06bcd5ffadf73ee016722e54070fb2dacfa39
Owner decision: Required. See Decision needed.

Review scores

Measure Result What it means
Overall readiness 🦪 silver shellfish (2/6) Real Windows migration evidence is strong, but automatic alpha exposure and missing old-record upgrade proof prevent merge readiness.
Proof confidence 🦞 diamond lobster (5/6) Sufficient (terminal): The snapshot-matched PR body reports a current-head signed-MSIX run through orphan-registration admission, startup finalization, receipt cleanup, and gateway reconnection, plus a prior-head full Inno-to-MSIX lifecycle run; Store distribution and ARM64 remain release gates. The stored-record format is additive, but direct upgrade proof using foundation-written intent and completion records is still missing.
Patch quality 🦪 silver shellfish (2/6) 1 actionable review finding remain.

Verification

Check Result Evidence
Real behavior Verified Sufficient (terminal): The snapshot-matched PR body reports a current-head signed-MSIX run through orphan-registration admission, startup finalization, receipt cleanup, and gateway reconnection, plus a prior-head full Inno-to-MSIX lifecycle run; Store distribution and ARM64 remain release gates. The stored-record format is additive, but direct upgrade proof using foundation-written intent and completion records is still missing.
Evidence reviewed 9 items Introduced production default: The branch enables production migration in non-Dev Release builds by default.
Automatic alpha publication: The existing scheduled workflow tags current main and dispatches the release pipeline; that pipeline builds Release Inno installers with the alpha version.
Alpha migration dead end: The new Settings entry is shown for production-enabled Inno builds, but its action requires a detected installation; the detector rejects a prerelease DisplayVersion.
Findings 1 actionable finding [P1] Gate production migration in automatically published alpha builds
Security None None.

How this fits together

The 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]
Loading

Decision needed

Question Recommendation
Should production migration remain enabled in automatically published alpha artifacts before the exact Store, ARM64, and gateway acceptance gates pass? Gate alpha activation: Keep scheduled alpha installers outside production migration while retaining an explicit developer test package and enabling the coordinated release after acceptance.

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

  • Gate production migration in automatically published alpha builds (P1) - This new true default enables the migration UI in every non-Dev Release build. The existing daily-alpha workflow automatically tags main and publishes Release Inno installers with a prerelease DisplayVersion; the new Settings action is visible, but its installation check rejects that version before consent can be granted. Gate alpha activation or publication and cover the scheduled release path.
  • Resolve merge risk (P1) - The enabled Release default can enter the scheduled alpha-release pipeline before the proposed tag-time acceptance. Alpha Inno builds then advertise a migration action that rejects their own prerelease registration.
  • Resolve merge risk (P1) - An intent or completion record produced by the merged foundation has not been directly read and finalized by this branch in an upgrade test.
  • Resolve merge risk (P1) - The release owner has not recorded acceptance of the exact Store-distributed, ARM64, and remaining gateway and data-preservation cases before production publication.
  • Complete next step (P2) - Fix alpha-release exposure, prove foundation-record upgrade compatibility, and obtain the release owner's activation decision before merge.
  • Improve patch quality - Gate scheduled alpha artifacts so their visible migration action cannot reject the installer they ship.
  • Improve patch quality - Read and finalize intent and completion records written by the merged foundation.
  • Improve patch quality - Record release-owner acceptance against the exact production artifacts.
  • Resolve maintainer decision - Resolve the maintainer decision shown above before merge.

Findings

  • [P1] Gate production migration in automatically published alpha builds — src/OpenClaw.Tray.WinUI/Migration.Build.props:10
Agent review details

Security

None.

Review metrics

Metric Value Why it matters
Changed surface 63 files The handoff spans the installer, stored records, WinUI, build policy, CI, and tests.
Source and test delta production +3044/-424, tests +4401/-61 lines The production growth supports a full migration flow and warrants focused upgrade and release review.

Root-cause cluster

Relationship: fixed_by_candidate
Canonical: #1374
Summary: This PR is the candidate completion of the open Inno-to-Store migration request; the merged foundation PR covers its earlier phase.

Members:

Proposal only: this assessment does not dispatch repair, suppress jobs, mutate sibling items, close, or merge anything.

Merge-risk options

Maintainer options:

  1. Gate alpha builds and prove upgrades (recommended)
    Exclude scheduled alpha releases from production migration and test foundation-written records against this branch before merging.
  2. Accept the release exposure
    A release owner could explicitly accept early alpha behavior and own the resulting user recovery and publication gates.

Technical review

Best 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:

  • [P1] Gate production migration in automatically published alpha builds — src/OpenClaw.Tray.WinUI/Migration.Build.props:10
    This new true default enables the migration UI in every non-Dev Release build. The existing daily-alpha workflow automatically tags main and publishes Release Inno installers with a prerelease DisplayVersion; the new Settings action is visible, but its installation check rejects that version before consent can be granted. Gate alpha activation or publication and cover the scheduled release path.
    Confidence: 0.94

Overall correctness: patch is incorrect
Overall confidence: 0.91

AGENTS.md: found and applied where relevant.

Codex review notes: model internal, reasoning medium; reviewed against 24263d3b06c4.

Labels

Label changes:

  • add rating: 🦪 silver shellfish: Overall readiness is 🦪 silver shellfish; proof is 🦞 diamond lobster and patch quality is 🦪 silver shellfish. Replaced prior rating: 🦐 gold shrimp.
  • remove rating: 🦐 gold shrimp: Current PR rating is rating: 🦪 silver shellfish, so this older rating label is no longer current.

Label justifications:

  • P1: Scheduled alpha publication can expose a user-facing migration action that rejects the published alpha installer.
  • merge-risk: 🚨 compatibility: The production default changes installer-to-Store upgrade behavior, and old protected-record compatibility lacks a direct upgrade test.
  • merge-risk: 🚨 availability: Migration admission can keep the Store app inactive while a previous installation or completion receipt remains.
  • rating: 🦪 silver shellfish: Overall readiness is 🦪 silver shellfish; proof is 🦞 diamond lobster and patch quality is 🦪 silver shellfish. Replaced prior rating: 🦐 gold shrimp.
  • status: 👀 ready for maintainer look: ClawSweeper has no concrete contributor-facing blocker left for this PR. Sufficient (terminal): The snapshot-matched PR body reports a current-head signed-MSIX run through orphan-registration admission, startup finalization, receipt cleanup, and gateway reconnection, plus a prior-head full Inno-to-MSIX lifecycle run; Store distribution and ARM64 remain release gates. The stored-record format is additive, but direct upgrade proof using foundation-written intent and completion records is still missing.
  • proof: sufficient: Contributor real behavior proof is sufficient. The snapshot-matched PR body reports a current-head signed-MSIX run through orphan-registration admission, startup finalization, receipt cleanup, and gateway reconnection, plus a prior-head full Inno-to-MSIX lifecycle run; Store distribution and ARM64 remain release gates. The stored-record format is additive, but direct upgrade proof using foundation-written intent and completion records is still missing.

Evidence

What I checked:

Likely related people:

  • bkudiess: Suggested for follow-up; no historical authorship or introduction is verified. (role: unverified routing candidate; confidence: low)
  • Natalie Aguinaldo: Raw commit 5a59535 adds src/OpenClaw.Connection/Migration/MigrationRecordCodec.cs:80 relative to its recorded parents. This identifies author metadata, not feature responsibility or a PR merger. (role: source-line author; confidence: high; commits: 5a59535216ee; files: src/OpenClaw.Connection/Migration/MigrationRecordCodec.cs)
  • Dallin Romney: Suggested for follow-up; no historical authorship or introduction is verified. (role: unverified routing candidate; confidence: low)

Rating scale

Score Internal tier Crab rank Meaning
6/6 S 🦀 challenger crab Exceptional readiness
5/6 A 🦞 diamond lobster Very strong readiness
4/6 B 🐚 platinum hermit Good normal PR; ordinary maintainer review
3/6 C 🦐 gold shrimp Useful, but confidence is limited
2/6 D 🦪 silver shellfish Proof or implementation needs work
1/6 F 🧂 unranked krab Not merge-ready
N/A NA 🌊 off-meta tidepool Rating does not apply

Overall follows the weaker of proof and patch quality.
Shiny media proof means a screenshot, video, or linked artifact directly shows the changed behavior. Runtime, network, CSP, and security claims still need visible diagnostics.

Workflow

  • ClawSweeper keeps one durable marker-backed review comment per issue or PR.
  • Re-runs edit this comment so the latest verdict, findings, and automation markers stay together instead of adding duplicate bot comments.
  • A fresh review can be triggered by eligible @clawsweeper re-review comments, exact-item GitHub events, scheduled/background review runs, or manual workflow dispatch.
  • PR/issue authors and users with repository write access can comment @clawsweeper re-review or @clawsweeper re-run on an open PR or issue to request a fresh review only.
  • Maintainers can also comment @clawsweeper review to request a fresh review only.
  • Fresh-review commands do not start repair, autofix, rebase, CI repair, or automerge.
  • Maintainer-only repair and merge flows require explicit commands such as @clawsweeper autofix, @clawsweeper automerge, @clawsweeper fix ci, or @clawsweeper address review.
  • Maintainers can comment @clawsweeper explain to ask for more context, or @clawsweeper stop to stop active automation.

History

Review history (15 earlier review cycles; latest 8 shown)
  • reviewed 2026-09-28T18:07:45.601Z sha 2735a63 :: needs real behavior proof before merge. :: none
  • reviewed 2026-09-28T19:45:11.904Z sha 2735a63 :: needs real behavior proof before merge. :: none
  • reviewed 2026-09-28T19:59:08.473Z sha dd85663 :: needs real behavior proof before merge. :: none
  • reviewed 2026-09-28T20:22:15.747Z sha dd85663 :: needs real behavior proof before merge. :: none
  • reviewed 2026-09-28T20:58:04.958Z sha dd85663 :: blocked before merge. :: [P2] [P2] Explain unsupported source versions in the refusal
  • reviewed 2026-09-28T21:41:05.096Z sha a8a608f :: blocked before merge. :: [P3] Update the prerelease guidance in the release guide
  • reviewed 2026-09-28T21:52:23.262Z sha 1937c76 :: blocked before merge. :: none
  • reviewed 2026-09-28T23:22:08.868Z sha dde0a9b :: blocked before merge. :: none

@natalie-aguinaldo
natalie-aguinaldo marked this pull request as ready for review September 25, 2026 20:17
@clawsweeper clawsweeper Bot added P1 Urgent regression or broken agent/channel workflow affecting real users now. merge-risk: 🚨 availability 🚨 Merging this PR could cause crashes, hangs, restart loops, stalls, or process outages. merge-risk: 🚨 compatibility 🚨 Merging this PR could break existing users, config, migrations, defaults, or upgrades. rating: 🦐 gold shrimp Decent PR readiness signal, but merge confidence is limited. status: 📣 needs proof The PR needs real behavior proof before ClawSweeper can clear the contributor ask. proof: sufficient Contributor real behavior proof is sufficient. rating: 🐚 platinum hermit Good normal PR readiness with ordinary maintainer review expected. status: 👀 ready for maintainer look ClawSweeper has no concrete contributor-facing blocker left for this PR. and removed status: 📣 needs proof The PR needs real behavior proof before ClawSweeper can clear the contributor ask. rating: 🦐 gold shrimp Decent PR readiness signal, but merge confidence is limited. rating: 🐚 platinum hermit Good normal PR readiness with ordinary maintainer review expected. status: 👀 ready for maintainer look ClawSweeper has no concrete contributor-facing blocker left for this PR. proof: sufficient Contributor real behavior proof is sufficient. labels Sep 25, 2026
@natalie-aguinaldo
natalie-aguinaldo force-pushed the user/natalie-aguinaldo/inno-to-store-shipping-main branch from 2735a63 to dd85663 Compare September 28, 2026 19:52
@clawsweeper clawsweeper Bot added proof: sufficient Contributor real behavior proof is sufficient. status: ⏳ waiting on author ClawSweeper has contributor-facing work open and is waiting for author action. status: 👀 ready for maintainer look ClawSweeper has no concrete contributor-facing blocker left for this PR. and removed status: 📣 needs proof The PR needs real behavior proof before ClawSweeper can clear the contributor ask. status: ⏳ waiting on author ClawSweeper has contributor-facing work open and is waiting for author action. labels Sep 28, 2026
@natalie-aguinaldo

Copy link
Copy Markdown
Contributor Author

Note on the red X from the Connection Tests job: it is a known flake in MigrationRecordTests.CleanupScript_CompletedReceiptPreservesFilesWithoutCallingWsl, not a regression from this PR.

The case deliberately holds prepare.lock with FileShare.None and expects Uninstall-LocalGateway.ps1 to exit 2. In that run the script's own log contained the contention line ~300 ms in, Starting local gateway cleanup was absent (so cleanup correctly did not continue), but captured stdout was empty and the test was killed at its 30 second deadline. A five-iteration local reproduction of the identical scenario exits 2 in ~0.6 s every time, so the script is correct and the runner stalled writing the small log and result files.

This branch cannot reach that code path. Its only change to Uninstall-LocalGateway.ps1 is at lines 420-484, inside a function called after Starting local gateway cleanup at line 851, and the failing case exits at line 808. The lock-acquisition region is byte-identical to main, and the test and its helper are untouched.

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.

@bkudiess bkudiess added the status: 🚢 actively landing A maintainer or agent is actively driving this item through implementation, validation, or merge. label Sep 28, 2026
natalie-aguinaldo and others added 3 commits September 28, 2026 16:06
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
natalie-aguinaldo and others added 11 commits September 28, 2026 16:06
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
@bkudiess

Copy link
Copy Markdown
Collaborator

Hanselman dual-model review

Claude 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 consensus

None.

One-model findings - LOW consensus

Issue Opus Codex Fix confidence
Unavailable migration records skip source detection. Codex predicted concurrent Store and Inno clients, but the shared production single-instance mutex prevents that. Dismissing the error can still continue Store startup if Inno has stopped; this is the existing dismiss-and-launch behavior, not a confirmed concurrency bug. - HIGH (impact disputed) N/A; the proposed behavior change was 75% and is not recommended without a product decision
Finalization performs synchronous registry, process, and inventory work before its first await, briefly blocking the migration UI. No data-safety impact was identified. LOW - 95%
The discard diagnostic count includes consent.lock even when it did not exist. Cosmetic log discrepancy only. LOW - 99%

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 OpenClaw.Connection.Tests: 387 passed; migration-filtered OpenClaw.Tray.Tests: 191 passed. Codex reported no additional high-confidence findings.

@natalie-aguinaldo
natalie-aguinaldo force-pushed the user/natalie-aguinaldo/inno-to-store-shipping-main branch from 1937c76 to dde0a9b Compare September 28, 2026 23:16
@natalie-aguinaldo

Copy link
Copy Markdown
Contributor Author

Note on the red X from the Connection Tests job: it is a known flake in MigrationRecordTests.CleanupScript_CompletedReceiptPreservesFilesWithoutCallingWsl, not a regression from this PR.

The case deliberately holds prepare.lock with FileShare.None and expects Uninstall-LocalGateway.ps1 to exit 2. In that run the script's own log contained the contention line ~300 ms in, Starting local gateway cleanup was absent (so cleanup correctly did not continue), but captured stdout was empty and the test was killed at its 30 second deadline. A five-iteration local reproduction of the identical scenario exits 2 in ~0.6 s every time, so the script is correct and the runner stalled writing the small log and result files.

This branch cannot reach that code path. Its only change to Uninstall-LocalGateway.ps1 is at lines 420-484, inside a function called after Starting local gateway cleanup at line 851, and the failing case exits at line 808. The lock-acquisition region is byte-identical to main, and the test and its helper are untouched.

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.

rebased to get the test fix

@bkudiess

bkudiess commented Sep 29, 2026 •

Copy link
Copy Markdown
Collaborator

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 Unsupported and block Store startup. The old executable is gone, so the user has neither app available. I reproduced the admission result at dde0a9bf with real old-format receipts and simulated orphan-registration evidence.

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 File.Exists: access denied must not mean “removed.”

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.

natalie-aguinaldo and others added 2 commits September 29, 2026 13:39
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
@natalie-aguinaldo

natalie-aguinaldo commented Sep 29, 2026 •

Copy link
Copy Markdown
Contributor Author

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 Unsupported and block Store startup. The old executable is gone, so the user has neither app available. I reproduced the admission result at dde0a9bf with real old-format receipts and simulated orphan-registration evidence.

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 File.Exists: access denied must not mean “removed.”

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.

@bkudiess Addressed in the latest 2 commits!

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

Labels

merge-risk: 🚨 availability 🚨 Merging this PR could cause crashes, hangs, restart loops, stalls, or process outages. merge-risk: 🚨 compatibility 🚨 Merging this PR could break existing users, config, migrations, defaults, or upgrades. P1 Urgent regression or broken agent/channel workflow affecting real users now. proof: sufficient Contributor real behavior proof is sufficient. rating: 🦐 gold shrimp Decent PR readiness signal, but merge confidence is limited. status: 🚢 actively landing A maintainer or agent is actively driving this item through implementation, validation, or merge. status: 👀 ready for maintainer look ClawSweeper has no concrete contributor-facing blocker left for this PR.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Migrate from the Inno (.exe) app to the Store MSIX

2 participants