Summary / Requirement
When the Windows Tray app provisions an OpenClawGateway in the local WSL environment, that gateway's version must be able to follow the Tray app through compatibility upgrades. Today the gateway version is frozen at install time and never moves again, even after the Tray app itself auto-updates. This can leave the Tray app running against a gateway version it was never built or tested against.
We want the local WSL gateway to stay in lock-step with the Tray app: the Tray app requires one specific gateway version (the one it ships with), detects when the installed gateway does not match, and drives an in-place upgrade to bring them into alignment.
Context / Background
Current behavior:
- During first-run onboarding, the Setup Engine installs a pinned gateway/CLI version into a dedicated WSL distro (
OpenClawGateway). The version comes from a compile-time constant (the "LKG" — Last Known Good — version) baked into the Tray build.
- After that install, the Tray app never tracks, compares, or upgrades the WSL gateway again. The gateway is effectively frozen at whatever version was installed.
- The Tray app does have its own update mechanism (self-update of the Windows executable via GitHub Releases, checked at startup), but that path only updates the Windows EXE — it never touches the WSL gateway.
The problem this creates:
Because the Tray EXE updates itself automatically and quickly, while the gateway stays frozen, the two drift apart over time. A newly updated Tray app can end up talking to an older gateway. Nothing today notices or corrects this.
Why lock-step instead of broad compatibility:
Supporting "one Tray version compatible with many gateway versions" turns compatibility testing into an M×N matrix — every Tray release would have to be validated against every historical gateway version. That cost is prohibitive. Instead, we constrain each Tray version to require exactly one gateway version (the one it ships with), collapsing the matrix to M×1: each Tray build is only ever responsible for the single gateway version it targets.
Design philosophy alignment:
The repo already establishes a "Tray-driven, pinned version, human-gated" model — CI only warns (non-blocking) when the pinned version drifts from upstream latest, and an automation workflow opens a draft PR so a human reviews and merges every version bump. Version advancement is always a deliberate, gated decision — never an automatic chase of the newest upstream release. This proposal extends that same philosophy to the runtime: the Tray app drives the gateway to its own required version, not to whatever is newest on npm.
Goals
- The local WSL gateway version "travels with" the Tray EXE: Tray self-update ships a new required version → Tray detects the installed gateway no longer matches → prompts the user → user confirms → Tray upgrades the gateway in place to the required version.
- Detect and surface version mismatch clearly, without silently tolerating drift.
- Keep this entirely separate from the existing Tray EXE self-update — two independent mechanisms that happen to share a UI location.
Non-Goals
- Not chasing npm "latest" — the upgrade target is always the version the Tray app ships with, preserving the human-gated review model.
- Not reusing the gateway's own self-update path (the upstream
update.available / update.run mechanism), which would chase latest and is unverified against our locked-down WSL appliance.
- Not re-running the full onboarding pipeline or touching the WSL distro lifecycle — no distro unregister/recreate. This is an in-place CLI upgrade + service restart only.
- Not managing remote gateways — this applies only to the local, Tray-provisioned WSL gateway.
- Not changing the Tray EXE self-update mechanism.
Design Decisions
-
Constraint strength — strict version lock with a strong prompt, but no hard block.
When the gateway version does not match the Tray's required version, the Tray shows a prominent, persistent prompt guiding the user to a one-click upgrade — but it does not refuse the connection or disable features. This safety valve exists because the upgrade path (a network-dependent install script run inside WSL) can fail and currently has no rollback; a hard block would turn "upgrade failed" into "completely unusable / bricked gateway." This still captures the full M×1 testing benefit without betting availability on every upgrade succeeding.
-
Match criterion — exact equality.
The gateway version must be strictly equal to the Tray's required version. Any difference means "needs alignment," regardless of whether the gateway is older or newer (e.g. a user who manually installed a newer gateway is also prompted to align back to the Tray's required version).
-
Upgrade trigger — user-confirmed.
Mismatch is detected and surfaced, but the actual upgrade only runs after the user explicitly confirms. No silent background upgrade.
-
Detection point — after the WSL distro is ready (not after a WebSocket connection).
The check runs once the local WSL distro is up and only for the local, Tray-provisioned WSL gateway. It reads the installed version directly from WSL by running openclaw --version — it does not wait for a successful gateway connection or handshake. The natural hook is the existing startup keepalive flow, which already ensures the distro is running and already gates on "is this the local WSL gateway." See "Why read the version from WSL" below for the rationale, including why gating on a WebSocket connection would be self-defeating.
Why read the version from WSL instead of the handshake
The gateway reports a version over its connection handshake, and the Tray already receives it. However, for a lock-step install check, reading the version directly from WSL (openclaw --version) is both more correct and more robust:
- It measures the right thing. The handshake version is what the running gateway process reports (what's currently executing in memory). The lock-step model cares about what's installed on disk — the openclaw CLI version that the install script put there. These can diverge: right after an upgrade replaces the on-disk CLI but before the gateway service restarts, the handshake still reports the old (still-running) version.
openclaw --version reads the on-disk CLI — the same artifact install-cli.sh --version <X> produces — so we compare with the same yardstick we install with. The loop is closed end to end.
- It's decoupled from the protocol layer — so the check must not be gated on a connection. Obtaining the handshake version requires a successful protocol negotiation and connection. If a gateway is old enough that protocol negotiation fails, the handshake version is unavailable — precisely when we most need to prompt for alignment. Running
openclaw --version only requires the WSL distro to be running (not the gateway service, not a WebSocket): as long as WSL can run a command, we get the version. Crucially, this means the trigger must not be a successful gateway connection either — gating the check on "connected" would re-introduce the exact coupling we are trying to remove, and would silently skip the check for a gateway too old to connect (leaving the user stuck with no upgrade prompt). Reading from WSL after the distro is ready keeps "what version is installed" fully independent of "can we connect."
- The prerequisite is already satisfied at startup. The Tray already runs a keepalive step at startup that ensures the local WSL distro is up and running (and already decides whether the active gateway is the local WSL one). The version check attaches naturally to that flow — no new precondition, no waiting on the gateway service or the connection.
- The primitives already exist.
openclaw --version is already used during onboarding to verify the install; the Tray runtime already runs WSL commands (for keepalive and gateway start/stop) via the same command runner; and the required PATH prefix for invoking openclaw is already defined. Reuse is straightforward.
The handshake-reported version is still useful for display (it tells the user what's currently running) and remains shown in Settings, but it is no longer the basis for the lock-step decision.
How It Works (Mechanism)
The feature is a self-contained loop. It reuses the existing startup keepalive flow and the existing WSL command/control primitives — it does not invent a new install pathway, and it does not depend on the gateway connection/handshake.
-
Version basis.
The Tray app already carries a compile-time "required gateway version" constant (the LKG). When the Tray EXE self-updates, a new build ships with a (possibly) new required version. That constant is the single source of truth for what the gateway should be.
-
Detect after the distro is ready, by reading WSL directly.
Hooked into the existing startup keepalive flow: once the local WSL distro is confirmed running, and only when the active gateway is the local, Tray-provisioned WSL gateway, the Tray runs openclaw --version inside the distro (with the standard PATH prefix) and parses the installed CLI version from stdout. It compares that against its required version using an exact-equality check.
- Equal → nothing to do.
- Not equal (older or newer) → mark "needs alignment."
- Unparseable / command fails → conservatively do nothing (log only), to avoid false alarms.
This does not wait for a WebSocket connection or handshake — the version comes straight from disk, so the check works even when the gateway is too old to connect.
-
Surface the mismatch.
A mismatch is exposed through app state so the UI can react. In the Settings "Gateway info" area (which already displays the gateway version), a strong, persistent prompt appears — "gateway version does not match the required version — upgrade to align" — with an upgrade action. A one-time toast notification can also alert the user and deep-link into Settings. (There is already an unused "Gateway update required" string resource that fits this exactly.)
-
Upgrade on confirmation.
When the user confirms, a lightweight upgrade coordinator:
- resolves the local WSL distro name,
- ensures the WSL VM / distro is ready,
- runs the existing install-script command (the same
install-cli.sh --version <required> mechanism used at onboarding, with its built-in HTTPS/safety constraints) pinned to the Tray's required version,
- restarts the gateway service via the existing WSL gateway control primitive,
- re-reads
openclaw --version from WSL to confirm the on-disk version is now aligned (more direct and faster than waiting for a fresh handshake, and unaffected by the protocol layer),
- lets the existing reconnect-with-backoff logic bring the connection back,
- reports success/failure via a toast.
Crucially this is a lightweight path — it drives the install command and the service restart directly. It does not run the full multi-step setup pipeline and does not clean/recreate the distro, so an upgrade can never be destructive to existing gateway state.
-
Safety valve.
If the upgrade fails (network, script error, WSL not ready), the old gateway remains usable and connected. The mismatch prompt persists until alignment succeeds, so the user can retry — but they are never left with a broken, unusable gateway.
-
User control.
A settings toggle (default on) governs whether the Tray performs this mismatch check and prompt. The upgrade itself always requires an explicit user click regardless of the toggle.
Relationship to Tray EXE self-update
These are two independent mechanisms with adjacent UI:
|
Tray EXE self-update (existing) |
Gateway alignment (this issue) |
| Target |
Windows EXE |
WSL gateway/CLI |
| Version read from |
GitHub Releases (latest tag) |
openclaw --version inside WSL (on-disk CLI) |
| Upgrade source |
GitHub Releases |
install script, pinned to Tray's required version |
| Trigger |
Startup, synchronous |
Startup keepalive (after distro ready), async |
| Effect |
Replace EXE, restart Windows app |
In-place CLI upgrade + gateway restart, Tray auto-reconnects |
The net effect: Tray self-update advances the required gateway version → next startup detects (by reading WSL) that the installed gateway no longer matches → user is prompted to align. The two stay coupled through the required-version constant, while remaining separate execution paths.
Open Questions / Follow-ups
- Upgrade atomicity & rollback. The current install path has no rollback. Hardening this (atomic upgrade, restore-previous-on-failure, offline handling) is a prerequisite if we ever want to tighten the constraint from "strong prompt" to "hard block."
Summary / Requirement
When the Windows Tray app provisions an OpenClawGateway in the local WSL environment, that gateway's version must be able to follow the Tray app through compatibility upgrades. Today the gateway version is frozen at install time and never moves again, even after the Tray app itself auto-updates. This can leave the Tray app running against a gateway version it was never built or tested against.
We want the local WSL gateway to stay in lock-step with the Tray app: the Tray app requires one specific gateway version (the one it ships with), detects when the installed gateway does not match, and drives an in-place upgrade to bring them into alignment.
Context / Background
Current behavior:
OpenClawGateway). The version comes from a compile-time constant (the "LKG" — Last Known Good — version) baked into the Tray build.The problem this creates:
Because the Tray EXE updates itself automatically and quickly, while the gateway stays frozen, the two drift apart over time. A newly updated Tray app can end up talking to an older gateway. Nothing today notices or corrects this.
Why lock-step instead of broad compatibility:
Supporting "one Tray version compatible with many gateway versions" turns compatibility testing into an M×N matrix — every Tray release would have to be validated against every historical gateway version. That cost is prohibitive. Instead, we constrain each Tray version to require exactly one gateway version (the one it ships with), collapsing the matrix to M×1: each Tray build is only ever responsible for the single gateway version it targets.
Design philosophy alignment:
The repo already establishes a "Tray-driven, pinned version, human-gated" model — CI only warns (non-blocking) when the pinned version drifts from upstream latest, and an automation workflow opens a draft PR so a human reviews and merges every version bump. Version advancement is always a deliberate, gated decision — never an automatic chase of the newest upstream release. This proposal extends that same philosophy to the runtime: the Tray app drives the gateway to its own required version, not to whatever is newest on npm.
Goals
Non-Goals
update.available/update.runmechanism), which would chase latest and is unverified against our locked-down WSL appliance.Design Decisions
Constraint strength — strict version lock with a strong prompt, but no hard block.
When the gateway version does not match the Tray's required version, the Tray shows a prominent, persistent prompt guiding the user to a one-click upgrade — but it does not refuse the connection or disable features. This safety valve exists because the upgrade path (a network-dependent install script run inside WSL) can fail and currently has no rollback; a hard block would turn "upgrade failed" into "completely unusable / bricked gateway." This still captures the full M×1 testing benefit without betting availability on every upgrade succeeding.
Match criterion — exact equality.
The gateway version must be strictly equal to the Tray's required version. Any difference means "needs alignment," regardless of whether the gateway is older or newer (e.g. a user who manually installed a newer gateway is also prompted to align back to the Tray's required version).
Upgrade trigger — user-confirmed.
Mismatch is detected and surfaced, but the actual upgrade only runs after the user explicitly confirms. No silent background upgrade.
Detection point — after the WSL distro is ready (not after a WebSocket connection).
The check runs once the local WSL distro is up and only for the local, Tray-provisioned WSL gateway. It reads the installed version directly from WSL by running
openclaw --version— it does not wait for a successful gateway connection or handshake. The natural hook is the existing startup keepalive flow, which already ensures the distro is running and already gates on "is this the local WSL gateway." See "Why read the version from WSL" below for the rationale, including why gating on a WebSocket connection would be self-defeating.Why read the version from WSL instead of the handshake
The gateway reports a version over its connection handshake, and the Tray already receives it. However, for a lock-step install check, reading the version directly from WSL (
openclaw --version) is both more correct and more robust:openclaw --versionreads the on-disk CLI — the same artifactinstall-cli.sh --version <X>produces — so we compare with the same yardstick we install with. The loop is closed end to end.openclaw --versiononly requires the WSL distro to be running (not the gateway service, not a WebSocket): as long as WSL can run a command, we get the version. Crucially, this means the trigger must not be a successful gateway connection either — gating the check on "connected" would re-introduce the exact coupling we are trying to remove, and would silently skip the check for a gateway too old to connect (leaving the user stuck with no upgrade prompt). Reading from WSL after the distro is ready keeps "what version is installed" fully independent of "can we connect."openclaw --versionis already used during onboarding to verify the install; the Tray runtime already runs WSL commands (for keepalive and gateway start/stop) via the same command runner; and the required PATH prefix for invokingopenclawis already defined. Reuse is straightforward.The handshake-reported version is still useful for display (it tells the user what's currently running) and remains shown in Settings, but it is no longer the basis for the lock-step decision.
How It Works (Mechanism)
The feature is a self-contained loop. It reuses the existing startup keepalive flow and the existing WSL command/control primitives — it does not invent a new install pathway, and it does not depend on the gateway connection/handshake.
Version basis.
The Tray app already carries a compile-time "required gateway version" constant (the LKG). When the Tray EXE self-updates, a new build ships with a (possibly) new required version. That constant is the single source of truth for what the gateway should be.
Detect after the distro is ready, by reading WSL directly.
Hooked into the existing startup keepalive flow: once the local WSL distro is confirmed running, and only when the active gateway is the local, Tray-provisioned WSL gateway, the Tray runs
openclaw --versioninside the distro (with the standard PATH prefix) and parses the installed CLI version from stdout. It compares that against its required version using an exact-equality check.This does not wait for a WebSocket connection or handshake — the version comes straight from disk, so the check works even when the gateway is too old to connect.
Surface the mismatch.
A mismatch is exposed through app state so the UI can react. In the Settings "Gateway info" area (which already displays the gateway version), a strong, persistent prompt appears — "gateway version does not match the required version — upgrade to align" — with an upgrade action. A one-time toast notification can also alert the user and deep-link into Settings. (There is already an unused "Gateway update required" string resource that fits this exactly.)
Upgrade on confirmation.
When the user confirms, a lightweight upgrade coordinator:
install-cli.sh --version <required>mechanism used at onboarding, with its built-in HTTPS/safety constraints) pinned to the Tray's required version,openclaw --versionfrom WSL to confirm the on-disk version is now aligned (more direct and faster than waiting for a fresh handshake, and unaffected by the protocol layer),Crucially this is a lightweight path — it drives the install command and the service restart directly. It does not run the full multi-step setup pipeline and does not clean/recreate the distro, so an upgrade can never be destructive to existing gateway state.
Safety valve.
If the upgrade fails (network, script error, WSL not ready), the old gateway remains usable and connected. The mismatch prompt persists until alignment succeeds, so the user can retry — but they are never left with a broken, unusable gateway.
User control.
A settings toggle (default on) governs whether the Tray performs this mismatch check and prompt. The upgrade itself always requires an explicit user click regardless of the toggle.
Relationship to Tray EXE self-update
These are two independent mechanisms with adjacent UI:
openclaw --versioninside WSL (on-disk CLI)The net effect: Tray self-update advances the required gateway version → next startup detects (by reading WSL) that the installed gateway no longer matches → user is prompted to align. The two stay coupled through the required-version constant, while remaining separate execution paths.
Open Questions / Follow-ups