Skip to content

fix(daemon): use KeepAlive SuccessfulExit=false to prevent launchd crash-loop#73849

Closed
kaseonedge wants to merge 5 commits into
openclaw:mainfrom
kaseonedge:fix/73673-launchd-crash-loop
Closed

fix(daemon): use KeepAlive SuccessfulExit=false to prevent launchd crash-loop#73849
kaseonedge wants to merge 5 commits into
openclaw:mainfrom
kaseonedge:fix/73673-launchd-crash-loop

Conversation

@kaseonedge

Copy link
Copy Markdown
Contributor

Fixes #73673

Problem

The macOS LaunchAgent plist uses KeepAlive=true, which restarts the gateway after ANY exit (including clean exits with code 0). This creates a crash-loop where:

  1. Gateway exits (clean or crash)
  2. launchd immediately restarts it
  3. If the root cause was a crash, it crashes again
  4. Repeat indefinitely, making debugging extremely difficult

Solution

Change KeepAlive=true to KeepAlive with SuccessfulExit=false:

  • Gateway only restarts on crash (non-zero exit code)
  • Clean exits are respected (gateway stays stopped for debugging)
  • ThrottleInterval=1 still applies to crash restarts

Changes

  • src/daemon/launchd-plist.ts: Updated plist template to use KeepAlive<dict>SuccessfulExit<false/></dict>
  • src/daemon/launchd.test.ts: Updated test to expect new KeepAlive format
  • src/daemon/service-audit.ts: Updated audit check to recognize both KeepAlive forms

metal added 5 commits April 28, 2026 18:38
ensureDockerImage now tries to pull custom images and gracefully returns
when the Docker daemon is down, instead of throwing 'Sandbox image not
found'. This closes the gap identified in the PR review where only
DEFAULT_SANDBOX_IMAGE was guarded.
…port

- Export from docker.ts, import in browser.ts and doctor-sandbox.ts
- Eliminates byte-for-byte duplicated logic across three files
- Addresses codex review feedback on PR #73671
Introduced during isDockerDaemonUnavailable consolidation refactor.
Fixes CI build failures from duplicate identifier declarations.
…ash-loop

Change macOS LaunchAgent plist from KeepAlive=true to KeepAlive with
SuccessfulExit=false. This prevents launchd from restarting the gateway
after clean exits, making crash debugging much easier while still
restarting on actual failures.

Also update the service audit check to recognize both KeepAlive forms.
@kaseonedge
kaseonedge requested a review from a team as a code owner April 28, 2026 22:57
@openclaw-barnacle openclaw-barnacle Bot added gateway Gateway runtime commands Command implementations agents Agent runtime and tooling size: S labels Apr 28, 2026
@greptile-apps

greptile-apps Bot commented Apr 28, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR fixes the launchd crash-loop by replacing KeepAlive<true/> with KeepAlive<dict><SuccessfulExit><false/></dict>, so the gateway only restarts on non-zero exits. It also bundles unrelated Docker daemon availability handling across several sandbox files.

  • The ensureDockerImage change in docker.ts silently substitutes debian:bookworm-slim for DEFAULT_SANDBOX_IMAGE whenever the custom image is absent and Docker is running — directly contradicting the original design ("OpenClaw will not substitute plain debian:bookworm-slim") and silently giving users a degraded sandbox environment missing python3 and other helpers.

Confidence Score: 3/5

Safe to merge the launchd fix; the Docker image fallback logic in docker.ts introduces a silent behavioral regression that should be addressed first.

The core launchd plist change, test update, and audit regex update are correct and well-scoped. However, the Docker-related changes include a P1 issue: ensureDockerImage now silently falls back to debian:bookworm-slim even when Docker is fully operational, which was explicitly disallowed in the original code and degrades sandbox functionality without any user-visible warning.

src/agents/sandbox/docker.ts — the ensureDockerImage fallback logic

Prompt To Fix All With AI
This is a comment left during a code review.
Path: src/agents/sandbox/docker.ts
Line: 315-327

Comment:
**Silent fallback contradicts original design intent**

When Docker is running but the custom sandbox image is simply missing, this code silently pulls and tags `debian:bookworm-slim` as `DEFAULT_SANDBOX_IMAGE`. The original error message (still present in the failure branch on line 323) explicitly stated "OpenClaw will not substitute plain debian:bookworm-slim" — because the default image includes python3 and other sandbox helpers that `bookworm-slim` lacks. A user with Docker running who forgot to build the image will now silently get a degraded environment with no warning, and the error message they do see if the pull fails still says the substitution won't happen, which is now untrue.

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: src/commands/doctor-sandbox.ts
Line: 89-90

Comment:
**Unused variable `lower`**

`lower` is assigned but never read — `isDockerDaemonUnavailable` is called with the original `stderr` string. Either pass `lower` to the function or remove the assignment.

