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,