fix: expose safe restart deferral bypass (#78658)

Expose the existing safe-restart skipDeferral escape hatch through gateway RPC and the daemon CLI, document the flag, and add restart/CLI regression coverage.

Also keep CLI failure output off the cold bootstrap graph and align CLI guidance expectations needed by current CI.

Co-authored-by: Solomon Neas <solomonneas@users.noreply.github.com>
This commit is contained in:
Solomon Neas
2026-05-08 20:42:36 -04:00
committed by GitHub
parent 612e72ebbd
commit b81414be45
16 changed files with 263 additions and 18 deletions

View File

@@ -122,6 +122,7 @@ describe("runDaemonRestart health checks", () => {
json?: boolean;
safe?: boolean;
force?: boolean;
skipDeferral?: boolean;
}) => Promise<boolean>;
let runDaemonStop: (opts?: { json?: boolean; disable?: boolean }) => Promise<void>;
let envSnapshot: ReturnType<typeof captureEnv>;
@@ -283,6 +284,25 @@ describe("runDaemonRestart health checks", () => {
expect(runServiceRestart).toHaveBeenCalled();
});
it("forwards --safe --skip-deferral as skipDeferral: true on the RPC", async () => {
await runDaemonRestart({ json: true, safe: true, skipDeferral: true });
expect(callGatewayCli).toHaveBeenCalledWith({
method: "gateway.restart.request",
params: { reason: "gateway.restart.safe", skipDeferral: true },
timeoutMs: 10_000,
});
expect(runServiceRestart).not.toHaveBeenCalled();
});
it("rejects --skip-deferral without --safe", async () => {
await expect(runDaemonRestart({ json: true, skipDeferral: true })).rejects.toThrow(
"--skip-deferral requires --safe",
);
expect(callGatewayCli).not.toHaveBeenCalled();
expect(runServiceRestart).not.toHaveBeenCalled();
});
it("repairs stale loaded service definitions from gateway start", async () => {
repairLoadedGatewayServiceForStart.mockResolvedValue({
result: "started",

View File

@@ -155,9 +155,14 @@ async function requestSafeGatewayRestart(opts: DaemonLifecycleOptions): Promise<
if (opts.wait !== undefined) {
throw new Error("--safe cannot be combined with --wait; safe restart uses gateway deferral");
}
const skipDeferral = opts.skipDeferral === true;
const params: { reason: string; skipDeferral?: true } = { reason: "gateway.restart.safe" };
if (skipDeferral) {
params.skipDeferral = true;
}
const result = await callGatewayCli<SafeGatewayRestartRequestResult>({
method: "gateway.restart.request",
params: { reason: "gateway.restart.safe" },
params,
timeoutMs: 10_000,
});
const message =
@@ -165,7 +170,9 @@ async function requestSafeGatewayRestart(opts: DaemonLifecycleOptions): Promise<
? "safe restart request joined an existing pending gateway restart"
: result.status === "deferred"
? "safe restart requested; gateway will restart after active work drains"
: "safe restart requested; gateway will restart momentarily";
: skipDeferral
? "safe restart requested; gateway bypassing active-work deferral"
: "safe restart requested; gateway will restart momentarily";
const payload = {
ok: true,
result: result.status,
@@ -265,6 +272,9 @@ export async function runDaemonStop(opts: DaemonLifecycleOptions = {}) {
* Throws/exits on check or restart failures.
*/
export async function runDaemonRestart(opts: DaemonLifecycleOptions = {}): Promise<boolean> {
if (opts.skipDeferral && !opts.safe) {
throw new Error("--skip-deferral requires --safe");
}
if (opts.safe) {
return await requestSafeGatewayRestart(opts);
}

View File

@@ -129,6 +129,7 @@ export function addGatewayServiceCommands(parent: Command, opts?: { statusDescri
.description("Restart the Gateway service (launchd/systemd/schtasks)")
.option("--force", "Restart immediately without waiting for active gateway work", false)
.option("--safe", "Request an OpenClaw-aware restart after active work drains", false)
.option("--skip-deferral", "Bypass the safe-restart deferral gate; requires --safe", false)
.option(
"--wait <duration>",
"Wait duration before forcing restart (ms, 10s, 5m; 0 waits indefinitely)",

View File

@@ -28,6 +28,7 @@ export type DaemonLifecycleOptions = {
json?: boolean;
force?: boolean;
safe?: boolean;
skipDeferral?: boolean;
wait?: string;
disable?: boolean;
};