From 0278b59d0ea33514250a6089f95dee5e7ef4cacc Mon Sep 17 00:00:00 2001 From: liuhao1024 Date: Wed, 17 Jun 2026 01:15:24 +0800 Subject: [PATCH] fix(respawn): rewrite pnpm versioned entry paths to stable wrapper (fixes #52313) (#93671) * fix(respawn): rewrite pnpm versioned entry paths to stable wrapper During self-update the pnpm versioned directory (node_modules/.pnpm/openclaw@/) may be removed. If process.argv contains the versioned path, the respawned child fails to start because the entrypoint no longer exists. Detect pnpm versioned realpaths in spawnDetachedGatewayProcess and rewrite them to the stable node_modules//openclaw.mjs wrapper before spawning. Fixes #52313 * fix(respawn): scope pnpm entry rewrite to openclaw --------- Co-authored-by: Vincent Koc <25068+vincentkoc@users.noreply.github.com> --- src/infra/process-respawn.test.ts | 48 +++++++++++++++++++++++++++++-- src/infra/process-respawn.ts | 19 +++++++++++- 2 files changed, 64 insertions(+), 3 deletions(-) diff --git a/src/infra/process-respawn.test.ts b/src/infra/process-respawn.test.ts index 63a348ec71c..dee02ad40d9 100644 --- a/src/infra/process-respawn.test.ts +++ b/src/infra/process-respawn.test.ts @@ -298,7 +298,7 @@ describe("respawnGatewayProcessForUpdate", () => { process.execArgv = []; process.argv = [ "C:\\Program Files\\node.exe", - "C:\\openclaw\\dist\\index.js", + "C:\\openclaw\\node_modules\\.pnpm\\openclaw@2026.6.5\\node_modules\\openclaw\\dist\\index.js", "gateway", "run", ]; @@ -310,7 +310,7 @@ describe("respawnGatewayProcessForUpdate", () => { expect(result.pid).toBe(5151); expect(spawnMock).toHaveBeenCalledWith( process.execPath, - ["C:\\openclaw\\dist\\index.js", "gateway", "run"], + ["C:\\openclaw\\node_modules\\openclaw\\openclaw.mjs", "gateway", "run"], { detached: true, env: process.env, @@ -319,6 +319,50 @@ describe("respawnGatewayProcessForUpdate", () => { ); }); + it("rewrites a pnpm-versioned OpenClaw entry before detached update respawn", () => { + clearSupervisorHints(); + setPlatform("linux"); + process.execArgv = []; + process.argv = [ + "/usr/local/bin/node", + "/app/node_modules/.pnpm/openclaw@2026.6.5/node_modules/openclaw/dist/entry.js", + "gateway", + "run", + ]; + spawnMock.mockReturnValue({ pid: 7171, unref: vi.fn(), kill: vi.fn() }); + + const result = respawnGatewayProcessForUpdate(); + + expect(result.mode).toBe("spawned"); + expect(spawnMock).toHaveBeenCalledWith( + process.execPath, + ["/app/node_modules/openclaw/openclaw.mjs", "gateway", "run"], + { + detached: true, + env: process.env, + stdio: "inherit", + }, + ); + }); + + it("does not rewrite another package's pnpm-versioned entry", () => { + clearSupervisorHints(); + setPlatform("linux"); + process.execArgv = []; + const entry = + "/app/node_modules/.pnpm/@anthropic+sdk@1.0.0/node_modules/@anthropic/sdk/dist/index.js"; + process.argv = ["/usr/local/bin/node", entry, "gateway", "run"]; + spawnMock.mockReturnValue({ pid: 8181, unref: vi.fn(), kill: vi.fn() }); + + respawnGatewayProcessForUpdate(); + + expect(spawnMock).toHaveBeenCalledWith(process.execPath, [entry, "gateway", "run"], { + detached: true, + env: process.env, + stdio: "inherit", + }); + }); + it("spawns a detached update process when macOS only has inherited XPC state", () => { clearSupervisorHints(); setPlatform("darwin"); diff --git a/src/infra/process-respawn.ts b/src/infra/process-respawn.ts index 137582775e3..cde9662aaa7 100644 --- a/src/infra/process-respawn.ts +++ b/src/infra/process-respawn.ts @@ -26,11 +26,28 @@ function isTruthy(value: string | undefined): boolean { return normalized === "1" || normalized === "true" || normalized === "yes" || normalized === "on"; } +const PNPM_VERSIONED_OPENCLAW_ENTRY_PATTERN = + /^(.*?)([\\/])node_modules\2\.pnpm\2openclaw@[^\\/]+\2node_modules\2openclaw\2.+$/; + +function rewritePnpmVersionedOpenClawEntryPath(entryPath: string): string { + // pnpm can expose argv[1] as a versioned realpath that self-update removes. + // Respawn through the stable OpenClaw package wrapper instead. + return entryPath.replace( + PNPM_VERSIONED_OPENCLAW_ENTRY_PATTERN, + "$1$2node_modules$2openclaw$2openclaw.mjs", + ); +} + function spawnDetachedGatewayProcess(opts: GatewayRespawnOptions = {}): { child: ChildProcess; pid?: number; } { - const args = [...process.execArgv, ...process.argv.slice(1)]; + const [entryArg, ...entryArgs] = process.argv.slice(1); + const args = [ + ...process.execArgv, + ...(entryArg ? [rewritePnpmVersionedOpenClawEntryPath(entryArg)] : []), + ...entryArgs, + ]; const child = spawn(process.execPath, args, { env: opts.env ? { ...process.env, ...opts.env } : process.env, detached: true,