fix: windows restart fallback when scheduled task is unregistered (#58943) (thanks @imechZhangLY)

* fix(infra): windows-task-restart fallback to startup entry when schtasks task is unregistered

* fix code style problem

* use /min for startup fallback and assert schtasks pre-check in test

* fix: windows restart fallback when scheduled task is unregistered (#58943) (thanks @imechZhangLY)

---------

Co-authored-by: Luyao Zhang <zhangluyao@microsoft.com>
Co-authored-by: Ayaan Zaidi <hi@obviy.us>
This commit is contained in:
imechZhangLY
2026-04-05 16:21:57 +08:00
committed by GitHub
parent 5ac07b8ef0
commit 0e61a1d0ca
3 changed files with 65 additions and 7 deletions

View File

@@ -132,6 +132,7 @@ Docs: https://docs.openclaw.ai
- Android/Talk Mode: restore voice replies on gateway-backed talk mode sessions by updating embedded runner transport overrides to the current agent transport API. (#61214) Thanks @obviyus.
- Amazon Bedrock/aws-sdk auth: stop injecting the fake `AWS_PROFILE` apiKey marker when no AWS auth env vars exist, so instance-role and other default-chain setups keep working without poisoning provider config. (#61194) Thanks @wirjo.
- Providers/Google: add model-level `cacheRetention` support for direct Gemini system prompts by creating, reusing, and refreshing `cachedContents` automatically on Google AI Studio runs. (#51372) Thanks @rafaelmariano-glitch.
- Windows/restart: fall back to the installed Startup-entry launcher when the scheduled task was never registered, so `/restart` can relaunch the gateway on Windows setups where `schtasks` install fell back during onboarding. (#58943) Thanks @imechZhangLY.
## 2026.4.2

View File

@@ -6,6 +6,12 @@ import { captureFullEnv } from "../test-utils/env.js";
const spawnMock = vi.hoisted(() => vi.fn());
const resolvePreferredOpenClawTmpDirMock = vi.hoisted(() => vi.fn(() => os.tmpdir()));
const resolveTaskScriptPathMock = vi.hoisted(() =>
vi.fn((env: Record<string, string | undefined>) => {
const home = env.USERPROFILE || env.HOME || os.homedir();
return path.join(home, ".openclaw", "gateway.cmd");
}),
);
vi.mock("node:child_process", async () => {
const { mockNodeBuiltinModule } = await import("../../test/helpers/node-builtin-mocks.js");
@@ -19,6 +25,9 @@ vi.mock("node:child_process", async () => {
vi.mock("./tmp-openclaw-dir.js", () => ({
resolvePreferredOpenClawTmpDir: () => resolvePreferredOpenClawTmpDirMock(),
}));
vi.mock("../daemon/schtasks.js", () => ({
resolveTaskScriptPath: (env: Record<string, string | undefined>) => resolveTaskScriptPathMock(env),
}));
type WindowsTaskRestartModule = typeof import("./windows-task-restart.js");
@@ -64,6 +73,11 @@ describe("relaunchGatewayScheduledTask", () => {
spawnMock.mockReset();
resolvePreferredOpenClawTmpDirMock.mockReset();
resolvePreferredOpenClawTmpDirMock.mockReturnValue(os.tmpdir());
resolveTaskScriptPathMock.mockReset();
resolveTaskScriptPathMock.mockImplementation((env: Record<string, string | undefined>) => {
const home = env.USERPROFILE || env.HOME || os.homedir();
return path.join(home, ".openclaw", "gateway.cmd");
});
});
it("writes a detached schtasks relaunch helper", () => {
@@ -145,4 +159,27 @@ describe("relaunchGatewayScheduledTask", () => {
expect.any(Object),
);
});
it("includes startup fallback", () => {
const taskScriptDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-state-"));
createdTmpDirs.add(taskScriptDir);
const taskScriptPath = path.join(taskScriptDir, "gateway.cmd");
fs.writeFileSync(taskScriptPath, "@echo off\r\nrem placeholder\r\n", "utf8");
resolveTaskScriptPathMock.mockReturnValue(taskScriptPath);
spawnMock.mockImplementation((_file: string, args: string[]) => {
createdScriptPaths.add(decodeCmdPathArg(args[3]));
return { unref: vi.fn() };
});
const result = relaunchGatewayScheduledTask({ OPENCLAW_PROFILE: "work" });
expect(result.ok).toBe(true);
const scriptPath = [...createdScriptPaths][0];
const script = fs.readFileSync(scriptPath, "utf8");
expect(script).toContain(`schtasks /Query /TN`);
expect(script).toContain(":fallback");
expect(script).toContain(`start "" /min cmd.exe /d /c`);
expect(script).toContain(taskScriptPath);
});
});

View File

@@ -4,6 +4,7 @@ import fs from "node:fs";
import path from "node:path";
import { quoteCmdScriptArg } from "../daemon/cmd-argv.js";
import { resolveGatewayWindowsTaskName } from "../daemon/constants.js";
import { resolveTaskScriptPath } from "../daemon/schtasks.js";
import type { RestartAttempt } from "./restart.js";
import { resolvePreferredOpenClawTmpDir } from "./tmp-openclaw-dir.js";
@@ -18,33 +19,52 @@ function resolveWindowsTaskName(env: NodeJS.ProcessEnv): string {
return resolveGatewayWindowsTaskName(env.OPENCLAW_PROFILE);
}
function buildScheduledTaskRestartScript(taskName: string): string {
function buildScheduledTaskRestartScript(
taskName: string,
taskScriptPath?: string,
): string {
const quotedTaskName = quoteCmdScriptArg(taskName);
return [
const lines = [
"@echo off",
"setlocal",
`schtasks /Query /TN ${quotedTaskName} >nul 2>&1`,
"if errorlevel 1 goto fallback",
"set /a attempts=0",
":retry",
`timeout /t ${TASK_RESTART_RETRY_DELAY_SEC} /nobreak >nul`,
"set /a attempts+=1",
`schtasks /Run /TN ${quotedTaskName} >nul 2>&1`,
"if not errorlevel 1 goto cleanup",
`if %attempts% GEQ ${TASK_RESTART_RETRY_LIMIT} goto cleanup`,
`if %attempts% GEQ ${TASK_RESTART_RETRY_LIMIT} goto fallback`,
"goto retry",
":cleanup",
'del "%~f0" >nul 2>&1',
].join("\r\n");
":fallback",
];
if (taskScriptPath) {
const quotedScript = quoteCmdScriptArg(taskScriptPath);
lines.push(
`if exist ${quotedScript} (`,
` start "" /min cmd.exe /d /c ${quotedScript}`,
")",
);
}
lines.push(":cleanup", 'del "%~f0" >nul 2>&1');
return lines.join("\r\n");
}
export function relaunchGatewayScheduledTask(env: NodeJS.ProcessEnv = process.env): RestartAttempt {
const taskName = resolveWindowsTaskName(env);
const taskScriptPath = resolveTaskScriptPath(env);
const scriptPath = path.join(
resolvePreferredOpenClawTmpDir(),
`openclaw-schtasks-restart-${randomUUID()}.cmd`,
);
const quotedScriptPath = quoteCmdScriptArg(scriptPath);
try {
fs.writeFileSync(scriptPath, `${buildScheduledTaskRestartScript(taskName)}\r\n`, "utf8");
fs.writeFileSync(
scriptPath,
`${buildScheduledTaskRestartScript(taskName, taskScriptPath)}\r\n`,
"utf8",
);
const child = spawn("cmd.exe", ["/d", "/s", "/c", quotedScriptPath], {
detached: true,
stdio: "ignore",