openclaw - 💡(How to fix) Fix openclaw gateway stop calls launchctl disable unconditionally, breaks KeepAlive auto-recovery [1 comments, 2 participants]

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…
GitHub stats
openclaw/openclaw#77934Fetched 2026-05-06 06:19:08
View on GitHub
Comments
1
Participants
2
Timeline
1
Reactions
2
Timeline (top)
commented ×1

openclaw gateway stop (on macOS) calls launchctl disable unconditionally before stopping the LaunchAgent. Once a service is disabled, launchd treats KeepAlive=true as a no-op until the service is re-enabled with launchctl enable. This silently breaks the gateway's auto-recovery story.

Root Cause

KeepAlive=true is the headline reliability feature for the gateway daemon. Any path that silently nullifies it is a footgun. With this fix, the documented "if the gateway crashes, launchd will restart it" property actually holds for unexpected/abnormal stops. With the current code, KeepAlive only protects against process-crashes that don't go through gateway stop — which excludes the most common recovery flow.

Code Example

async function stopLaunchAgent({ stdout, env }) {
  const serviceEnv = env ?? process.env;
  const domain = resolveGuiDomain();
  const label = resolveLaunchAgentLabel({ env: serviceEnv });
  const serviceTarget = `${domain}/${label}`;

  const disable = await execLaunchctl(["disable", serviceTarget]);    // ← unconditional
  if (disable.code !== 0) {
    await bootoutLaunchAgentOrThrow({ ... });
    return;
  }
  // ... stop / wait / bootout ...
}

---

launchctl enable gui/501/ai.openclaw.gateway
launchctl bootstrap gui/501 ~/Library/LaunchAgents/ai.openclaw.gateway.plist

---

async function stopLaunchAgent({ stdout, env, permanent = false }) {
  const serviceTarget = `${domain}/${label}`;

  if (permanent) {
    const disable = await execLaunchctl(["disable", serviceTarget]);
    if (disable.code !== 0) { /* existing fallback */ }
  }

  // Default path: bootout only — service stays enabled, KeepAlive can respawn.
  await bootoutLaunchAgentOrThrow({ serviceTarget, stdout, warning: "stopped" });
}

---

openclaw gateway stop                  # bootout only, service stays enabled, KeepAlive respawns
openclaw gateway stop --disable        # current behavior: disable + stop, no respawn
RAW_BUFFERClick to expand / collapse

Summary

openclaw gateway stop (on macOS) calls launchctl disable unconditionally before stopping the LaunchAgent. Once a service is disabled, launchd treats KeepAlive=true as a no-op until the service is re-enabled with launchctl enable. This silently breaks the gateway's auto-recovery story.

Reproduction

dist/launchd-8T4SfMJh.js:470-505:

async function stopLaunchAgent({ stdout, env }) {
  const serviceEnv = env ?? process.env;
  const domain = resolveGuiDomain();
  const label = resolveLaunchAgentLabel({ env: serviceEnv });
  const serviceTarget = `${domain}/${label}`;

  const disable = await execLaunchctl(["disable", serviceTarget]);    // ← unconditional
  if (disable.code !== 0) {
    await bootoutLaunchAgentOrThrow({ ... });
    return;
  }
  // ... stop / wait / bootout ...
}

After openclaw gateway stop, the LaunchAgent is in state = disabled and KeepAlive cannot fire. The plist is still on disk; launchctl bootstrap gui/$UID/<plist> returns success but the service stays disabled and does not actually start.

Observed impact

On 2026-05-05 09:11 EDT this gateway took an embedded 5-min LLM-call timeout, the run controller escalated to a process-level shutdown, and launchd KeepAlive=true did not respawn the gateway. The user (Brian) had to manually run:

launchctl enable gui/501/ai.openclaw.gateway
launchctl bootstrap gui/501 ~/Library/LaunchAgents/ai.openclaw.gateway.plist

…to recover. Total downtime ~55 minutes during a workday. This is the second multi-hour outage in a week traced to the same trap.

The trap is also opaque to operators: launchctl print gui/$UID/<label> shows state = disabled only if you read carefully; nothing in openclaw status / openclaw doctor (as of this version) calls out the disable state with a recovery hint.

Proposed fix

Make disable opt-in via a --disable (or --permanent) flag.

async function stopLaunchAgent({ stdout, env, permanent = false }) {
  const serviceTarget = `${domain}/${label}`;

  if (permanent) {
    const disable = await execLaunchctl(["disable", serviceTarget]);
    if (disable.code !== 0) { /* existing fallback */ }
  }

  // Default path: bootout only — service stays enabled, KeepAlive can respawn.
  await bootoutLaunchAgentOrThrow({ serviceTarget, stdout, warning: "stopped" });
}

CLI:

openclaw gateway stop                  # bootout only, service stays enabled, KeepAlive respawns
openclaw gateway stop --disable        # current behavior: disable + stop, no respawn

Also worth surfacing in openclaw doctor: if a cached LaunchAgent label resolves to state = disabled, print a one-liner with the launchctl enable recovery command. (Doctor currently flags missing-plist with a bootout hint at dist/doctor-gateway-daemon-flow-BJRWGNB5.js:51 — same pattern, different state.)

Why this matters

KeepAlive=true is the headline reliability feature for the gateway daemon. Any path that silently nullifies it is a footgun. With this fix, the documented "if the gateway crashes, launchd will restart it" property actually holds for unexpected/abnormal stops. With the current code, KeepAlive only protects against process-crashes that don't go through gateway stop — which excludes the most common recovery flow.

Environment

  • OpenClaw installed from npm at /opt/homebrew/lib/node_modules/openclaw/ (macOS, M-series, node v22)
  • LaunchAgent label ai.openclaw.gateway, domain gui/501
  • Diagnostic timestamp: 2026-05-05

extent analysis

TL;DR

The openclaw gateway stop command should be modified to conditionally disable the LaunchAgent to prevent silently breaking the gateway's auto-recovery feature.

Guidance

  • Modify the stopLaunchAgent function to make the disable command opt-in via a --disable flag.
  • Update the CLI to support the new --disable flag, allowing users to choose between booting out the service only or disabling it permanently.
  • Consider adding a check in openclaw doctor to detect and report if a LaunchAgent is in a disabled state, providing a recovery hint.
  • Verify the fix by testing the openclaw gateway stop command with and without the --disable flag, ensuring the LaunchAgent behaves as expected.

Example

async function stopLaunchAgent({ stdout, env, permanent = false }) {
  // ...
  if (permanent) {
    const disable = await execLaunchctl(["disable", serviceTarget]);
    // ...
  }
  // ...
}

Notes

The proposed fix requires updating the stopLaunchAgent function and the CLI to support the new --disable flag. Additionally, surfacing the disabled state in openclaw doctor can help operators quickly identify and recover from this issue.

Recommendation

Apply the proposed fix by making the disable command opt-in via a --disable flag, as it provides more control over the LaunchAgent's behavior and prevents silently breaking the auto-recovery feature.

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING