From 8421b2e848415b9715672eb3bd5b3e76a0ddbad3 Mon Sep 17 00:00:00 2001 From: Fologan <164580328+Fologan@users.noreply.github.com> Date: Mon, 2 Mar 2026 08:12:24 -0600 Subject: [PATCH] fix(gateway): avoid stale running status from Windows Scheduled Task (openclaw#19504) thanks @Fologan Verified: - pnpm vitest src/daemon/schtasks.test.ts - pnpm check - pnpm build Co-authored-by: Fologan <164580328+Fologan@users.noreply.github.com> Co-authored-by: Tak Hoffman <781889+Takhoffman@users.noreply.github.com> --- src/daemon/schtasks.test.ts | 47 +++++++++++++++++++++++++++++++- src/daemon/schtasks.ts | 53 ++++++++++++++++++++++++++++++++++--- 2 files changed, 96 insertions(+), 4 deletions(-) diff --git a/src/daemon/schtasks.test.ts b/src/daemon/schtasks.test.ts index 3923f197b..6eb4e23ff 100644 --- a/src/daemon/schtasks.test.ts +++ b/src/daemon/schtasks.test.ts @@ -2,7 +2,12 @@ import fs from "node:fs/promises"; import os from "node:os"; import path from "node:path"; import { describe, expect, it } from "vitest"; -import { parseSchtasksQuery, readScheduledTaskCommand, resolveTaskScriptPath } from "./schtasks.js"; +import { + deriveScheduledTaskRuntimeStatus, + parseSchtasksQuery, + readScheduledTaskCommand, + resolveTaskScriptPath, +} from "./schtasks.js"; describe("schtasks runtime parsing", () => { it.each(["Ready", "Running"])("parses %s status", (status) => { @@ -20,6 +25,46 @@ describe("schtasks runtime parsing", () => { }); }); +describe("scheduled task runtime derivation", () => { + it("treats Running + 0x41301 as running", () => { + expect( + deriveScheduledTaskRuntimeStatus({ + status: "Running", + lastRunResult: "0x41301", + }), + ).toEqual({ status: "running" }); + }); + + it("treats Running + decimal 267009 as running", () => { + expect( + deriveScheduledTaskRuntimeStatus({ + status: "Running", + lastRunResult: "267009", + }), + ).toEqual({ status: "running" }); + }); + + it("treats Running without last result as running", () => { + expect( + deriveScheduledTaskRuntimeStatus({ + status: "Running", + }), + ).toEqual({ status: "running" }); + }); + + it("downgrades stale Running status when last result is not a running code", () => { + expect( + deriveScheduledTaskRuntimeStatus({ + status: "Running", + lastRunResult: "0x0", + }), + ).toEqual({ + status: "stopped", + detail: "Task reports Running but Last Run Result=0x0; treating as stale runtime state.", + }); + }); +}); + describe("resolveTaskScriptPath", () => { it.each([ { diff --git a/src/daemon/schtasks.ts b/src/daemon/schtasks.ts index c00d98864..091dad88b 100644 --- a/src/daemon/schtasks.ts +++ b/src/daemon/schtasks.ts @@ -132,6 +132,53 @@ export function parseSchtasksQuery(output: string): ScheduledTaskInfo { return info; } +function normalizeTaskResultCode(value?: string): string | null { + if (!value) { + return null; + } + const raw = value.trim().toLowerCase(); + if (!raw) { + return null; + } + + if (/^0x[0-9a-f]+$/.test(raw)) { + return `0x${raw.slice(2).replace(/^0+/, "") || "0"}`; + } + + if (/^\d+$/.test(raw)) { + const numeric = Number.parseInt(raw, 10); + if (Number.isFinite(numeric)) { + return `0x${numeric.toString(16)}`; + } + } + + return raw; +} + +export function deriveScheduledTaskRuntimeStatus(parsed: ScheduledTaskInfo): { + status: GatewayServiceRuntime["status"]; + detail?: string; +} { + const statusRaw = parsed.status?.trim().toLowerCase(); + if (!statusRaw) { + return { status: "unknown" }; + } + if (statusRaw !== "running") { + return { status: "stopped" }; + } + + const normalizedResult = normalizeTaskResultCode(parsed.lastRunResult); + const runningCodes = new Set(["0x41301"]); + if (normalizedResult && !runningCodes.has(normalizedResult)) { + return { + status: "stopped", + detail: `Task reports Running but Last Run Result=${parsed.lastRunResult}; treating as stale runtime state.`, + }; + } + + return { status: "running" }; +} + function buildTaskScript({ description, programArguments, @@ -307,12 +354,12 @@ export async function readScheduledTaskRuntime( }; } const parsed = parseSchtasksQuery(res.stdout || ""); - const statusRaw = parsed.status?.toLowerCase(); - const status = statusRaw === "running" ? "running" : statusRaw ? "stopped" : "unknown"; + const derived = deriveScheduledTaskRuntimeStatus(parsed); return { - status, + status: derived.status, state: parsed.status, lastRunTime: parsed.lastRunTime, lastRunResult: parsed.lastRunResult, + ...(derived.detail ? { detail: derived.detail } : {}), }; }