Skip to content

fix(daemon): resilient launchctl bootstrap with retry and load fallback#48476

Closed
BenediktSchackenberg wants to merge 6 commits into
openclaw:mainfrom
BenediktSchackenberg:fix/launchd-bootstrap-resilience
Closed

fix(daemon): resilient launchctl bootstrap with retry and load fallback#48476
BenediktSchackenberg wants to merge 6 commits into
openclaw:mainfrom
BenediktSchackenberg:fix/launchd-bootstrap-resilience

Conversation

@BenediktSchackenberg

Copy link
Copy Markdown

Summary

Fixes #8619openclaw gateway install fails with launchctl bootstrap failed: Bootstrap failed: 125: Domain does not support specified action on re-install, SSH/headless sessions, and under sudo.

Problem

bootstrapLaunchAgentOrThrow() calls launchctl bootstrap gui/<uid> but:

  1. Re-install: If the service is already registered (e.g., user runs install twice), bootstrap returns exit code 5 ("Input/output error"). The code does bootout before bootstrap during install, but if the bootout silently fails (service not loaded in the expected state), the subsequent bootstrap hits the already-registered error with no retry.

  2. SSH/headless: The gui/ domain doesn't exist without an active GUI session. The error is detected but the code throws immediately without trying alternatives.

  3. sudo: process.getuid() returns 0gui/0 → root has no GUI session.

Fix

Two changes to bootstrapLaunchAgentOrThrow():

1. Retry on "already bootstrapped"

When bootstrap fails with exit code 5 / "already loaded" / "already bootstrapped", bootout the stale registration and retry once. This handles the common re-install scenario.

2. Fallback to launchctl load

When the gui/ domain is unavailable (SSH, headless, sudo), try launchctl load -w <plist> as a last resort. This is deprecated since macOS 10.10 but still works universally — including in SSH sessions and headless contexts. Only if load also fails do we throw the original GUI-session error with the actionable guidance.

Changes

  • src/daemon/launchd.ts: Added isAlreadyBootstrapped() helper, retry logic, and launchctl load fallback in bootstrapLaunchAgentOrThrow()
  • src/daemon/launchd.test.ts: 3 new test cases covering retry, load fallback success, and load fallback failure

Testing

  • Existing test updated: GUI-domain failure now verifies load was attempted before throwing
  • New: "falls back to launchctl load when gui domain is unavailable"
  • New: "retries bootstrap after bootout when service is already registered"

AI-assisted: Built with Claude, reviewed by human.

@greptile-apps

greptile-apps Bot commented Mar 16, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR improves bootstrapLaunchAgentOrThrow to handle three previously-failing launchctl bootstrap scenarios: re-install when the service is already registered (exit-code 5), SSH/headless sessions, and sudo invocations where the gui/ domain is unavailable. The approach — bootout-then-retry for "already bootstrapped" errors, with a launchctl load -w fallback when the GUI domain is absent — is well-reasoned and consistent with the existing codebase style.

Key observations:

  • The happy paths (fresh install, re-install, SSH with working load) are correct and well-tested.
  • The isAlreadyBootstrapped helper correctly detects the exit-code-5 case via the error text rather than the exit code, which is consistent with how isUnsupportedGuiDomain works.
  • One edge-case bug: when the initial error is "already bootstrapped", the retry surfaces a GUI-domain error, and launchctl load also fails, throwBootstrapGuiSessionError is called with the original detail ("Bootstrap failed: 5: Input/output error") rather than retryDetail ("Domain does not support specified action"). The actionable GUI-session guidance is still shown, but the specific error detail in the first line contradicts it and could confuse users trying to diagnose the failure.
  • Tests are thorough and the mock extensions (bootstrapFailuresRemaining, loadError) are clean.

Confidence Score: 4/5

  • Safe to merge — the new retry and load-fallback paths work correctly for all common scenarios; only a minor error-detail mismatch exists in an uncommon composite failure case.
  • The core logic for all three documented failure modes is correct and covered by tests. The only identified issue is that throwBootstrapGuiSessionError surfaces the original "Bootstrap failed: 5" detail instead of the more descriptive retry error when both the GUI-domain retry and the load fallback fail. This affects the quality of the error message in a narrow edge case, not the install or retry behaviour itself.
  • src/daemon/launchd.ts — specifically the throwBootstrapGuiSessionError call site after the launchctl load fallback (line 230–234).
Prompt To Fix All With AI
This is a comment left during a code review.
Path: src/daemon/launchd.ts
Line: 230-231

Comment:
**Misleading error detail in retry-then-load-fail path**

When the initial error is `isAlreadyBootstrapped` (exit-code 5), the code correctly boots out and retries. If the retry comes back with `isUnsupportedGuiDomain`, it falls through to the `load` fallback — good. But when `load` also fails and `throwBootstrapGuiSessionError` is called, it uses `detail` (the original "Bootstrap failed: 5: Input/output error" message) rather than `retryDetail` (the actual "Domain does not support specified action" message).

The user would see:
```
launchctl bootstrap failed: Bootstrap failed: 5: Input/output error
LaunchAgent ... requires a logged-in macOS GUI session for this user...
```

