Skip to content

[Bug] Auto-update subprocess crashes before restart handoff: piped stdout/stderr breaks when bootout kills parent gateway #58890

Description

@moxy-kit

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

function resolveCommandStdio(params): ["pipe" | "inherit" | "ignore", "pipe", "pipe"] {
  return [
    params.hasInput ? "pipe" : params.preferInherit ? "inherit" : "pipe",
    "pipe",  // stdout always piped
    "pipe",  // stderr always piped
  ];
}

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.

2. refreshGatewayServiceEnv kills the pipe-holder

The update flow:

  1. Gateway spawns openclaw update --yes --channel stable --json (child, piped stdout/stderr)
  2. npm install succeeds (binary updated on disk)
  3. printResult writes JSON to stdout → pipe to gateway (still alive, works fine)
  4. updatePluginsAfterCoreUpdate runs
  5. maybeRestartServicerefreshGatewayServiceEnvopenclaw gateway install --force
  6. gateway install --forceactivateLaunchAgentlaunchctl bootout
  7. Bootout kills the gateway (the process holding the pipe's reading end)
  8. The update subprocess's stdout/stderr pipes break
  9. Any subsequent write to stdout/stderr triggers EPIPE / unhandled error
  10. Update subprocess crashes before runRestartScript is called in maybeRestartService
  11. 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 maybeRestartService after refreshGatewayServiceEnv completes. But the update subprocess crashes during refreshGatewayServiceEnv (or shortly after, when any code path writes to stdout/stderr). The restart script never gets a chance to run.

Evidence

  • Confirmed on macOS 26.4 (arm64), Node 22.22.0, OpenClaw 2026.3.28 → 2026.3.31
  • The same openclaw update --yes --channel stable --json command works perfectly when run from a standalone process (not a child of the gateway)
  • All launchctl operations (bootout, bootstrap, kickstart) work correctly in isolation
  • openclaw gateway install --force succeeds when run independently
  • The ONLY failure mode is when the update runs as a child of the process being bootout'd

Workaround

Disable built-in auto-update and run updates from a standalone launchd job:

// openclaw.json
"update": { "auto": { "enabled": false } }
<!-- ~/Library/LaunchAgents/ai.openclaw.auto-update.plist -->
<plist version="1.0">
  <dict>
    <key>Label</key>
    <string>ai.openclaw.auto-update</string>
    <key>ProgramArguments</key>
    <array>
      <string>/opt/homebrew/opt/node@22/bin/node</string>
      <string>/opt/homebrew/lib/node_modules/openclaw/dist/entry.js</string>
      <string>update</string>
      <string>--yes</string>
      <string>--channel</string>
      <string>stable</string>
    </array>
    <key>StartInterval</key>
    <integer>3600</integer>
    <key>EnvironmentVariables</key>
    <dict>
      <key>HOME</key>
      <string>/Users/you</string>
      <key>PATH</key>
      <string>/opt/homebrew/opt/node@22/bin:/opt/homebrew/bin:/usr/bin:/bin</string>
      <key>OPENCLAW_LAUNCHD_LABEL</key>
      <string>ai.openclaw.gateway</string>
    </dict>
  </dict>
</plist>

Suggested Fix

Several possible approaches (not mutually exclusive):

  1. 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
  2. Spawn the restart script before refreshGatewayServiceEnv — ensure the recovery mechanism exists before bootout can kill the parent
  3. Use launchctl stop or launchctl kickstart -k instead of bootout in activateLaunchAgent — keeps the service registered so KeepAlive can recover it (see PR fix(gateway): use launchctl stop instead of bootout to prevent Launch… #43652)
  4. 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

Related Issues

Environment

  • OS: macOS 26.4 (arm64, Mac Mini)
  • Node: 22.22.0
  • OpenClaw: 2026.3.28 → 2026.3.31
  • Install method: npm global
  • Service manager: launchd (LaunchAgent)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    Priority

    None yet

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions