#22304: Gateway: fix launchd start after stop
gateway
cli
size: M
Cluster:
Gateway and macOS Improvements
Supersedes #17784, #17763 and #22313
This fixes a lifecycle gap on macOS launchd.
After `openclaw gateway stop`, the LaunchAgent is unloaded (`bootout`).
Before this patch, `openclaw gateway start` only worked if the service was already loaded (it used restart semantics), so users got "service not loaded" guidance instead of an actual start.
What changed:
- Added an explicit service `start` path in daemon lifecycle.
- Implemented launchd start as `enable + bootstrap + kickstart`.
- Kept restart behavior unchanged for already-loaded services.
- Adjusted launchd runtime detection so plist-present + unloaded reports as `stopped` (not "not installed").
Tests:
- `pnpm -s vitest run src/daemon/launchd.test.ts src/cli/daemon-cli/lifecycle-core.test.ts`
AI-assisted:
- [x] AI-assisted change
- [x] Tested locally (targeted)
<!-- greptile_comment -->
<h3>Greptile Summary</h3>
This PR fixes a lifecycle gap where `openclaw gateway start` failed after `openclaw gateway stop` on macOS launchd. The fix adds an explicit `start` operation across all daemon backends (launchd, systemd, schtasks) and updates runtime detection to correctly report stopped-but-installed services.
- Added `start()` method to the `GatewayService` interface and implemented it for all three backends (launchd, systemd, Windows Scheduled Tasks)
- Updated launchd runtime detection to check for plist existence, reporting `stopped` (not `unknown`) when plist exists but service is unloaded
- Modified `runServiceStart` in lifecycle-core to invoke `service.start()` when the service is not loaded, falling back to hints only on error
- Launchd start implementation uses `enable + bootstrap + kickstart` sequence with proper error handling for already-loaded and GUI domain issues
- Comprehensive test coverage added for both the launchd start operation and the lifecycle-core start flow
<h3>Confidence Score: 5/5</h3>
- This PR is safe to merge with minimal risk
- The implementation correctly addresses the stated issue with a clean, well-tested solution. The changes follow existing patterns across all three daemon backends (launchd, systemd, schtasks), maintain backward compatibility with the restart flow, and include comprehensive test coverage. Error handling is robust with proper fallback to hints when start fails. The launchd-specific logic correctly handles edge cases like already-loaded services and GUI domain errors.
- No files require special attention
<sub>Last reviewed commit: ceedecc</sub>
<!-- greptile_other_comments_section -->
<sub>(2/5) Greptile learns from your feedback when you react with thumbs up/down!</sub>
<!-- /greptile_comment -->
Most Similar PRs
#18236: macOS daemon: bootstrap LaunchAgent on gateway start after stop
by agisilaos · 2026-02-16
88.1%
#11147: fix(daemon): stop gateway by port when no daemon service is active
by jasonthewhale · 2026-02-07
81.6%
#16845: fix(daemon): gateway auto-restart on SIGTERM + agent restart guidel...
by kiminbean · 2026-02-15
80.3%
#7155: fix(gateway): use kill SIGTERM instead of bootout for stop
by rafaelreis-r · 2026-02-02
79.7%
#11327: fix(launchd): reload plist from disk on restartLaunchAgent
by caiop91 · 2026-02-07
78.8%
#22313: fix: check plist existence before marking LaunchAgent missing
by MisterGuy420 · 2026-02-21
78.2%
#8260: fix(macOS): gateway readiness detection + reversible Configure later
by xksteven · 2026-02-03
78.0%
#23584: fix(daemon): improve gateway service detection to avoid false posit...
by mohandshamada · 2026-02-22
77.4%
#13084: fix(daemon): multi-layer defense against zombie gateway processes
by openperf · 2026-02-10
77.2%
#20272: fix: LaunchAgent KeepAlive causes restart loop (fixes #20257)
by MisterGuy420 · 2026-02-18
77.0%