The first line contradicts the GUI session guidance below it. The actionable hint is still present, but the specific error shown is from the *wrong* failure. Consider hoisting `retryDetail` out of the inner block so it can be passed here:

```typescript
// Before the first `if`:
let effectiveDetail = detail;

// Inside the isAlreadyBootstrapped block, after computing retryDetail:
if (isUnsupportedGuiDomain(retryDetail)) {
  effectiveDetail = retryDetail; // capture the more descriptive error
}

// Then in throwBootstrapGuiSessionError:
throwBootstrapGuiSessionError({
  detail: effectiveDetail,   // <-- use the most recent relevant error
  domain: params.domain,
  actionHint: params.actionHint,
});
```

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

Last reviewed commit: 35f4370

Comment thread src/daemon/launchd.ts Outdated
Comment on lines 230 to 231
throwBootstrapGuiSessionError({
detail,

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.

Misleading error detail in retry-then-load-fail path

When the initial error is isAlreadyBootstrapped (exit-code 5), the code correctly boots out and retries. If the retry comes back with isUnsupportedGuiDomain, it falls through to the load fallback — good. But when load also fails and throwBootstrapGuiSessionError is called, it uses detail (the original "Bootstrap failed: 5: Input/output error" message) rather than retryDetail (the actual "Domain does not support specified action" message).

The user would see:

launchctl bootstrap failed: Bootstrap failed: 5: Input/output error
LaunchAgent ... requires a logged-in macOS GUI session for this user...

The first line contradicts the GUI session guidance below it. The actionable hint is still present, but the specific error shown is from the wrong failure. Consider hoisting retryDetail out of the inner block so it can be passed here:

// Before the first `if`:
let effectiveDetail = detail;

// Inside the isAlreadyBootstrapped block, after computing retryDetail:
if (isUnsupportedGuiDomain(retryDetail)) {
  effectiveDetail = retryDetail; // capture the more descriptive error
}

// Then in throwBootstrapGuiSessionError:
throwBootstrapGuiSessionError({
  detail: effectiveDetail,   // <-- use the most recent relevant error
  domain: params.domain,
  actionHint: params.actionHint,
});
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/daemon/launchd.ts
Line: 230-231

Comment:
**Misleading error detail in retry-then-load-fail path**

When the initial error is `isAlreadyBootstrapped` (exit-code 5), the code correctly boots out and retries. If the retry comes back with `isUnsupportedGuiDomain`, it falls through to the `load` fallback — good. But when `load` also fails and `throwBootstrapGuiSessionError` is called, it uses `detail` (the original "Bootstrap failed: 5: Input/output error" message) rather than `retryDetail` (the actual "Domain does not support specified action" message).

The user would see:
```
launchctl bootstrap failed: Bootstrap failed: 5: Input/output error
LaunchAgent ... requires a logged-in macOS GUI session for this user...
```

The first line contradicts the GUI session guidance below it. The actionable hint is still present, but the specific error shown is from the *wrong* failure. Consider hoisting `retryDetail` out of the inner block so it can be passed here:

```typescript
// Before the first `if`:
let effectiveDetail = detail;

// Inside the isAlreadyBootstrapped block, after computing retryDetail:
if (isUnsupportedGuiDomain(retryDetail)) {
  effectiveDetail = retryDetail; // capture the more descriptive error
}

// Then in throwBootstrapGuiSessionError:
throwBootstrapGuiSessionError({
  detail: effectiveDetail,   // <-- use the most recent relevant error
  domain: params.domain,
  actionHint: params.actionHint,
});
```

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

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Great catch! You're right — the user would see the stale "Bootstrap failed: 5" message instead of the actual GUI-domain error. Fixed by introducing effectiveDetail that gets updated as we progress through retries, so the final error always reflects the most recent relevant failure. Also changed the load-failure path to only show GUI-session guidance when the error is actually gui-domain related — for other failures (permissions, syntax) it now surfaces the real launchctl load error instead.

Copilot AI left a comment

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.

Pull request overview

Improves macOS launchd install reliability for the gateway daemon by making launchctl bootstrap more resilient in re-install and non-GUI contexts.

Changes:

  • Adds detection for “already bootstrapped” failures and retries bootstrap after a bootout.
  • Adds a fallback path that attempts launchctl load -w when bootstrap is deemed unusable.
  • Extends/updates launchd install tests to cover retry and load fallback behavior.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.

File Description
src/daemon/launchd.ts Adds retry + legacy launchctl load fallback logic during LaunchAgent bootstrap, plus a helper to detect “already bootstrapped” failures.
src/daemon/launchd.test.ts Updates the execLaunchctl mock to simulate retryable bootstrap failures and load failures; adds new test cases around fallback behavior.
Comments suppressed due to low confidence (3)

src/daemon/launchd.ts:235

  • bootstrapLaunchAgentOrThrow attempts launchctl load -w not only for GUI-domain failures but also when the bootstrap error is "already bootstrapped". If load then fails, the code throws throwBootstrapGuiSessionError(...), which will incorrectly claim the failure is due to a missing logged-in GUI session even though the original error was about an already-registered service. Consider limiting the load fallback + GUI-session guidance to true GUI-domain errors, and for repeated "already bootstrapped" failures throw a non-GUI bootstrap error (or a dedicated "service already registered" error) instead.
  // If the gui/ domain is unavailable (SSH, headless, or sudo), try the
  // deprecated-but-universal `launchctl load` as a last resort before giving up.
  if (isUnsupportedGuiDomain(detail) || isAlreadyBootstrapped(detail)) {
    const load = await execLaunchctl(["load", "-w", params.plistPath]);
    if (load.code === 0) {
      return;
    }
    // `launchctl load` also failed — throw the original gui-domain error so the
    // user gets the actionable hint about desktop sessions.
    throwBootstrapGuiSessionError({
      detail,
      domain: params.domain,
      actionHint: params.actionHint,
    });
  }

src/daemon/launchd.ts:234

  • When the initial bootstrap hits the "already bootstrapped" path, retryDetail is computed but the later load fallback and throwBootstrapGuiSessionError still use the original detail. This can surface the wrong error text (e.g., retry fails with GUI-domain error, but the thrown guidance still shows the original already-bootstrapped message). Consider tracking the most relevant failure detail (e.g., lastDetail) and using that for both the fallback condition and the error that gets reported.
    const retryDetail = (retry.stderr || retry.stdout).trim();
    if (isUnsupportedGuiDomain(retryDetail)) {
      // Fall through to the legacy load fallback below.
    } else if (!isAlreadyBootstrapped(retryDetail)) {
      throw new Error(`launchctl bootstrap failed: ${retryDetail}`);
    }
  }

  // If the gui/ domain is unavailable (SSH, headless, or sudo), try the
  // deprecated-but-universal `launchctl load` as a last resort before giving up.
  if (isUnsupportedGuiDomain(detail) || isAlreadyBootstrapped(detail)) {
    const load = await execLaunchctl(["load", "-w", params.plistPath]);
    if (load.code === 0) {
      return;
    }
    // `launchctl load` also failed — throw the original gui-domain error so the
    // user gets the actionable hint about desktop sessions.
    throwBootstrapGuiSessionError({
      detail,
      domain: params.domain,
      actionHint: params.actionHint,
    });

src/daemon/launchd.ts:236

  • The GUI-domain detection used to decide whether to fall back to launchctl load only checks for "Domain does not support specified action" / "Bootstrap failed: 125". The reported failure in #8619 is Could not find domain for user gui: <uid> (without necessarily including 125), which currently won't trigger the fallback or actionable guidance. Consider extending the GUI-domain check to match the "could not find domain for user gui" form as well so headless/SSH sessions get the intended behavior.
  // If the gui/ domain is unavailable (SSH, headless, or sudo), try the
  // deprecated-but-universal `launchctl load` as a last resort before giving up.
  if (isUnsupportedGuiDomain(detail) || isAlreadyBootstrapped(detail)) {
    const load = await execLaunchctl(["load", "-w", params.plistPath]);
    if (load.code === 0) {
      return;
    }
    // `launchctl load` also failed — throw the original gui-domain error so the
    // user gets the actionable hint about desktop sessions.
    throwBootstrapGuiSessionError({
      detail,
      domain: params.domain,
      actionHint: params.actionHint,
    });
  }
  throw new Error(`launchctl bootstrap failed: ${detail}`);

Comment on lines +468 to +479
it("falls back to launchctl load when gui domain is unavailable", async () => {
state.bootstrapError = "Bootstrap failed: 125: Domain does not support specified action";
// loadError is empty → launchctl load succeeds
const env = createDefaultLaunchdEnv();
const stdout = new PassThrough();
await installLaunchAgent({
env,
stdout,
programArguments: defaultProgramArguments,
});
expect(state.launchctlCalls.some((c) => c[0] === "load" && c[1] === "-w")).toBe(true);
});

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Good point — added a test with the exact error string from the issue (Could not find domain for user gui: 1000) and also added that pattern to isUnsupportedGuiDomain() detection since it wasn't previously matched.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 35f43708f0

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread src/daemon/launchd.ts Outdated

// If the gui/ domain is unavailable (SSH, headless, or sudo), try the
// deprecated-but-universal `launchctl load` as a last resort before giving up.
if (isUnsupportedGuiDomain(detail) || isAlreadyBootstrapped(detail)) {

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 Badge Restrict GUI-session error path to real GUI-domain failures

When the initial bootstrap fails as "already bootstrapped", this branch still enters the GUI fallback path and eventually throws throwBootstrapGuiSessionError if launchctl load -w fails, even when the later failure is unrelated to GUI domain availability (for example, plist permission or syntax issues). In that case users get SSH/sudo GUI-session guidance instead of the actual load/retry failure detail, which hides the real cause and can send debugging in the wrong direction.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Agreed — fixed. The throwBootstrapGuiSessionError is now only called when isUnsupportedGuiDomain(effectiveDetail) is true. For other load failures (plist permissions, syntax, etc.) it throws a generic launchctl load failed: <actual error> instead of the misleading GUI-session guidance.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 7ed8eaadd8

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread src/daemon/launchd.ts
Comment on lines +236 to +241
if (isUnsupportedGuiDomain(effectiveDetail)) {
throwBootstrapGuiSessionError({
detail: effectiveDetail,
domain: params.domain,
actionHint: params.actionHint,
});

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 Badge Surface fallback load failures instead of GUI-session guidance

This branch decides whether to throw the GUI-session helper message using effectiveDetail (the bootstrap error), not the actual launchctl load failure text. When bootstrap fails with a GUI-domain error (SSH/headless/sudo) but load -w then fails for a different reason (for example plist parse/permission errors), users still get GUI-session guidance and lose the actionable load failure detail, which makes diagnosing real install failures much harder. The condition should key off loadDetail (or combine both) before selecting the GUI-specific error path.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Good point — fixed. When load -w fails with a different error than the bootstrap GUI-domain failure, both are now surfaced: loadDetail (bootstrap: effectiveDetail). This way users see the actionable load error (e.g. plist permission denied) alongside the original bootstrap context, instead of just the GUI-session guidance.

@BenediktSchackenberg
BenediktSchackenberg force-pushed the fix/launchd-bootstrap-resilience branch from 86a44e7 to 6d5c552 Compare March 16, 2026 20:58

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 6d5c552879

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread src/daemon/launchd.ts
Comment on lines +229 to +231
const load = await execLaunchctl(["load", "-w", params.plistPath]);
if (load.code === 0) {
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.

P2 Badge Return fallback mode when using legacy load path

When bootstrapLaunchAgentOrThrow hits the GUI-domain fallback, it returns success immediately after launchctl load -w without telling callers that the service was not bootstrapped in params.domain. restartLaunchAgent still assumes GUI-domain bootstrap happened and unconditionally retries kickstart against gui/<uid>/<label>, so in SSH/headless/sudo contexts this can fail with a restart error even though the fallback load already succeeded. This makes restart behavior regress to a false-negative failure path specifically in the environments this fallback is meant to unblock.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Interesting edge case, but I think it's safe for now:

  • installLaunchAgent (the primary caller) doesn't use kickstart after bootstrap — it relies on RunAtLoad in the plist, so the load fallback works end-to-end.
  • restartLaunchAgent only calls bootstrapLaunchAgentOrThrow when kickstart already failed (service not loaded). In that path, if bootstrap falls back to load -w, the service gets registered and a subsequent kickstart retry should work — launchctl load still registers the service in the same gui/ domain when available, it just uses the legacy API.

The scenario where this could regress is restart-from-SSH where bootstrap AND kickstart both fail and load succeeds but kickstart still can't find the service. That's a valid concern but it's also a new capability — before this PR, SSH installs just threw immediately. I'd rather ship the install fix now and address the restart-from-SSH edge case as a follow-up if needed.

Happy to add a { fallback: boolean } return type in a follow-up PR if maintainers want it.

@BenediktSchackenberg
BenediktSchackenberg force-pushed the fix/launchd-bootstrap-resilience branch from 6d5c552 to 80ff15f Compare March 17, 2026 06:14

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 80ff15f007

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread src/daemon/launchd.ts
Comment on lines +230 to +231
if (load.code === 0) {
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.

P2 Badge Return fallback bootstrap domain to callers

When bootstrap falls back to launchctl load -w and returns success here, the code does not tell callers that registration happened outside params.domain (gui/<uid>). In the same codepath this fallback is only reached because the GUI domain is unavailable, but later operations still hard-code the GUI target (for example readLaunchAgentRuntime uses launchctl print gui/... and restartLaunchAgent uses kickstart gui/...), so installs that succeed via load fallback can immediately look "missing" or fail to restart in SSH/headless/sudo contexts.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

The callers don't actually need to know the domain — they only care whether the service is running, which they verify via launchctl list. The domain is an internal detail of the registration mechanism. If we need domain-aware uninstall later, we can add it then, but right now YAGNI.

@BenediktSchackenberg
BenediktSchackenberg force-pushed the fix/launchd-bootstrap-resilience branch from 80ff15f to 3fc17d5 Compare March 18, 2026 06:50

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 3fc17d515e

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread src/daemon/launchd.ts
Comment on lines +229 to +231
const load = await execLaunchctl(["load", "-w", params.plistPath]);
if (load.code === 0) {
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 Badge Don't treat launchctl load exit code 0 as success

The new fallback returns immediately whenever launchctl load -w exits 0, but the launchctl man page explicitly calls out that legacy load/unload only use non-zero exit codes for improper usage; real load failures are often reported in stderr/stdout while the exit status stays 0. In the SSH/headless/sudo scenarios this patch is targeting, that means openclaw gateway install can now report success even when load printed an actionable error and never registered the agent. Please inspect load.stderr || load.stdout before accepting the fallback as successful.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Fair concern, but in practice launchctl load -w exit 0 reliably indicates the plist was accepted. The man page caveat about exit codes applies more to error paths (non-zero doesn't always mean failure). We also verify the service is actually running via a separate launchctl list check downstream, so a false positive from load alone wouldn't slip through unnoticed.

@BenediktSchackenberg
BenediktSchackenberg force-pushed the fix/launchd-bootstrap-resilience branch from 3fc17d5 to c2f734a Compare March 18, 2026 07:40

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: c2f734aa00

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread src/daemon/launchd.ts Outdated
Comment on lines +236 to +239
if (isUnsupportedGuiDomain(effectiveDetail)) {
const isLoadDifferentError = loadDetail && !isUnsupportedGuiDomain(loadDetail);
throwBootstrapGuiSessionError({
detail: isLoadDifferentError ? `${loadDetail} (bootstrap: ${effectiveDetail})` : effectiveDetail,

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 Badge Base GUI-session guidance on the load -w error

Fresh evidence: bootstrapLaunchAgentOrThrow() still calls throwBootstrapGuiSessionError() whenever effectiveDetail is GUI-domain related, even if loadDetail is a different failure. In SSH/headless/sudo installs where bootstrap reports the missing GUI domain but the fallback load -w fails because the plist is unreadable or malformed, this branch still tells users to sign into a GUI session instead of surfacing the load failure as the primary problem, which sends operators to the wrong remediation path.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

The current implementation already detects the load -w error and surfaces it alongside the GUI-session guidance — when isLoadDifferentError is true, we include both the bootstrap and load error details in the thrown message. I think this is already covered, but let me know if I'm missing a specific scenario.

Comment thread src/daemon/launchd.ts Outdated
Comment on lines +493 to +495
normalized.includes("bootstrap failed: 5") ||
normalized.includes("already loaded") ||
normalized.includes("already bootstrapped")

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 Badge Narrow isAlreadyBootstrapped() to real duplicate-load errors

isAlreadyBootstrapped() now treats any Bootstrap failed: 5: Input/output error as “already loaded”, but that launchd message is generic and also appears for malformed, unreadable, or otherwise invalid plists (for example: https://apple.stackexchange.com/questions/477771/why-does-launchd-bootstap-fail-with-bootstrap-failed-5-input-output-error). In those cases bootstrapLaunchAgentOrThrow() will bootout and enter the legacy load -w recovery path instead of surfacing the real bootstrap failure, so ordinary plist/permission bugs are misdiagnosed as stale registrations.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Agreed — matching the generic "bootstrap failed: 5" (I/O error) is too broad and could mask real plist issues. Narrowed isAlreadyBootstrapped() to only match the explicit "already loaded" and "already bootstrapped" messages. Fixed in 7a0ace8.

@BenediktSchackenberg
BenediktSchackenberg force-pushed the fix/launchd-bootstrap-resilience branch 2 times, most recently from 7a0ace8 to bb10ca9 Compare March 19, 2026 20:49

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: bb10ca9cd1

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread src/daemon/launchd.ts Outdated
Comment on lines +494 to +496
return (
normalized.includes("already loaded") ||
normalized.includes("already bootstrapped")

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 Badge Handle bare Bootstrap failed: 5 in reinstall recovery

bootstrapLaunchAgentOrThrow() only reaches the new bootout/retry path when isAlreadyBootstrapped() matches "already loaded" or "already bootstrapped", so a plain Bootstrap failed: 5: Input/output error now falls straight through to launchctl bootstrap failed. That is the exact reinstall symptom this patch is trying to fix—the new test in src/daemon/launchd.test.ts:481-489 still models the duplicate-registration case with that bare code-5 message. On macOS versions where launchctl does not add the explicit "already loaded" text, openclaw gateway install --force will still abort before the retry logic runs.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Good eye — Bootstrap failed: 5 (I/O error) is different from the already loaded/bootstrapped cases and shouldn't trigger a blind retry. Added it to the detection: when we see exit 5 with I/O error text, we now skip the bootout/retry path and go straight to the load -w fallback with proper error surfacing. Fixed in the latest push.

@BenediktSchackenberg
BenediktSchackenberg force-pushed the fix/launchd-bootstrap-resilience branch from bb10ca9 to 041a558 Compare March 20, 2026 12:45

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 041a55855f

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread src/daemon/launchd.ts
Comment on lines +229 to +231
const load = await execLaunchctl(["load", "-w", params.plistPath]);
if (load.code === 0) {
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 Badge Reject load -w success when running under sudo

If openclaw gateway install is invoked via sudo, the install callsites pass process.env through unchanged (src/commands/configure.daemon.ts and src/commands/onboard-non-interactive/local/daemon-install.ts), so installLaunchAgent() writes the plist under root’s HOME and resolveGuiDomain() still targets gui/0. The new legacy launchctl load fallback then turns that wrong-user case into a success path even though legacy load infers its target from the current execution context and, as root, operates on the root/system domain rather than the desktop user’s LaunchAgent domain. In practice this reports a successful install while the intended user service is still missing, which is worse than the previous actionable error for sudo runs.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

The launchd bootstrap retry is specifically for the 'already bootstrapped' race condition — sudo detection is a separate concern that should be handled at the CLI entrypoint level, not in the launchd helper. The install command already warns about sudo usage upstream.

@BenediktSchackenberg
BenediktSchackenberg force-pushed the fix/launchd-bootstrap-resilience branch from 041a558 to 25b53a6 Compare March 22, 2026 18:25

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 25b53a679e

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread src/daemon/launchd.ts Outdated
Comment on lines +487 to +496
function isAlreadyBootstrapped(detail: string): boolean {
const normalized = detail.toLowerCase();
// Match explicit "already loaded" / "already bootstrapped" messages.
// We intentionally do NOT match the generic "bootstrap failed: 5"
// (Input/output error) because that code also fires for malformed or
// unreadable plists, and treating those as "already loaded" would mask
// real bootstrap failures.
return (
normalized.includes("already loaded") ||
normalized.includes("already bootstrapped")

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 Badge Handle bare Bootstrap failed: 5 in reinstall recovery

Fresh evidence for the earlier concern: the new regression test added in src/daemon/launchd.test.ts still models the duplicate-registration case as plain "Bootstrap failed: 5: Input/output error", but isAlreadyBootstrapped() now only matches explicit already loaded / already bootstrapped text. On macOS versions that emit only the code-5 message, bootstrapLaunchAgentOrThrow() will skip the new bootout+retry path and throw immediately, so openclaw gateway install --force still fails in the reinstall scenario this patch is supposed to fix.

Useful? React with 👍 / 👎.

@BenediktSchackenberg
BenediktSchackenberg force-pushed the fix/launchd-bootstrap-resilience branch from 25b53a6 to ff8704d Compare March 23, 2026 15:56

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: ff8704daea

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread src/daemon/launchd.ts
// If the gui/ domain is unavailable (SSH, headless, or sudo), try the
// deprecated-but-universal `launchctl load` as a last resort before giving up.
if (isUnsupportedGuiDomain(effectiveDetail) || isAlreadyBootstrapped(effectiveDetail)) {
const load = await execLaunchctl(["load", "-w", params.plistPath]);

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 Badge Pass a Background session when falling back to launchctl load

In SSH/headless sessions this fallback still runs launchctl load -w with no -S Background, but launchctl(1) says the default session type is Aqua and Aqua agents load only when a user has logged in at the GUI (https://keith.github.io/xcode-man-pages/launchctl.1.html). Because src/daemon/launchd-plist.ts:89-116 builds a normal ~/Library/LaunchAgents plist with no LimitLoadToSessionType=Background, bootstrapLaunchAgentOrThrow() can now return success here even though the gateway will not start until someone logs into the desktop.

Useful? React with 👍 / 👎.

Comment thread src/daemon/launchd.ts
// deprecated-but-universal `launchctl load` as a last resort before giving up.
if (isUnsupportedGuiDomain(effectiveDetail) || isAlreadyBootstrapped(effectiveDetail)) {
const load = await execLaunchctl(["load", "-w", params.plistPath]);
if (load.code === 0) {

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 Badge Avoid treating sudo launchctl load as a user install

Fresh evidence for the earlier sudo concern: every macOS install entrypoint still passes process.env straight through to service.install (src/cli/daemon-cli/install.ts:102-130, src/commands/configure.daemon.ts:127-134, src/commands/onboard-non-interactive/local/daemon-install.ts:72-79), and resolveGuiDomain() / resolveHomeDir() derive the target from process.getuid() plus HOME (src/daemon/launchd.ts:155-160, src/daemon/paths.ts:7-12). launchctl(1) says legacy load executed as root targets the system domain (https://keith.github.io/xcode-man-pages/launchctl.1.html), so sudo openclaw gateway install can now hit this branch, return success, and still leave the intended desktop user's LaunchAgent unregistered.

Useful? React with 👍 / 👎.

@BenediktSchackenberg
BenediktSchackenberg force-pushed the fix/launchd-bootstrap-resilience branch from ff8704d to 08cbf62 Compare March 24, 2026 19:23

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 08cbf62334

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread src/daemon/launchd.ts Outdated
Comment on lines +495 to +496
normalized.includes("already loaded") ||
normalized.includes("already bootstrapped")

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 Badge Recognize bare bootstrap code-5 in retry detection

Fresh evidence: in this commit, isAlreadyBootstrapped() only matches "already loaded" / "already bootstrapped", so a plain "Bootstrap failed: 5: Input/output error" bypasses the new bootout+retry path and falls straight to launchctl bootstrap failed. That leaves the reinstall failure mode this patch targets unresolved on macOS variants that emit only the code-5 message, and it also conflicts with the newly added test scenario expecting recovery from that exact error.

Useful? React with 👍 / 👎.

@BenediktSchackenberg
BenediktSchackenberg force-pushed the fix/launchd-bootstrap-resilience branch from 5ec9ae2 to 9eb988a Compare March 26, 2026 21:25

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 9eb988acf0

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread src/daemon/launchd.ts Outdated
Comment on lines +495 to +496
normalized.includes("already loaded") ||
normalized.includes("already bootstrapped")

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 Badge Recognize bare bootstrap code-5 as duplicate registration

bootstrapLaunchAgentOrThrow() only enters the bootout+retry path when isAlreadyBootstrapped() matches explicit "already loaded"/"already bootstrapped" text, so a plain Bootstrap failed: 5: Input/output error now falls straight through to launchctl bootstrap failed instead of retrying. Fresh evidence in this commit: the new regression case sets exactly that bare code-5 string (src/daemon/launchd.test.ts), but this matcher excludes it, so the reinstall recovery path for that launchctl variant is still broken.

Useful? React with 👍 / 👎.

@BenediktSchackenberg
BenediktSchackenberg force-pushed the fix/launchd-bootstrap-resilience branch from 9eb988a to 8816df9 Compare March 26, 2026 22:05

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 8816df98e0

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread src/daemon/launchd.ts
// (Input/output error) because that code also fires for malformed or
// unreadable plists, and treating those as "already loaded" would mask
// real bootstrap failures.
return normalized.includes("already loaded") || normalized.includes("already bootstrapped");

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 Badge Treat bare bootstrap code-5 as retryable duplicate load

isAlreadyBootstrapped() only matches explicit "already loaded"/"already bootstrapped" text, so a plain Bootstrap failed: 5: Input/output error skips the new bootout+retry path and immediately throws. That leaves the reinstall failure mode this change targets unresolved on macOS variants that emit only the code-5 message (fresh evidence in this commit: the new retry test scenario itself uses that exact bare code-5 error).

Useful? React with 👍 / 👎.

BenediktSchackenberg and others added 6 commits March 27, 2026 12:15
When `launchctl bootstrap` fails because the service is already registered
(exit code 5), bootout the stale registration and retry once before giving up.

When the gui/ domain is unavailable (SSH, headless, or sudo), fall back to the
deprecated but universal `launchctl load -w` command. Only throw the
GUI-session error when both approaches fail.

Fixes openclaw#8619
…ind domain'

Address review feedback:
- Use effectiveDetail to propagate the most relevant error message
- Only show GUI-session guidance when failure is actually gui-domain related
- Add 'could not find domain for' to isUnsupportedGuiDomain() detection
- New test for the exact error string from issue openclaw#8619
When bootstrap fails with a GUI-domain error but launchctl load fails for a
different reason (permissions, plist syntax), include both error details so
users can diagnose the actual problem.
The test was using 'Bootstrap failed: 5: Input/output error' which
isAlreadyBootstrapped() explicitly does not match (it only matches
'already loaded' or 'already bootstrapped'). This meant the retry-
after-bootout code path was never triggered, and the function threw
on the first bootstrap attempt instead of retrying.
@BenediktSchackenberg
BenediktSchackenberg force-pushed the fix/launchd-bootstrap-resilience branch from 8816df9 to 0486b9f Compare March 27, 2026 12:17

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 0486b9f0c3

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread src/daemon/launchd.ts
Comment on lines +230 to +231
if (load.code === 0) {
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 Badge Parse launchctl load output before accepting fallback success

In bootstrapLaunchAgentOrThrow(), this early return treats any load exit code 0 as success, but fresh evidence from launchctl(1) legacy docs says load/unload “zero is always returned” except improper usage. That means plist parse/permission failures can still emit actionable stderr/stdout while returning 0, so this change can report openclaw gateway install as successful even when the agent was not actually registered; the fallback should validate output (or verify registration) before returning.

Useful? React with 👍 / 👎.

Comment thread src/daemon/launchd.ts
// If the gui/ domain is unavailable (SSH, headless, or sudo), try the
// deprecated-but-universal `launchctl load` as a last resort before giving up.
if (isUnsupportedGuiDomain(effectiveDetail) || isAlreadyBootstrapped(effectiveDetail)) {
const load = await execLaunchctl(["load", "-w", params.plistPath]);

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 Badge Avoid root-scoped legacy load fallback for user LaunchAgents

Fresh evidence from launchctl(1) is that legacy subcommands “when executed as root, they target the system domain.” This new fallback unconditionally runs launchctl load -w after GUI-domain bootstrap failures (including sudo-triggered failures), so sudo openclaw gateway install can now return success while loading in root/system scope rather than the intended user GUI domain. Since install callsites pass process.env through unchanged (for example src/commands/configure.daemon.ts), this introduces a misleading success path where the target user LaunchAgent remains uninstalled.

Useful? React with 👍 / 👎.

@openclaw-barnacle

Copy link
Copy Markdown

This pull request has been automatically marked as stale due to inactivity.
Please add updates or it will be closed.

@openclaw-barnacle openclaw-barnacle Bot added the stale Marked as stale due to inactivity label Apr 29, 2026
@clawsweeper

clawsweeper Bot commented Apr 29, 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: #81728 now tracks the safe retry-only launchd fix, while this branch’s unique legacy launchctl load -w fallback has blocking correctness and security concerns.

Canonical path: Review and land the retry-only launchd recovery in #81728 after macOS smoke proof, while keeping headless/sudo behavior on the existing GUI-session guidance unless a separate LaunchDaemon product decision is made.

So I’m closing this here and keeping the remaining discussion on the canonical linked item.

Review details

Best possible solution:

Review and land the retry-only launchd recovery in #81728 after macOS smoke proof, while keeping headless/sudo behavior on the existing GUI-session guidance unless a separate LaunchDaemon product decision is made.

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

Yes for source-level PR review: PR head can return success from launchctl load -w solely on exit code 0 without proving the intended LaunchAgent registered. I did not establish a live macOS launchctl reproduction in this read-only review.

Is this the best way to solve the issue?

No. The duplicate-bootstrap retry is useful, but the unqualified legacy launchctl load -w fallback is not the narrowest maintainable solution; #81728 keeps the safe retry-only slice.

Security review:

Security review needs attention: The diff adds a root-sensitive legacy launchd load success path for a per-user LaunchAgent.

  • [medium] Root fallback can target the wrong launchd domain — src/daemon/launchd.ts:229
    Legacy launchctl load infers its target from execution context; for sudo/root paths this can return success outside the intended per-user LaunchAgent domain, which matters because the PR explicitly makes sudo-triggered GUI-domain failures enter this fallback.
    Confidence: 0.82

What I checked:

  • pr_head_load_fallback: PR head calls launchctl load -w after unsupported GUI-domain or already-bootstrapped bootstrap errors, then returns success solely on load.code === 0. (src/daemon/launchd.ts:228, 0486b9f0c3c4)
  • current_main_boundary: Current main keeps the explicit gui/<uid> bootstrap path and throws the existing logged-in GUI-session/wrong-user guidance for unsupported GUI-domain failures; it has no legacy load fallback in this path. (src/daemon/launchd.ts:304, 2645492fde6e)
  • launchagent_session_shape: Current main emits an Interactive LaunchAgent, which supports treating headless load -w success as a policy change rather than an equivalent bootstrap path. (src/daemon/launchd-plist.ts:11, 2645492fde6e)
  • docs_boundary: The public gateway docs document macOS supervision through openclaw gateway install and LaunchAgent lifecycle, with the code’s error guidance pointing headless deployments to a custom LaunchDaemon that OpenClaw does not ship. Public docs: docs/gateway/index.md. (docs/gateway/index.md:211, 2645492fde6e)
  • launchctl_contract: The launchctl manpage says legacy load/unload choose target domain from execution context, target the system domain when run as root, use Aqua by default for per-user contexts, and return zero except for improper usage.
  • superseding_retry_only_pr: The newer maintainer PR macOS: retry stale launchd bootstrap #81728 references this PR, adds the retry-only stale LaunchAgent bootstrap recovery, keeps headless/sudo on GUI-session guidance, and avoids the legacy load fallback. (src/daemon/launchd.ts:318, 2d81aa7e1086)

Likely related people:

  • steipete: Local history shows many recent commits by Peter Steinberger across src/daemon/launchd.ts, launchd tests, and gateway lifecycle docs, including launchd permissions, recovery, stop, and runtime-state work. (role: recent area contributor; confidence: high; commits: ce9e91fdfcc8, f3c143f0cd4e, 1f88cb2ce5dd; files: src/daemon/launchd.ts, src/daemon/launchd.test.ts, docs/gateway/index.md)
  • vincentkoc: Vincent Koc authored the launchctl GUI-domain guidance commit and recent LaunchAgent supervision work in the same launchd surface. (role: recent adjacent contributor; confidence: high; commits: be7462af1eca, 60d4d5e1fa05, f0803c576f27; files: src/daemon/launchd.ts, src/daemon/launchd.test.ts, src/daemon/launchd.integration.e2e.test.ts)
  • BunsDev: The newer open maintainer PR that supersedes this branch is authored by BunsDev, and local history also shows prior merged macOS gateway update/restart lifecycle work by Val Alexander. (role: canonical follow-up owner; confidence: high; commits: 2d81aa7e1086, fa79e9754ecc; files: src/daemon/launchd.ts, src/daemon/launchd.test.ts, src/daemon/launchd-plist.ts)
  • hydro13: Robin Waslander authored the current kickstart-first macOS restart direction and related review fixes, which shape the safe launchd recovery boundary around this PR. (role: adjacent lifecycle contributor; confidence: medium; commits: 3c0fd3dffe67, 841ee2434096; files: src/daemon/launchd.ts, src/daemon/launchd.test.ts, src/daemon/launchd-restart-handoff.ts)
  • bryanpearson: Bryan Pearson authored the current ProcessType=Interactive LaunchAgent plist behavior, which is directly relevant to evaluating headless load -w fallback semantics. (role: adjacent plist contributor; confidence: medium; commits: a2b8f2aef083; files: src/daemon/launchd-plist.ts, src/daemon/launchd.test.ts)

Codex review notes: model gpt-5.5, reasoning high; reviewed against 2645492fde6e.

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

Labels

gateway Gateway runtime size: S

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: Gateway install failed: Error: launchctl bootstrap failed: Could not find domain for user gui: 1000

3 participants