fix(daemon): bootstrap stopped service on gateway start

After `gateway stop` (which runs `launchctl bootout`), `gateway start`
checks `isLoaded` → false → prints "not loaded" hints and exits.
The service is never re-bootstrapped, so `start` cannot recover from
`stop` — only `gateway install` works.

Root cause: src/cli/daemon-cli/lifecycle-core.ts:208-217 — runServiceStart
calls handleServiceNotLoaded which only prints hints, never attempts
service.restart() (which already handles bootstrap via
bootstrapLaunchAgentOrThrow at launchd.ts:598).

Fix: when service is not loaded, attempt service.restart() first (which
handles re-bootstrapping on all platforms). If restart fails (e.g. plist
was deleted, not just booted out), fall back to the existing hints.

The restart path is already proven: restartLaunchAgent (launchd.ts:556)
handles "not loaded" via bootstrapLaunchAgentOrThrow. This fix routes
the start command through the same recovery path.

Closes #53878

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: HCL <chenglunhu@gmail.com>
This commit is contained in:
HCL
2026-03-25 07:39:05 +08:00
committed by Peter Steinberger
parent 7467f304a7
commit d2248534d8

View File

@@ -209,15 +209,35 @@ export async function runServiceStart(params: {
return;
}
if (!loaded) {
await handleServiceNotLoaded({
serviceNoun: params.serviceNoun,
service: params.service,
loaded,
renderStartHints: params.renderStartHints,
json,
emit,
});
return;
// Service was stopped (e.g. `gateway stop` booted out the LaunchAgent).
// Attempt a restart, which handles re-bootstrapping the service. Without
// this, `start` after `stop` just prints hints and does nothing (#53878).
try {
const restartResult = await params.service.restart({ env: process.env, stdout });
const restartStatus = describeGatewayServiceRestart(params.serviceNoun, restartResult);
emit({
ok: true,
result: restartStatus.daemonActionResult,
message: restartStatus.message,
service: buildDaemonServiceSnapshot(params.service, true),
});
if (!json) {
defaultRuntime.log(restartStatus.message);
}
return;
} catch {
// Bootstrap failed (e.g. plist was deleted, not just booted out).
// Fall through to the not-loaded hints.
await handleServiceNotLoaded({
serviceNoun: params.serviceNoun,
service: params.service,
loaded,
renderStartHints: params.renderStartHints,
json,
emit,
});
return;
}
}
// Pre-flight config validation (#35862)
{