fix(daemon): avoid freezing Windows PATH in task scripts (#39139, thanks @Narcooo)

Co-authored-by: majx_mac <mjxnarco@pku.edu.cn>
This commit is contained in:
Peter Steinberger
2026-03-07 21:15:01 +00:00
parent f51cac277c
commit b9dd6e99b6
7 changed files with 74 additions and 9 deletions

View File

@@ -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. - 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. - 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. - 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 ## 2026.3.2

View File

@@ -133,4 +133,22 @@ describe("installScheduledTask", () => {
).rejects.toThrow(/Task description cannot contain CR or LF/); ).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"');
});
});
}); });

View File

@@ -209,6 +209,9 @@ function buildTaskScript({
if (!value) { if (!value) {
continue; continue;
} }
if (key.toUpperCase() === "PATH") {
continue;
}
lines.push(renderCmdSetAssignment(key, value)); lines.push(renderCmdSetAssignment(key, value));
} }
} }

View File

@@ -268,7 +268,7 @@ describe("buildServiceEnvironment", () => {
}); });
expect(env.HOME).toBe("/home/user"); expect(env.HOME).toBe("/home/user");
if (process.platform === "win32") { if (process.platform === "win32") {
expect(env.PATH).toBe(""); expect(env).not.toHaveProperty("PATH");
} else { } else {
expect(env.PATH).toContain("/usr/bin"); expect(env.PATH).toContain("/usr/bin");
} }
@@ -331,6 +331,20 @@ describe("buildServiceEnvironment", () => {
expect(env.http_proxy).toBe("http://proxy.local:7890"); expect(env.http_proxy).toBe("http://proxy.local:7890");
expect(env.all_proxy).toBe("socks5://proxy.local:1080"); 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", () => { describe("buildNodeServiceEnvironment", () => {

View File

@@ -30,7 +30,7 @@ type SharedServiceEnvironmentFields = {
stateDir: string | undefined; stateDir: string | undefined;
configPath: string | undefined; configPath: string | undefined;
tmpDir: string; tmpDir: string;
minimalPath: string; minimalPath: string | undefined;
proxyEnv: Record<string, string | undefined>; proxyEnv: Record<string, string | undefined>;
nodeCaCerts: string | undefined; nodeCaCerts: string | undefined;
nodeUseSystemCa: string | undefined; nodeUseSystemCa: string | undefined;
@@ -297,16 +297,19 @@ function buildCommonServiceEnvironment(
env: Record<string, string | undefined>, env: Record<string, string | undefined>,
sharedEnv: SharedServiceEnvironmentFields, sharedEnv: SharedServiceEnvironmentFields,
): Record<string, string | undefined> { ): Record<string, string | undefined> {
return { const serviceEnv: Record<string, string | undefined> = {
HOME: env.HOME, HOME: env.HOME,
TMPDIR: sharedEnv.tmpDir, TMPDIR: sharedEnv.tmpDir,
PATH: sharedEnv.minimalPath,
...sharedEnv.proxyEnv, ...sharedEnv.proxyEnv,
NODE_EXTRA_CA_CERTS: sharedEnv.nodeCaCerts, NODE_EXTRA_CA_CERTS: sharedEnv.nodeCaCerts,
NODE_USE_SYSTEM_CA: sharedEnv.nodeUseSystemCa, NODE_USE_SYSTEM_CA: sharedEnv.nodeUseSystemCa,
OPENCLAW_STATE_DIR: sharedEnv.stateDir, OPENCLAW_STATE_DIR: sharedEnv.stateDir,
OPENCLAW_CONFIG_PATH: sharedEnv.configPath, OPENCLAW_CONFIG_PATH: sharedEnv.configPath,
}; };
if (sharedEnv.minimalPath) {
serviceEnv.PATH = sharedEnv.minimalPath;
}
return serviceEnv;
} }
function resolveSharedServiceEnvironmentFields( function resolveSharedServiceEnvironmentFields(
@@ -328,7 +331,9 @@ function resolveSharedServiceEnvironmentFields(
stateDir, stateDir,
configPath, configPath,
tmpDir, 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, proxyEnv,
nodeCaCerts, nodeCaCerts,
nodeUseSystemCa, nodeUseSystemCa,

View File

@@ -484,4 +484,20 @@ describe("tui-event-handlers: handleAgentEvent", () => {
expect(chatLog.dropAssistant).toHaveBeenCalledWith("run-silent"); expect(chatLog.dropAssistant).toHaveBeenCalledWith("run-silent");
expect(chatLog.finalizeAssistant).not.toHaveBeenCalled(); 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);
});
}); });

View File

@@ -136,10 +136,16 @@ export function createEventHandlers(context: EventHandlerContext) {
return sessionRuns.has(activeRunId); return sessionRuns.has(activeRunId);
}; };
const maybeRefreshHistoryForRun = (runId: string) => { const maybeRefreshHistoryForRun = (
if (isLocalRunId?.(runId)) { runId: string,
opts?: { allowLocalWithoutDisplayableFinal?: boolean },
) => {
const isLocalRun = isLocalRunId?.(runId) ?? false;
if (isLocalRun) {
forgetLocalRunId?.(runId); forgetLocalRunId?.(runId);
return; if (!opts?.allowLocalWithoutDisplayableFinal) {
return;
}
} }
if (hasConcurrentActiveRun(runId)) { if (hasConcurrentActiveRun(runId)) {
return; return;
@@ -202,7 +208,9 @@ export function createEventHandlers(context: EventHandlerContext) {
if (evt.state === "final") { if (evt.state === "final") {
const wasActiveRun = state.activeChatRunId === evt.runId; const wasActiveRun = state.activeChatRunId === evt.runId;
if (!evt.message) { if (!evt.message) {
maybeRefreshHistoryForRun(evt.runId); maybeRefreshHistoryForRun(evt.runId, {
allowLocalWithoutDisplayableFinal: true,
});
chatLog.dropAssistant(evt.runId); chatLog.dropAssistant(evt.runId);
finalizeRun({ runId: evt.runId, wasActiveRun, status: "idle" }); finalizeRun({ runId: evt.runId, wasActiveRun, status: "idle" });
tui.requestRender(); tui.requestRender();