Skip to content

Commit 44beb7b

Browse files
scoootscooobclaude
authored andcommitted
fix(daemon): also enable LaunchAgent in repairLaunchAgentBootstrap
The repair/recovery path had the same missing `enable` guard as `restartLaunchAgent`. If launchd persists a "disabled" state after a previous `bootout`, the `bootstrap` call in `repairLaunchAgentBootstrap` fails silently, leaving the gateway unloaded in the recovery flow. Add the same `enable` guard before `bootstrap` that was already applied to `installLaunchAgent` and (in this PR) `restartLaunchAgent`. Co-Authored-By: Claude Opus 4.6 <[email protected]>
1 parent 69cd376 commit 44beb7b

File tree

2 files changed

+20
-3
lines changed

2 files changed

+20
-3
lines changed

src/daemon/launchd.test.ts

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -156,7 +156,7 @@ describe("launchctl list detection", () => {
156156
});
157157

158158
describe("launchd bootstrap repair", () => {
159-
it("bootstraps and kickstarts the resolved label", async () => {
159+
it("enables, bootstraps, and kickstarts the resolved label", async () => {
160160
const env: Record<string, string | undefined> = {
161161
HOME: "/Users/test",
162162
OPENCLAW_PROFILE: "default",
@@ -167,9 +167,23 @@ describe("launchd bootstrap repair", () => {
167167
const domain = typeof process.getuid === "function" ? `gui/${process.getuid()}` : "gui/501";
168168
const label = "ai.openclaw.gateway";
169169
const plistPath = resolveLaunchAgentPlistPath(env);
170+
const serviceId = `${domain}/${label}`;
171+
172+
const enableIndex = state.launchctlCalls.findIndex(
173+
(c) => c[0] === "enable" && c[1] === serviceId,
174+
);
175+
const bootstrapIndex = state.launchctlCalls.findIndex(
176+
(c) => c[0] === "bootstrap" && c[1] === domain && c[2] === plistPath,
177+
);
178+
const kickstartIndex = state.launchctlCalls.findIndex(
179+
(c) => c[0] === "kickstart" && c[1] === "-k" && c[2] === serviceId,
180+
);
170181

171-
expect(state.launchctlCalls).toContainEqual(["bootstrap", domain, plistPath]);
172-
expect(state.launchctlCalls).toContainEqual(["kickstart", "-k", `${domain}/${label}`]);
182+
expect(enableIndex).toBeGreaterThanOrEqual(0);
183+
expect(bootstrapIndex).toBeGreaterThanOrEqual(0);
184+
expect(kickstartIndex).toBeGreaterThanOrEqual(0);
185+
expect(enableIndex).toBeLessThan(bootstrapIndex);
186+
expect(bootstrapIndex).toBeLessThan(kickstartIndex);
173187
});
174188
});
175189

src/daemon/launchd.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,9 @@ export async function repairLaunchAgentBootstrap(args: {
207207
const domain = resolveGuiDomain();
208208
const label = resolveLaunchAgentLabel({ env });
209209
const plistPath = resolveLaunchAgentPlistPath(env);
210+
// launchd can persist "disabled" state after bootout; clear it before bootstrap
211+
// (matches the same guard in installLaunchAgent and restartLaunchAgent).
212+
await execLaunchctl(["enable", `${domain}/${label}`]);
210213
const boot = await execLaunchctl(["bootstrap", domain, plistPath]);
211214
if (boot.code !== 0) {
212215
return { ok: false, detail: (boot.stderr || boot.stdout).trim() || undefined };

0 commit comments

Comments
 (0)