```suggestion
    if (isDockerDaemonUnavailable(stderr)) {
```

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: src/agents/sandbox/browser.ts
Line: 129

Comment:
**Misplaced duplicate import**

This import statement lands in the middle of the file (after function bodies), and `SandboxConfig` is already imported at line 49. `SandboxBrowserConfig` is not used anywhere in this file. The import should be removed or consolidated at the top of the file with the other type imports.

How can I resolve this? If you propose a fix, please make it concise.

Reviews (1): Last reviewed commit: "fix(daemon): use KeepAlive SuccessfulExi..." | Re-trigger Greptile

Comment on lines 315 to +327
if (image === DEFAULT_SANDBOX_IMAGE) {
throw new Error(
`Sandbox image not found: ${image}. Build it with scripts/sandbox-setup.sh before enabling Docker sandboxing. The default image includes python3 for sandbox write/edit helpers; OpenClaw will not substitute plain debian:bookworm-slim.`,
);
const pullResult = await execDocker(["pull", "debian:bookworm-slim"], { allowFailure: true });
if (pullResult.code !== 0) {
const stderr = pullResult.stderr.trim();
if (isDockerDaemonUnavailable(stderr)) {
return;
}
throw new Error(
`Sandbox image not found: ${image}. Build it with scripts/sandbox-setup.sh before enabling Docker sandboxing. The default image includes python3 for sandbox write/edit helpers; OpenClaw will not substitute plain debian:bookworm-slim. Failed to pull fallback: ${stderr}`,
);
}
await execDocker(["tag", "debian:bookworm-slim", DEFAULT_SANDBOX_IMAGE]);
return;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Silent fallback contradicts original design intent

When Docker is running but the custom sandbox image is simply missing, this code silently pulls and tags debian:bookworm-slim as DEFAULT_SANDBOX_IMAGE. The original error message (still present in the failure branch on line 323) explicitly stated "OpenClaw will not substitute plain debian:bookworm-slim" — because the default image includes python3 and other sandbox helpers that bookworm-slim lacks. A user with Docker running who forgot to build the image will now silently get a degraded environment with no warning, and the error message they do see if the pull fails still says the substitution won't happen, which is now untrue.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/agents/sandbox/docker.ts
Line: 315-327

Comment:
**Silent fallback contradicts original design intent**

When Docker is running but the custom sandbox image is simply missing, this code silently pulls and tags `debian:bookworm-slim` as `DEFAULT_SANDBOX_IMAGE`. The original error message (still present in the failure branch on line 323) explicitly stated "OpenClaw will not substitute plain debian:bookworm-slim" — because the default image includes python3 and other sandbox helpers that `bookworm-slim` lacks. A user with Docker running who forgot to build the image will now silently get a degraded environment with no warning, and the error message they do see if the pull fails still says the substitution won't happen, which is now untrue.

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines +89 to +90
const lower = stderr.toLowerCase();
if (isDockerDaemonUnavailable(stderr)) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Unused variable lower

lower is assigned but never read — isDockerDaemonUnavailable is called with the original stderr string. Either pass lower to the function or remove the assignment.

Suggested change
const lower = stderr.toLowerCase();
if (isDockerDaemonUnavailable(stderr)) {
if (isDockerDaemonUnavailable(stderr)) {
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/commands/doctor-sandbox.ts
Line: 89-90

Comment:
**Unused variable `lower`**

`lower` is assigned but never read — `isDockerDaemonUnavailable` is called with the original `stderr` string. Either pass `lower` to the function or remove the assignment.

```suggestion
    if (isDockerDaemonUnavailable(stderr)) {
```

How can I resolve this? If you propose a fix, please make it concise.

};
}

import type { SandboxBrowserConfig, SandboxConfig } from "./types.js";

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Misplaced duplicate import

This import statement lands in the middle of the file (after function bodies), and SandboxConfig is already imported at line 49. SandboxBrowserConfig is not used anywhere in this file. The import should be removed or consolidated at the top of the file with the other type imports.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/agents/sandbox/browser.ts
Line: 129

Comment:
**Misplaced duplicate import**

This import statement lands in the middle of the file (after function bodies), and `SandboxConfig` is already imported at line 49. `SandboxBrowserConfig` is not used anywhere in this file. The import should be removed or consolidated at the top of the file with the other type imports.

How can I resolve this? If you propose a fix, please make it concise.

@clawsweeper

clawsweeper Bot commented Apr 28, 2026

Copy link
Copy Markdown
Contributor

Thanks for the context here. I swept through the related work, and this is now duplicate or superseded.

Close as superseded: the focused launchd crash-loop work is now tracked by the newer maintainer-labeled PR, while this branch uses the wrong launchd failure policy and carries already-merged/regressive sandbox changes.

Canonical path: Continue the focused launchd lifecycle fix in #81726 or a replacement that preserves clean-exit restart handoffs, and keep sandbox Docker behavior on the merged #73671 path.

So I’m closing this here and keeping the remaining discussion on #81726 and #73671.

Review details

Best possible solution:

Continue the focused launchd lifecycle fix in #81726 or a replacement that preserves clean-exit restart handoffs, and keep sandbox Docker behavior on the merged #73671 path.

Do we have a high-confidence way to reproduce the issue?

Yes, source-backed rather than live: current main emits boolean KeepAlive=true, launchd's contract explains the restart conditions, and current run-loop code exits 0 for launchd restart/update handoffs. I did not run a live macOS LaunchAgent smoke in this read-only review.

Is this the best way to solve the issue?

No. This PR's SuccessfulExit=false policy is not the best fix for the linked crash-loop because it still restarts non-zero failures and can break clean-exit handoffs; the focused maintainer PR is the safer canonical path.

Security review:

Security review needs attention: The launchd change is not security-sensitive, but the sandbox diff introduces a concrete Docker image provenance regression.

  • [medium] Silent Docker base image substitution — src/agents/sandbox/docker.ts:308
    The PR can pull and tag debian:bookworm-slim as the OpenClaw sandbox image when the intended default image is missing, broadening image provenance and leaving users with a degraded sandbox environment without explicit consent.
    Confidence: 0.94

What I checked:

  • Live PR state: Live GitHub shows this PR is open, conflicting, authored by a contributor, closes [Bug]: Gateway repeatedly restarts under launchd after crash, making it difficult to debug #73673, and has no after-fix macOS LaunchAgent proof in the body or comments. (44ee2954ccd2)
  • PR diff launchd policy: The PR diff changes the generated LaunchAgent to KeepAlive with SuccessfulExit=false, which the launchd contract says restarts on the inverse of successful exit, so non-zero startup failures can still relaunch while clean-exit handoffs stop. (src/daemon/launchd-plist.ts:178, 44ee2954ccd2)
  • Current main launchd policy: Current main still renders boolean KeepAlive=true and now also includes ExitTimeOut, ProcessType, and 10-second ThrottleInterval; this stale PR head does not preserve all of that current hardening. (src/daemon/launchd-plist.ts:183, 4e10969aded0)
  • Current clean-exit restart dependency: Current restart/update handoff paths under launchd write handoff state and then exit with status 0, so a global SuccessfulExit=false policy can strand intended supervisor relaunches. (src/cli/gateway-cli/run-loop.ts:195, 4e10969aded0)
  • Launchd contract check: The launchd.plist manual says boolean KeepAlive=true unconditionally keeps the job alive, while SuccessfulExit=false restarts when the exit status is not zero.
  • Superseding launchd PR: macOS: avoid launchd startup crash loops #81726 is open, maintainer-labeled, closes the same launchd crash-loop issue, changes the focused daemon/docs/test surface to SuccessfulExit=true, and preserves current ExitTimeOut, ProcessType, ThrottleInterval, and Umask policy. (src/daemon/launchd-plist.ts:179, 68bf98c40700)

Likely related people:

  • BunsDev: Authored recent macOS update restart lifecycle hardening on the launchd plist and run-loop surface, and authored the focused replacement PR for the same linked launchd issue. (role: recent area contributor; confidence: high; commits: fa79e9754ecc, 68bf98c40700; files: src/daemon/launchd-plist.ts, src/daemon/launchd.test.ts, src/cli/gateway-cli/run-loop.ts)
  • steipete: Current blame and recent commit history connect this person to launchd plist/audit code and legacy update launchd-job cleanup relevant to clean-exit restart behavior. (role: recent adjacent contributor; confidence: medium; commits: 2eee70e0a64b, d5b87672f8ad, a6141a5a4196; files: src/daemon/launchd-plist.ts, src/daemon/service-audit.ts, src/daemon/launchd.ts)
  • bryanpearson: Authored the current ProcessType=Interactive LaunchAgent hardening that this stale PR head would not preserve if merged without rebasing. (role: recent launchd hardening contributor; confidence: medium; commits: a2b8f2aef083; files: src/daemon/launchd-plist.ts)
  • gumadeiras: Authored earlier launchd work explicitly keeping KeepAlive while preserving restart hardening on the same plist/test surface. (role: earlier KeepAlive behavior contributor; confidence: medium; commits: 4ebefe647a7c, c0ce125512d8; files: src/daemon/launchd-plist.ts, src/daemon/launchd.test.ts)
  • Kevin Shenghui: Introduced earlier LaunchAgent ThrottleInterval restart-loop mitigation in the same plist and test area. (role: earlier launchd throttle contributor; confidence: medium; commits: 16ccd5a87452; files: src/daemon/launchd-plist.ts, src/daemon/launchd.test.ts)

Codex review notes: model gpt-5.5, reasoning high; reviewed against 4e10969aded0.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

agents Agent runtime and tooling commands Command implementations gateway Gateway runtime size: S

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: Gateway repeatedly restarts under launchd after crash, making it difficult to debug

1 participant