Summary
On macOS, openclaw update schedules its out-of-process update job using launchctl submit. launchctl submit creates the job with KeepAlive = true by default, so the update runner (run-openclaw-update.zsh) is relaunched immediately every time it exits. Each iteration runs openclaw update, whose first action is "Stopping managed gateway service before package update" — so the managed gateway is killed roughly once per minute and never stays up. The package update itself succeeds on the first pass; the loop continues forever afterward with availability.available = false.
Net user-visible symptom: after updating, the gateway won't stay up.
Environment
- OpenClaw
2026.5.28 (commit e932160); first observed after auto-update 2026.5.26 → 2026.5.28
- Install: global package at
/opt/homebrew/lib/node_modules/openclaw (pnpm)
- node
v25.6.1
- macOS
26.4 (arm64)
Impact
- Managed gateway (
ai.openclaw.gateway) is force-stopped ~1×/minute indefinitely → all agents/channels (Discord, Telegram, cron) keep dropping.
- Two jobs were submitted ~1s apart (
ai.openclaw.manual-update.<epoch>), so the gateway was being stopped by two concurrent loops.
~/.openclaw/work/update-logs/openclaw-update-*.log grew to ~2 MB / 80+ repeated == START / == UPDATE / == DOCTOR cycles in ~80 min.
~/.openclaw/logs/gateway-restart.log fills with Could not find service "ai.openclaw.gateway" in domain for user gui: 501 as the loop races with itself (kickstart attempted against a service that a concurrent iteration has just booted out).
Root cause
The submitted launchd jobs are KeepAlive. Confirmed via:
$ launchctl print gui/501/ai.openclaw.manual-update.<epoch>
path = (submitted by launchctl[...])
program = /Users/<user>/.openclaw/work/update-logs/run-openclaw-update.zsh
properties = keepalive | inferred program
path = (submitted by launchctl ...) + properties = keepalive = a launchctl submit job using the default unconditional KeepAlive. run-openclaw-update.zsh exits cleanly (DOCTOR_EXIT 0) each cycle, and launchd relaunches it in the same second:
== END Sun May 31 19:49:38 SAST 2026 ==
== START Sun May 31 19:49:38 SAST 2026 ==
Steps to reproduce
- On macOS, trigger an update that uses the launchctl-submit path (e.g. auto-update or
openclaw update).
- Observe a transient
ai.openclaw.manual-update.<epoch> job appear: launchctl list | grep manual-update.
launchctl print gui/$(id -u)/ai.openclaw.manual-update.<epoch> → properties = keepalive.
- Watch the gateway get stopped each cycle (
gateway-restart.log / update log) and never stay up.
Expected
The update job runs once and exits. The gateway is stopped only for the actual package swap, then restarted and left running.
Suggested fix
launchctl submit is deprecated and defaults to unconditional KeepAlive — it's the wrong primitive for a run-once task. Options:
- Preferred: write a temporary LaunchAgent plist with
KeepAlive = false (or omit it) and RunAtLoad = true, bootstrap it, and bootout it on completion — instead of launchctl submit.
- If staying with
launchctl submit, the runner must remove its own job on exit (e.g. launchctl remove ai.openclaw.manual-update.<epoch> as the last step) so it can't be relaunched.
- Defensive: never submit a second update job while one is already registered (the two near-simultaneous submits doubled the damage).
- The "stop the gateway before update" step should be gated on an update actually being applied — when
availability.available == false, the job should no-op instead of stopping the gateway.
Workaround (for affected users)
# tear down the runaway loop
for l in $(launchctl list | awk '/ai\.openclaw\.manual-update/ {print $3}'); do launchctl bootout gui/$(id -u)/$l; done
pkill -f run-openclaw-update.zsh; pkill -f openclaw-doctor
# the gateway is left booted out — reload it
launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/ai.openclaw.gateway.plist
launchctl kickstart -k gui/$(id -u)/ai.openclaw.gateway
The submitted jobs are not persisted to disk, so they don't survive logout/reboot — but the next update re-triggers the loop until this is fixed.
Summary
On macOS,
openclaw updateschedules its out-of-process update job usinglaunchctl submit.launchctl submitcreates the job withKeepAlive = trueby default, so the update runner (run-openclaw-update.zsh) is relaunched immediately every time it exits. Each iteration runsopenclaw update, whose first action is "Stopping managed gateway service before package update" — so the managed gateway is killed roughly once per minute and never stays up. The package update itself succeeds on the first pass; the loop continues forever afterward withavailability.available = false.Net user-visible symptom: after updating, the gateway won't stay up.
Environment
2026.5.28(commite932160); first observed after auto-update2026.5.26 → 2026.5.28/opt/homebrew/lib/node_modules/openclaw(pnpm)v25.6.126.4(arm64)Impact
ai.openclaw.gateway) is force-stopped ~1×/minute indefinitely → all agents/channels (Discord, Telegram, cron) keep dropping.ai.openclaw.manual-update.<epoch>), so the gateway was being stopped by two concurrent loops.~/.openclaw/work/update-logs/openclaw-update-*.loggrew to ~2 MB / 80+ repeated== START / == UPDATE / == DOCTORcycles in ~80 min.~/.openclaw/logs/gateway-restart.logfills withCould not find service "ai.openclaw.gateway" in domain for user gui: 501as the loop races with itself (kickstart attempted against a service that a concurrent iteration has just booted out).Root cause
The submitted launchd jobs are KeepAlive. Confirmed via:
path = (submitted by launchctl ...)+properties = keepalive= alaunchctl submitjob using the default unconditional KeepAlive.run-openclaw-update.zshexits cleanly (DOCTOR_EXIT 0) each cycle, and launchd relaunches it in the same second:Steps to reproduce
openclaw update).ai.openclaw.manual-update.<epoch>job appear:launchctl list | grep manual-update.launchctl print gui/$(id -u)/ai.openclaw.manual-update.<epoch>→properties = keepalive.gateway-restart.log/ update log) and never stay up.Expected
The update job runs once and exits. The gateway is stopped only for the actual package swap, then restarted and left running.
Suggested fix
launchctl submitis deprecated and defaults to unconditional KeepAlive — it's the wrong primitive for a run-once task. Options:KeepAlive = false(or omit it) andRunAtLoad = true,bootstrapit, andbootoutit on completion — instead oflaunchctl submit.launchctl submit, the runner must remove its own job on exit (e.g.launchctl remove ai.openclaw.manual-update.<epoch>as the last step) so it can't be relaunched.availability.available == false, the job should no-op instead of stopping the gateway.Workaround (for affected users)
The submitted jobs are not persisted to disk, so they don't survive logout/reboot — but the next update re-triggers the loop until this is fixed.