Summary
In systemd-supervised installs (openclaw-gateway.service running under systemctl --user), clicking Update now in the Control UI silently fails on every attempt. The gateway restarts on the same version, the UI surfaces Update skipped: managed-service-handoff-started, and npm install -g openclaw@latest is never actually invoked.
The handoff helper script (/tmp/openclaw-update-run-handoff-XXXXXX/handoff.cjs) gets created but never executes — its handoff.log never appears on disk, while handoff.cjs is still sitting in /tmp.
Reproduced cleanly upgrading 2026.5.16-beta.7 → 2026.5.18 on Linux/systemd.
Repro
Environment:
- OpenClaw 2026.5.16-beta.7 (npm-global install)
- Node 22.22.2, Linux 6.8.0-111-generic
- Gateway managed by
systemctl --user (openclaw-gateway.service)
- Cloudflare Tunnel in front of Control UI; no special auth involvement
Steps:
- Latest version on
latest tag is newer than current binary (2026.5.18 > 2026.5.16-beta.7).
- Control UI shows "Update available: v2026.5.18". Click Update now.
- Gateway writes:
/tmp/openclaw-update-run-handoff-XXXXXX/handoff.cjs (executable)
/tmp/openclaw-update-run-handoff-XXXXXX/handoff.json
/tmp/openclaw-update-run-handoff-XXXXXX/sentinel-meta.json
~/.openclaw/gateway-supervisor-restart-handoff.json
~/.openclaw/restart-sentinel.json with reason: "managed-service-handoff-started", before.version == after.version
- Systemd restarts the unit (per the supervisor-restart-handoff).
- New gateway starts on the same version. UI reads the stale sentinel and shows "Update skipped: managed-service-handoff-started".
Observed after the failed click:
$ ls -la /tmp/openclaw-update-run-handoff-Aka0m3/
-rwx------ 1 user user 6150 May 19 08:59 handoff.cjs
-rw------- 1 user user 878 May 19 08:59 handoff.json
-rw------- 1 user user 170 May 19 08:59 sentinel-meta.json
# handoff.log is MISSING — helper never ran appendLog() even once.
$ openclaw --version
OpenClaw 2026.5.16-beta.7 # unchanged
Manual workaround that succeeds in seconds:
npm install -g openclaw@latest && systemctl --user restart openclaw-gateway
Root cause (my read)
Looking at dist/server-methods-CfPYovlX.js (the update-managed-service-handoff region) and handoff.cjs:
- The gateway uses
child_process.spawn with {detached: true} to launch handoff.cjs, intending the helper to outlive the parent.
- It then triggers a supervisor restart via
gateway-supervisor-restart-handoff.json.
- Under
systemctl --user, the gateway and its spawned children all live in the same unit cgroup (openclaw-gateway.service). When systemd restarts the unit, it sends SIGTERM to the entire cgroup — including the freshly-spawned handoff helper, before the helper has reached its parent-exit-wait loop, much less the npm install step.
{detached: true} only moves the child into a new process group; it does not escape the systemd cgroup. To survive a unit restart, the helper has to be launched outside the unit's scope — e.g. via systemd-run --user --scope --unit=... or systemctl --user start openclaw-update@<id>.service, not a plain spawn.
Evidence the helper never starts work:
handoff.log is never created (the helper's very first action on any code path is appendLog(...)).
handoff.cjs is left behind in /tmp (it deletes its own sensitivePaths on completion or failure).
- Final sentinel shows
before.version === after.version === 2026.5.16-beta.7 with reason: "managed-service-handoff-started" — i.e. the gateway wrote the "in-flight" sentinel and was killed before the helper could overwrite it with either a success payload or a real failure reason like managed-service-handoff-spawn-failed/managed-service-handoff-parent-timeout/managed-service-handoff-failed.
This means every systemd-supervised install hits the same race. Non-systemd installs (PM2, raw node, foreground) probably work because the parent doesn't drag the helper down on exit.
Suggested fix
Make the helper escape the systemd unit before it's launched:
// pseudocode for the supervisor=systemd path
spawn("systemd-run", [
"--user", "--scope",
"--unit", `openclaw-update-${handoffId}.scope`,
"--collect",
process.execPath, helperPath, paramsPath,
], { detached: true, stdio: "ignore" }).unref();
Plus a sanity check: if handoffId exists in the sentinel but handoff.log was never written within N seconds after gateway start, surface a more diagnostic error like managed-service-handoff-helper-never-ran instead of leaving the in-flight "handoff-started" reason as the final state.
Impact
- All
systemctl --user-supervised installs (the recommended Linux pattern in the docs) are stuck on whatever version was first installed; the in-product Update button is a no-op.
- The error message in the UI tells the user nothing actionable —
doctorHint: "Run: openclaw doctor --non-interactive" doesn't detect the race.
Happy to provide more traces if useful. Manual upgrade path is fine for now, so this isn't urgent for me, but it's broken for everyone who finds it.
Summary
In systemd-supervised installs (
openclaw-gateway.servicerunning undersystemctl --user), clicking Update now in the Control UI silently fails on every attempt. The gateway restarts on the same version, the UI surfacesUpdate skipped: managed-service-handoff-started, andnpm install -g openclaw@latestis never actually invoked.The handoff helper script (
/tmp/openclaw-update-run-handoff-XXXXXX/handoff.cjs) gets created but never executes — itshandoff.lognever appears on disk, whilehandoff.cjsis still sitting in/tmp.Reproduced cleanly upgrading 2026.5.16-beta.7 → 2026.5.18 on Linux/systemd.
Repro
Environment:
systemctl --user(openclaw-gateway.service)Steps:
latesttag is newer than current binary (2026.5.18>2026.5.16-beta.7)./tmp/openclaw-update-run-handoff-XXXXXX/handoff.cjs(executable)/tmp/openclaw-update-run-handoff-XXXXXX/handoff.json/tmp/openclaw-update-run-handoff-XXXXXX/sentinel-meta.json~/.openclaw/gateway-supervisor-restart-handoff.json~/.openclaw/restart-sentinel.jsonwithreason: "managed-service-handoff-started",before.version == after.versionObserved after the failed click:
Manual workaround that succeeds in seconds:
npm install -g openclaw@latest && systemctl --user restart openclaw-gatewayRoot cause (my read)
Looking at
dist/server-methods-CfPYovlX.js(theupdate-managed-service-handoffregion) andhandoff.cjs:child_process.spawnwith{detached: true}to launchhandoff.cjs, intending the helper to outlive the parent.gateway-supervisor-restart-handoff.json.systemctl --user, the gateway and its spawned children all live in the same unit cgroup (openclaw-gateway.service). When systemd restarts the unit, it sends SIGTERM to the entire cgroup — including the freshly-spawned handoff helper, before the helper has reached its parent-exit-wait loop, much less thenpm installstep.{detached: true}only moves the child into a new process group; it does not escape the systemd cgroup. To survive a unit restart, the helper has to be launched outside the unit's scope — e.g. viasystemd-run --user --scope --unit=...orsystemctl --user start openclaw-update@<id>.service, not a plainspawn.Evidence the helper never starts work:
handoff.logis never created (the helper's very first action on any code path isappendLog(...)).handoff.cjsis left behind in/tmp(it deletes its ownsensitivePathson completion or failure).before.version === after.version === 2026.5.16-beta.7withreason: "managed-service-handoff-started"— i.e. the gateway wrote the "in-flight" sentinel and was killed before the helper could overwrite it with either a success payload or a real failure reason likemanaged-service-handoff-spawn-failed/managed-service-handoff-parent-timeout/managed-service-handoff-failed.This means every systemd-supervised install hits the same race. Non-systemd installs (PM2, raw
node, foreground) probably work because the parent doesn't drag the helper down on exit.Suggested fix
Make the helper escape the systemd unit before it's launched:
Plus a sanity check: if
handoffIdexists in the sentinel buthandoff.logwas never written within N seconds after gateway start, surface a more diagnostic error likemanaged-service-handoff-helper-never-raninstead of leaving the in-flight "handoff-started" reason as the final state.Impact
systemctl --user-supervised installs (the recommended Linux pattern in the docs) are stuck on whatever version was first installed; the in-product Update button is a no-op.doctorHint: "Run: openclaw doctor --non-interactive"doesn't detect the race.Happy to provide more traces if useful. Manual upgrade path is fine for now, so this isn't urgent for me, but it's broken for everyone who finds it.