diff --git a/CHANGELOG.md b/CHANGELOG.md index 4ce6381ea07..5151bdff365 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -284,6 +284,7 @@ Docs: https://docs.openclaw.ai - Daemon/Windows PATH freeze fix: stop persisting install-time `PATH` snapshots into Scheduled Task scripts so runtime tool lookup follows current host PATH updates; also refresh local TUI history on silent local finals. (#39139) Thanks @Narcooo. - Gateway/systemd service restart hardening: clear stale gateway listeners by explicit run-port before service bind, add restart stale-pid port-override support, tune systemd start/stop/exit handling, and disable detached child mode only in service-managed runtime so cgroup stop semantics clean up descendants reliably. (#38463) Thanks @spirittechie. - Discord/plugin native command aliases: let plugins declare provider-specific slash names so native Discord registration can avoid built-in command collisions; the bundled Talk voice plugin now uses `/talkvoice` natively on Discord while keeping text `/voice`. +- Daemon/Windows schtasks status normalization: derive runtime state from locale-neutral numeric `Last Run Result` codes only (without language string matching) and surface unknown when numeric result data is unavailable, preventing locale-specific misclassification drift. (#39153) Thanks @scoootscooob. ## 2026.3.2 diff --git a/src/daemon/schtasks.test.ts b/src/daemon/schtasks.test.ts index 5aff10ea543..4b45445f727 100644 --- a/src/daemon/schtasks.test.ts +++ b/src/daemon/schtasks.test.ts @@ -44,15 +44,18 @@ describe("scheduled task runtime derivation", () => { ).toEqual({ status: "running" }); }); - it("treats Running without last result as running", () => { + it("treats Running without numeric result as unknown", () => { expect( deriveScheduledTaskRuntimeStatus({ status: "Running", }), - ).toEqual({ status: "running" }); + ).toEqual({ + status: "unknown", + detail: "Task status is locale-dependent and no numeric Last Run Result was available.", + }); }); - it("downgrades stale Running status when last result is not a running code", () => { + it("treats non-running result codes as stopped", () => { expect( deriveScheduledTaskRuntimeStatus({ status: "Running", @@ -60,7 +63,7 @@ describe("scheduled task runtime derivation", () => { }), ).toEqual({ status: "stopped", - detail: "Task reports Running but Last Run Result=0x0; treating as stale runtime state.", + detail: "Task Last Run Result=0x0; treating as not running.", }); }); @@ -88,15 +91,21 @@ describe("scheduled task runtime derivation", () => { status: "Wird ausgeführt", lastRunResult: "0x0", }), - ).toEqual({ status: "stopped" }); + ).toEqual({ + status: "stopped", + detail: "Task Last Run Result=0x0; treating as not running.", + }); }); - it("treats localized status without result code as stopped", () => { + it("treats localized status without result code as unknown", () => { expect( deriveScheduledTaskRuntimeStatus({ status: "Wird ausgeführt", }), - ).toEqual({ status: "stopped" }); + ).toEqual({ + status: "unknown", + detail: "Task status is locale-dependent and no numeric Last Run Result was available.", + }); }); }); diff --git a/src/daemon/schtasks.ts b/src/daemon/schtasks.ts index 3e11d6a93b8..af09d2ca564 100644 --- a/src/daemon/schtasks.ts +++ b/src/daemon/schtasks.ts @@ -152,41 +152,31 @@ function normalizeTaskResultCode(value?: string): string | null { } } - return raw; + return null; } +const RUNNING_RESULT_CODES = new Set(["0x41301"]); +const UNKNOWN_STATUS_DETAIL = + "Task status is locale-dependent and no numeric Last Run Result was available."; + export function deriveScheduledTaskRuntimeStatus(parsed: ScheduledTaskInfo): { status: GatewayServiceRuntime["status"]; detail?: string; } { - const statusRaw = parsed.status?.trim().toLowerCase(); - if (!statusRaw) { - return { status: "unknown" }; - } - const normalizedResult = normalizeTaskResultCode(parsed.lastRunResult); - const runningCodes = new Set(["0x41301"]); - const isRunningByCode = normalizedResult != null && runningCodes.has(normalizedResult); - const isRunningByStatus = statusRaw === "running"; - - // schtasks.exe localizes its Status field ("Running" in English, - // "Wird ausgeführt" in German, "En cours" in French, etc.). - // Prefer the locale-invariant Last Run Result code 0x41301 - // ("task is currently running") over string matching. (#39057) - if (!isRunningByStatus && !isRunningByCode) { - return { status: "stopped" }; - } - - // Cross-check: if the English status says "running" but the result - // code disagrees, the runtime state is likely stale. - if (isRunningByStatus && normalizedResult && !isRunningByCode) { + if (normalizedResult != null) { + if (RUNNING_RESULT_CODES.has(normalizedResult)) { + return { status: "running" }; + } return { status: "stopped", - detail: `Task reports Running but Last Run Result=${parsed.lastRunResult}; treating as stale runtime state.`, + detail: `Task Last Run Result=${parsed.lastRunResult}; treating as not running.`, }; } - - return { status: "running" }; + if (parsed.status?.trim()) { + return { status: "unknown", detail: UNKNOWN_STATUS_DETAIL }; + } + return { status: "unknown" }; } function buildTaskScript({