From e69c2853b28db724cbd72fb2119c339af747b225 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sun, 26 Apr 2026 06:48:50 +0100 Subject: [PATCH] fix(cli): handle Volta shim respawns --- CHANGELOG.md | 1 + src/entry.respawn.test.ts | 33 +++++++++++++++++++++++++++++++++ src/entry.respawn.ts | 26 +++++++++++++++++++++++++- src/entry.ts | 2 +- 4 files changed, 60 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9a0350749db..99f2c3b78f2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -74,6 +74,7 @@ Docs: https://docs.openclaw.ai - Installer/macOS: rerun Homebrew install steps without the gum spinner when raw-mode ioctl failures occur, and avoid claiming `node@24` was installed when the Homebrew keg binary is missing. Fixes #70411. Thanks @1fanwang and @dad-io. - Installer: load nvm before Node.js detection so `curl | bash` installs respect nvm-managed Node instead of stale system Node. Fixes #49556. Thanks @heavenlxj. +- CLI/Volta: respawn raw `openclaw` CLI runs through the named `node` shim when the current Node executable resolves to `volta-shim`, avoiding direct shim execution failures in non-interactive shells. Fixes #68672. Thanks @sanchezm86. - Docker: copy patched dependency files into runtime images so downstream `pnpm install` layers keep working. Fixes #69224. Thanks @gucasbrg. - Agents/runtime: submit heartbeat, cron, and exec wakeups as transient runtime context instead of visible user prompts, keeping synthetic system work out of chat transcripts. Fixes #66496 and #66814. Thanks @jeades and @mandomaker. - Telegram: include native quote excerpts automatically for threaded replies and reply tags when the original Telegram text is available, without adding another config knob. Fixes #6975. Thanks @rex05ai. diff --git a/src/entry.respawn.test.ts b/src/entry.respawn.test.ts index 6c2b917d2d2..afe34c09722 100644 --- a/src/entry.respawn.test.ts +++ b/src/entry.respawn.test.ts @@ -4,6 +4,7 @@ import { EXPERIMENTAL_WARNING_FLAG, OPENCLAW_NODE_EXTRA_CA_CERTS_READY, OPENCLAW_NODE_OPTIONS_READY, + resolveCliRespawnCommand, } from "./entry.respawn.js"; const shouldSkipRespawnForArgvMock = vi.hoisted(() => vi.fn(() => false)); @@ -42,6 +43,7 @@ describe("buildCliRespawnPlan", () => { }); expect(plan).not.toBeNull(); + expect(plan?.command).toBe(process.execPath); expect(plan?.argv[0]).toBe(EXPERIMENTAL_WARNING_FLAG); expect(plan?.env.NODE_EXTRA_CA_CERTS).toBe("/etc/ssl/certs/ca-certificates.crt"); expect(plan?.env[OPENCLAW_NODE_EXTRA_CA_CERTS_READY]).toBe("1"); @@ -88,4 +90,35 @@ describe("buildCliRespawnPlan", () => { }), ).toBeNull(); }); + + it("respawns Volta shims through node so the shim is not called directly", () => { + const plan = buildCliRespawnPlan({ + argv: ["/home/alice/.volta/bin/volta-shim", "/usr/local/bin/openclaw", "status"], + env: { PATH: "/home/alice/.volta/bin:/usr/bin:/bin" }, + execArgv: [], + execPath: "/home/alice/.volta/bin/volta-shim", + autoNodeExtraCaCerts: undefined, + platform: "linux", + }); + + expect(plan?.command).toBe("node"); + expect(plan?.argv).toEqual([EXPERIMENTAL_WARNING_FLAG, "/usr/local/bin/openclaw", "status"]); + }); +}); + +describe("resolveCliRespawnCommand", () => { + it("keeps normal node paths absolute", () => { + expect(resolveCliRespawnCommand({ execPath: "/usr/bin/node", platform: "linux" })).toBe( + "/usr/bin/node", + ); + }); + + it("maps Volta's Unix shim target back to the named node shim", () => { + expect( + resolveCliRespawnCommand({ + execPath: "/home/alice/.volta/bin/volta-shim", + platform: "linux", + }), + ).toBe("node"); + }); }); diff --git a/src/entry.respawn.ts b/src/entry.respawn.ts index 19ec362372c..622f837f1cb 100644 --- a/src/entry.respawn.ts +++ b/src/entry.respawn.ts @@ -1,3 +1,4 @@ +import path from "node:path"; import { resolveNodeStartupTlsEnvironment } from "./bootstrap/node-startup-env.js"; import { shouldSkipRespawnForArgv } from "./cli/respawn-policy.js"; import { isTruthyEnvValue } from "./infra/env.js"; @@ -6,6 +7,28 @@ export const EXPERIMENTAL_WARNING_FLAG = "--disable-warning=ExperimentalWarning" export const OPENCLAW_NODE_OPTIONS_READY = "OPENCLAW_NODE_OPTIONS_READY"; export const OPENCLAW_NODE_EXTRA_CA_CERTS_READY = "OPENCLAW_NODE_EXTRA_CA_CERTS_READY"; +export type CliRespawnPlan = { + command: string; + argv: string[]; + env: NodeJS.ProcessEnv; +}; + +function pathModuleForPlatform(platform: NodeJS.Platform): typeof path.posix { + return platform === "win32" ? path.win32 : path.posix; +} + +export function resolveCliRespawnCommand(params: { + execPath: string; + platform?: NodeJS.Platform; +}): string { + const platform = params.platform ?? process.platform; + const basename = pathModuleForPlatform(platform).basename(params.execPath).toLowerCase(); + if (basename === "volta-shim" || basename === "volta-shim.exe") { + return "node"; + } + return params.execPath; +} + export function hasExperimentalWarningSuppressed( params: { env?: NodeJS.ProcessEnv; @@ -30,7 +53,7 @@ export function buildCliRespawnPlan( autoNodeExtraCaCerts?: string | undefined; platform?: NodeJS.Platform; } = {}, -): { argv: string[]; env: NodeJS.ProcessEnv } | null { +): CliRespawnPlan | null { const argv = params.argv ?? process.argv; const env = params.env ?? process.env; const execArgv = params.execArgv ?? process.execArgv; @@ -80,6 +103,7 @@ export function buildCliRespawnPlan( } return { + command: resolveCliRespawnCommand({ execPath, platform }), argv: [...childExecArgv, ...argv.slice(1)], env: childEnv, }; diff --git a/src/entry.ts b/src/entry.ts index 2d0d7f5df5f..6d9ec51a623 100644 --- a/src/entry.ts +++ b/src/entry.ts @@ -70,7 +70,7 @@ if ( return false; } - const child = spawn(process.execPath, plan.argv, { + const child = spawn(plan.command, plan.argv, { stdio: "inherit", env: plan.env, });