From b9dd6e99b677cd44ed5d04fcfface0effdcbc83a Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sat, 7 Mar 2026 21:15:01 +0000 Subject: [PATCH] fix(daemon): avoid freezing Windows PATH in task scripts (#39139, thanks @Narcooo) Co-authored-by: majx_mac --- CHANGELOG.md | 1 + src/daemon/schtasks.install.test.ts | 18 ++++++++++++++++++ src/daemon/schtasks.ts | 3 +++ src/daemon/service-env.test.ts | 16 +++++++++++++++- src/daemon/service-env.ts | 13 +++++++++---- src/tui/tui-event-handlers.test.ts | 16 ++++++++++++++++ src/tui/tui-event-handlers.ts | 16 ++++++++++++---- 7 files changed, 74 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 71e0058cb12..d56475d7dca 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -281,6 +281,7 @@ Docs: https://docs.openclaw.ai - Models/default alias refresh: bump `gpt` to `openai/gpt-5.4` and Gemini defaults to `gemini-3.1` preview aliases (including normalization/default wiring) to track current model IDs. (#38638) Thanks @ademczuk. - Config/env substitution degraded mode: convert missing `${VAR}` resolution in config reads from hard-fail to warning-backed degraded behavior, while preventing unresolved placeholders from being accepted as gateway credentials. (#39050) Thanks @akz142857. - Discord inbound listener non-blocking dispatch: make `MESSAGE_CREATE` listener handoff asynchronous (no per-listener queue blocking), so long runs no longer stall unrelated incoming events. (#39154) Thanks @yaseenkadlemakki. +- 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. ## 2026.3.2 diff --git a/src/daemon/schtasks.install.test.ts b/src/daemon/schtasks.install.test.ts index 36051aff200..16311b21dfd 100644 --- a/src/daemon/schtasks.install.test.ts +++ b/src/daemon/schtasks.install.test.ts @@ -133,4 +133,22 @@ describe("installScheduledTask", () => { ).rejects.toThrow(/Task description cannot contain CR or LF/); }); }); + + it("does not persist a frozen PATH snapshot into the generated task script", async () => { + await withUserProfileDir(async (_tmpDir, env) => { + const { scriptPath } = await installScheduledTask({ + env, + stdout: new PassThrough(), + programArguments: ["node", "gateway.js"], + environment: { + PATH: "C:\\Windows\\System32;C:\\Program Files\\Docker\\Docker\\resources\\bin", + OPENCLAW_GATEWAY_PORT: "18789", + }, + }); + + const script = await fs.readFile(scriptPath, "utf8"); + expect(script).not.toContain('set "PATH='); + expect(script).toContain('set "OPENCLAW_GATEWAY_PORT=18789"'); + }); + }); }); diff --git a/src/daemon/schtasks.ts b/src/daemon/schtasks.ts index 880e0430135..3e11d6a93b8 100644 --- a/src/daemon/schtasks.ts +++ b/src/daemon/schtasks.ts @@ -209,6 +209,9 @@ function buildTaskScript({ if (!value) { continue; } + if (key.toUpperCase() === "PATH") { + continue; + } lines.push(renderCmdSetAssignment(key, value)); } } diff --git a/src/daemon/service-env.test.ts b/src/daemon/service-env.test.ts index f1dcb6e6f6f..b3ad08a76a4 100644 --- a/src/daemon/service-env.test.ts +++ b/src/daemon/service-env.test.ts @@ -268,7 +268,7 @@ describe("buildServiceEnvironment", () => { }); expect(env.HOME).toBe("/home/user"); if (process.platform === "win32") { - expect(env.PATH).toBe(""); + expect(env).not.toHaveProperty("PATH"); } else { expect(env.PATH).toContain("/usr/bin"); } @@ -331,6 +331,20 @@ describe("buildServiceEnvironment", () => { expect(env.http_proxy).toBe("http://proxy.local:7890"); expect(env.all_proxy).toBe("socks5://proxy.local:1080"); }); + + it("omits PATH on Windows so Scheduled Tasks can inherit the current shell path", () => { + const env = buildServiceEnvironment({ + env: { + HOME: "C:\\Users\\alice", + PATH: "C:\\Windows\\System32;C:\\Tools\\rg", + }, + port: 18789, + platform: "win32", + }); + + expect(env).not.toHaveProperty("PATH"); + expect(env.OPENCLAW_WINDOWS_TASK_NAME).toBe("OpenClaw Gateway"); + }); }); describe("buildNodeServiceEnvironment", () => { diff --git a/src/daemon/service-env.ts b/src/daemon/service-env.ts index 181e45a7590..f9c10ddf1bd 100644 --- a/src/daemon/service-env.ts +++ b/src/daemon/service-env.ts @@ -30,7 +30,7 @@ type SharedServiceEnvironmentFields = { stateDir: string | undefined; configPath: string | undefined; tmpDir: string; - minimalPath: string; + minimalPath: string | undefined; proxyEnv: Record; nodeCaCerts: string | undefined; nodeUseSystemCa: string | undefined; @@ -297,16 +297,19 @@ function buildCommonServiceEnvironment( env: Record, sharedEnv: SharedServiceEnvironmentFields, ): Record { - return { + const serviceEnv: Record = { HOME: env.HOME, TMPDIR: sharedEnv.tmpDir, - PATH: sharedEnv.minimalPath, ...sharedEnv.proxyEnv, NODE_EXTRA_CA_CERTS: sharedEnv.nodeCaCerts, NODE_USE_SYSTEM_CA: sharedEnv.nodeUseSystemCa, OPENCLAW_STATE_DIR: sharedEnv.stateDir, OPENCLAW_CONFIG_PATH: sharedEnv.configPath, }; + if (sharedEnv.minimalPath) { + serviceEnv.PATH = sharedEnv.minimalPath; + } + return serviceEnv; } function resolveSharedServiceEnvironmentFields( @@ -328,7 +331,9 @@ function resolveSharedServiceEnvironmentFields( stateDir, configPath, tmpDir, - minimalPath: buildMinimalServicePath({ env }), + // On Windows, Scheduled Tasks should inherit the current task PATH instead of + // freezing the install-time snapshot into gateway.cmd/node-host.cmd. + minimalPath: platform === "win32" ? undefined : buildMinimalServicePath({ env, platform }), proxyEnv, nodeCaCerts, nodeUseSystemCa, diff --git a/src/tui/tui-event-handlers.test.ts b/src/tui/tui-event-handlers.test.ts index d976839d466..7b08ddceaf5 100644 --- a/src/tui/tui-event-handlers.test.ts +++ b/src/tui/tui-event-handlers.test.ts @@ -484,4 +484,20 @@ describe("tui-event-handlers: handleAgentEvent", () => { expect(chatLog.dropAssistant).toHaveBeenCalledWith("run-silent"); expect(chatLog.finalizeAssistant).not.toHaveBeenCalled(); }); + + it("reloads history when a local run ends without a displayable final message", () => { + const { state, loadHistory, noteLocalRunId, handleChatEvent } = createHandlersHarness({ + state: { activeChatRunId: "run-local-silent" }, + }); + + noteLocalRunId("run-local-silent"); + + handleChatEvent({ + runId: "run-local-silent", + sessionKey: state.currentSessionKey, + state: "final", + }); + + expect(loadHistory).toHaveBeenCalledTimes(1); + }); }); diff --git a/src/tui/tui-event-handlers.ts b/src/tui/tui-event-handlers.ts index b46a6653f17..54e4654ee96 100644 --- a/src/tui/tui-event-handlers.ts +++ b/src/tui/tui-event-handlers.ts @@ -136,10 +136,16 @@ export function createEventHandlers(context: EventHandlerContext) { return sessionRuns.has(activeRunId); }; - const maybeRefreshHistoryForRun = (runId: string) => { - if (isLocalRunId?.(runId)) { + const maybeRefreshHistoryForRun = ( + runId: string, + opts?: { allowLocalWithoutDisplayableFinal?: boolean }, + ) => { + const isLocalRun = isLocalRunId?.(runId) ?? false; + if (isLocalRun) { forgetLocalRunId?.(runId); - return; + if (!opts?.allowLocalWithoutDisplayableFinal) { + return; + } } if (hasConcurrentActiveRun(runId)) { return; @@ -202,7 +208,9 @@ export function createEventHandlers(context: EventHandlerContext) { if (evt.state === "final") { const wasActiveRun = state.activeChatRunId === evt.runId; if (!evt.message) { - maybeRefreshHistoryForRun(evt.runId); + maybeRefreshHistoryForRun(evt.runId, { + allowLocalWithoutDisplayableFinal: true, + }); chatLog.dropAssistant(evt.runId); finalizeRun({ runId: evt.runId, wasActiveRun, status: "idle" }); tui.requestRender();