Files
openclaw/cli-parent-lifetime.mjs
Peter Steinberger 71fac53596 fix: preserve standalone CLI commands across startup respawns (#174545)
* fix: preserve standalone CLI commands across startup respawns

Resolve launcher-bound runtime arguments at the CLI invocation owner. Consume a one-child parent PID signal in compiled runtimes while preserving the loose preload and detached worker lifetimes.

* test: keep respawn transport guard independent of local names

* test: include respawn owner dependencies in CLI fixtures
2026-10-16 12:34:54 -07:00

53 lines
1.9 KiB
JavaScript

import { GATEWAY_SHUTDOWN_RESERVE_MS } from "./gateway-shutdown-budget.mjs";
const parentKey = Symbol.for("openclaw.cliRespawnParent");
export const CLI_RESPAWN_PARENT_ENV = "OPENCLAW_CLI_RESPAWN_PARENT";
// Compiled executables cannot run a preload. Consume its one-child equivalent
// before any detached worker can inherit it, and reject a different parent.
const inheritedParent = process.env[CLI_RESPAWN_PARENT_ENV];
delete process.env[CLI_RESPAWN_PARENT_ENV];
if (inheritedParent === String(process.ppid)) {
globalThis[parentKey] = { pid: process.ppid };
}
// Consume the private preload so later detached workers cannot inherit this contract.
export const CLI_PARENT_BOUND_PRELOAD = `data:text/javascript,${encodeURIComponent(`
globalThis[Symbol.for("openclaw.cliRespawnParent")] = { pid: process.ppid };
process.execArgv = process.execArgv.filter((arg, index, args) =>
arg !== import.meta.url && !(arg === "--import" && args[index + 1] === import.meta.url));
`)}`;
/** Only the respawn producer opts in; detached update drivers keep independent custody. */
export function getCliParentPid() {
return globalThis[parentKey]?.pid;
}
let terminating = false;
export function requestCliParentTermination(
signal = () => {
if (!process.emit("SIGTERM")) {
process.kill(process.pid, "SIGTERM");
}
},
) {
if (terminating) {
return;
}
terminating = true;
// Doctor's signal gate cancels work and settles rollback/restoration before exit.
setTimeout(() => process.kill(process.pid, "SIGKILL"), GATEWAY_SHUTDOWN_RESERVE_MS).unref();
signal();
}
export function watchCliRespawnParent() {
const parent = globalThis[parentKey];
if (!parent || parent.watching || !process.channel) {
return;
}
parent.watching = true;
process.once("disconnect", requestCliParentTermination);
process.channel.unref();
if (!process.connected) {
requestCliParentTermination();
}
}