You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
The built-in auto-update consistently kills the gateway on macOS with no recovery, leaving the service unloaded indefinitely. The root cause is that resolveCommandStdio forces piped stdout/stderr for the update subprocess, creating a fatal dependency on the gateway parent process — which is then killed by launchctl bootout during the restart step.
This is the underlying mechanism behind several related reports (#54861, #57379, #58041, #40811, #48992) that describe symptoms (service unloaded, multi-hour outages, KeepAlive not respected) without pinpointing why the restart handoff never completes.
Root Cause
1. Piped stdout/stderr creates a parent dependency
In src/process/spawn-utils.ts, resolveCommandStdio always returns "pipe" for stdout and stderr:
When runAutoUpdateCommand calls runCommandWithTimeout, the update subprocess's stdout/stderr are piped back to the gateway process for capture. The gateway holds the reading end of these pipes.
Bootout kills the gateway (the process holding the pipe's reading end)
The update subprocess's stdout/stderr pipes break
Any subsequent write to stdout/stderr triggers EPIPE / unhandled error
Update subprocess crashes beforerunRestartScript is called in maybeRestartService
Service stays unloaded — no restart script, no recovery
3. The restart script is spawned too late
The detached restart script (which could recover the service) is spawned in maybeRestartServiceafterrefreshGatewayServiceEnv completes. But the update subprocess crashes duringrefreshGatewayServiceEnv (or shortly after, when any code path writes to stdout/stderr). The restart script never gets a chance to run.
Several possible approaches (not mutually exclusive):
Skip refreshGatewayServiceEnv when running as auto-update child — detect via OPENCLAW_AUTO_UPDATE=1 env var, defer all restart logic to the detached restart script only
Spawn the restart script beforerefreshGatewayServiceEnv — ensure the recovery mechanism exists before bootout can kill the parent
Detach the update subprocess from the gateway's pipes — use stdio: ['ignore', 'ignore', 'ignore'] or write to a temp file instead of piping, so the parent dying doesn't crash the child
Summary
The built-in auto-update consistently kills the gateway on macOS with no recovery, leaving the service unloaded indefinitely. The root cause is that
resolveCommandStdioforces piped stdout/stderr for the update subprocess, creating a fatal dependency on the gateway parent process — which is then killed bylaunchctl bootoutduring the restart step.This is the underlying mechanism behind several related reports (#54861, #57379, #58041, #40811, #48992) that describe symptoms (service unloaded, multi-hour outages, KeepAlive not respected) without pinpointing why the restart handoff never completes.
Root Cause
1. Piped stdout/stderr creates a parent dependency
In
src/process/spawn-utils.ts,resolveCommandStdioalways returns"pipe"for stdout and stderr:When
runAutoUpdateCommandcallsrunCommandWithTimeout, the update subprocess's stdout/stderr are piped back to the gateway process for capture. The gateway holds the reading end of these pipes.2.
refreshGatewayServiceEnvkills the pipe-holderThe update flow:
openclaw update --yes --channel stable --json(child, piped stdout/stderr)printResultwrites JSON to stdout → pipe to gateway (still alive, works fine)updatePluginsAfterCoreUpdaterunsmaybeRestartService→refreshGatewayServiceEnv→openclaw gateway install --forcegateway install --force→activateLaunchAgent→launchctl bootoutrunRestartScriptis called inmaybeRestartService3. The restart script is spawned too late
The detached restart script (which could recover the service) is spawned in
maybeRestartServiceafterrefreshGatewayServiceEnvcompletes. But the update subprocess crashes duringrefreshGatewayServiceEnv(or shortly after, when any code path writes to stdout/stderr). The restart script never gets a chance to run.Evidence
openclaw update --yes --channel stable --jsoncommand works perfectly when run from a standalone process (not a child of the gateway)launchctloperations (bootout, bootstrap, kickstart) work correctly in isolationopenclaw gateway install --forcesucceeds when run independentlyWorkaround
Disable built-in auto-update and run updates from a standalone launchd job:
Suggested Fix
Several possible approaches (not mutually exclusive):
refreshGatewayServiceEnvwhen running as auto-update child — detect viaOPENCLAW_AUTO_UPDATE=1env var, defer all restart logic to the detached restart script onlyrefreshGatewayServiceEnv— ensure the recovery mechanism exists before bootout can kill the parentlaunchctl stoporlaunchctl kickstart -kinstead ofbootoutinactivateLaunchAgent— keeps the service registered so KeepAlive can recover it (see PR fix(gateway): use launchctl stop instead of bootout to prevent Launch… #43652)stdio: ['ignore', 'ignore', 'ignore']or write to a temp file instead of piping, so the parent dying doesn't crash the childRelated Issues
openclaw update— plist not reinstalled #40811 — gateway entrypoint mismatch after update (relatedrefreshGatewayServiceEnvissue)Environment