Skip to content

Commit b74fce2

Browse files
myps6415claude
andcommitted
fix(gateway): eager-load lifecycle runtime to survive in-place upgrades
After a package-swap update (e.g. via update.run), dist/ chunk hashes rotate while the gateway is still running. The SIGUSR1 listener's first dynamic import of the lifecycle runtime module then throws ERR_MODULE_NOT_FOUND inside its async IIFE, silently rejects, and leaves restart.ts's emittedRestartToken permanently unconsumed. From that point every scheduleGatewaySigusr1Restart() — including the one update.run schedules for itself — returns { coalesced: true } without scheduling anything, and the gateway never restarts until manually kickstarted. Fix: 1. Eagerly resolve the lifecycle runtime module as the first statement of runGatewayLoop, before any signal listener is installed. lifecycle.runtime is a 36-line re-export hub, so loading it once pulls the entire restart / respawn / queue / sentinel / handoff graph into memory, immune to later disk rotation. If the module is missing at startup, fail fast with a loud error so the supervisor can recover instead of running half-broken. 2. Defense in depth: catch SIGUSR1 IIFE rejections and call markGatewaySigusr1RestartHandled() via the eagerly captured reference, so a transient listener failure doesn't permanently stick the restart token. Co-Authored-By: Claude Opus 4.7 <[email protected]>
1 parent ec7495c commit b74fce2

1 file changed

Lines changed: 28 additions & 1 deletion

File tree

src/cli/gateway-cli/run-loop.ts

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,20 @@ export async function runGatewayLoop(params: {
107107
waitForHealthyChild?: (port: number, pid?: number, host?: string) => Promise<boolean>;
108108
}) {
109109
let startupStartedAt = Date.now();
110+
// Eagerly resolve the lifecycle runtime module before installing signal
111+
// listeners. Without this, every subsequent lifecycle path (SIGUSR1,
112+
// SIGTERM-with-intent, restart iteration hook, stability bundle writer)
113+
// depends on a dynamic import() call. After an in-place package upgrade
114+
// (e.g. `npm install -g openclaw@latest` triggered via update.run),
115+
// dist/ chunk hashes rotate while the process is still running. The next
116+
// SIGUSR1 — including the one update.run schedules for itself — would
117+
// hit ERR_MODULE_NOT_FOUND from inside its async IIFE, reject silently,
118+
// and leave restart.ts's emittedRestartToken permanently unconsumed.
119+
// From that point every scheduleGatewaySigusr1Restart() returns
120+
// { coalesced: true } and the gateway never restarts. Priming the loader
121+
// here pulls the whole re-export graph (lifecycle.runtime.ts is a 36-line
122+
// re-export hub) into memory, immune to later disk rotation.
123+
const eagerLifecycleRuntime = await loadGatewayLifecycleRuntimeModule();
110124
let lock = await acquireGatewayLock({ port: params.lockPort });
111125
let server: Awaited<ReturnType<typeof startGatewayServer>> | null = null;
112126
let shuttingDown = false;
@@ -745,7 +759,20 @@ export async function runGatewayLoop(params: {
745759
const restartReason = peekGatewaySigusr1RestartReason();
746760
markGatewaySigusr1RestartHandled();
747761
request("restart", "SIGUSR1", restartReason);
748-
})();
762+
})().catch((err) => {
763+
// Defense in depth: if anything in the listener body rejects, the
764+
// SIGUSR1 emit has already advanced emittedRestartToken but no one
765+
// called markGatewaySigusr1RestartHandled. Without unsticking the
766+
// token here, every subsequent scheduleGatewaySigusr1Restart() would
767+
// silently coalesce into the dead in-flight signal and the gateway
768+
// would never restart again until manually kickstarted.
769+
gatewayLog.error(`SIGUSR1 handler failed: ${formatErrorMessage(err)}`);
770+
try {
771+
eagerLifecycleRuntime.markGatewaySigusr1RestartHandled();
772+
} catch {
773+
// Best-effort: the eager reference itself is the recovery path.
774+
}
775+
});
749776
};
750777

751778
process.on("SIGTERM", onSigterm);

0 commit comments

Comments
 (0)