mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 05:30:42 +00:00
fix: keep gateway shutdown runtime stable across updates
This commit is contained in:
@@ -56,6 +56,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Exec approvals: detect `env -S` split-string command-carrier risks when `-S`/`-s` is combined with other env short options, so approval explanations do not miss split payloads hidden behind `env -iS...`. Thanks @vincentkoc.
|
||||
- Google Meet: log the concrete agent-mode TTS provider, model, voice, output format, and sample rate after speech synthesis, so Meet logs show which voice backend spoke each reply.
|
||||
- Voice Call: mark realtime calls completed when the realtime provider closes normally, so Twilio/OpenAI/Google realtime stop events do not leave active call records behind. Thanks @vincentkoc.
|
||||
- Gateway/update: keep the shutdown close path behind a stable runtime chunk and ship compatibility aliases for recent `server-close-*` hashes, so manual npm package replacement cannot leave an already-running Gateway unable to shut down cleanly. Fixes #77087. Thanks @westlife219.
|
||||
- Exec approvals: treat POSIX `exec` as a command carrier for inline eval, shell-wrapper, and eval/source detection, so approval explanations and command-risk checks do not miss payloads hidden behind `exec`. Thanks @vincentkoc.
|
||||
- Google Meet: log the resolved audio provider model when starting Chrome and paired-node Meet talk-back bridges, so agent-mode joins show the STT model and bidi joins show the realtime voice model.
|
||||
- Diagnostics: handle missing session-tail files in cron recovery context without tripping extension test typecheck. Thanks @vincentkoc.
|
||||
|
||||
@@ -93,6 +93,12 @@ curl -fsSL https://openclaw.ai/install.sh | bash -s -- --install-method npm --ve
|
||||
npm i -g openclaw@latest
|
||||
```
|
||||
|
||||
Prefer `openclaw update` for supervised installs because it can coordinate the
|
||||
package swap with the running Gateway service. If you update manually while a
|
||||
managed Gateway is running, restart the Gateway immediately after the package
|
||||
manager finishes so the old process does not keep serving from replaced package
|
||||
files.
|
||||
|
||||
When `openclaw update` manages a global npm install, it installs the target into
|
||||
a temporary npm prefix first, verifies the packaged `dist` inventory, then swaps
|
||||
the clean package tree into the real global prefix. That avoids npm overlaying a
|
||||
|
||||
@@ -36,6 +36,10 @@ const LEGACY_ROOT_RUNTIME_COMPAT_ALIASES = [
|
||||
["route-reply.runtime-uzaOjbd1.js", "route-reply.runtime.js"],
|
||||
["runtime-plugins.runtime-CNAfmQRG.js", "runtime-plugins.runtime.js"],
|
||||
["tts.runtime-D-THXDsp.js", "tts.runtime.js"],
|
||||
// v2026.5.2 -> v2026.5.3-beta.3 gateway shutdown chunks. The running
|
||||
// gateway may resolve these only after an npm package tree replacement.
|
||||
["server-close-DsVPJDIx.js", "server-close.runtime.js"],
|
||||
["server-close-DvAvfgr8.js", "server-close.runtime.js"],
|
||||
];
|
||||
const LEGACY_CLI_EXIT_COMPAT_CHUNKS = [
|
||||
{
|
||||
|
||||
1
src/gateway/server-close.runtime.ts
Normal file
1
src/gateway/server-close.runtime.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from "./server-close.js";
|
||||
@@ -246,6 +246,22 @@ describe("createGatewayCloseHandler", () => {
|
||||
expect(stopChannel).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it("uses caller-provided channel ids instead of the local channel registry", async () => {
|
||||
mocks.listChannelPlugins.mockReturnValue([]);
|
||||
const stopChannel = vi.fn(async (_id: string) => undefined);
|
||||
const close = createGatewayCloseHandler(
|
||||
createGatewayCloseTestDeps({
|
||||
channelIds: ["telegram", "discord"],
|
||||
stopChannel,
|
||||
}),
|
||||
);
|
||||
|
||||
await close({ reason: "test shutdown" });
|
||||
|
||||
expect(mocks.listChannelPlugins).not.toHaveBeenCalled();
|
||||
expect(stopChannel.mock.calls.map(([id]) => id)).toEqual(["telegram", "discord"]);
|
||||
});
|
||||
|
||||
it("unsubscribes lifecycle listeners and disposes bundle runtimes during shutdown", async () => {
|
||||
const lifecycleUnsub = vi.fn();
|
||||
const transcriptUnsub = vi.fn();
|
||||
|
||||
@@ -175,6 +175,7 @@ export function createGatewayCloseHandler(params: {
|
||||
canvasHost: CanvasHostHandler | null;
|
||||
canvasHostServer: CanvasHostServer | null;
|
||||
releasePluginRouteRegistry?: (() => void) | null;
|
||||
channelIds?: readonly ChannelId[];
|
||||
stopChannel: (name: ChannelId, accountId?: string) => Promise<void>;
|
||||
pluginServices: PluginServicesHandle | null;
|
||||
disposeSessionMcpRuntimes?: () => Promise<void>;
|
||||
@@ -270,8 +271,9 @@ export function createGatewayCloseHandler(params: {
|
||||
if (params.canvasHostServer) {
|
||||
await shutdownStep("canvas-host-server", () => params.canvasHostServer!.close(), warnings);
|
||||
}
|
||||
for (const plugin of listChannelPlugins()) {
|
||||
await shutdownStep(`channel/${plugin.id}`, () => params.stopChannel(plugin.id), warnings);
|
||||
const channelIds = params.channelIds ?? listChannelPlugins().map((plugin) => plugin.id);
|
||||
for (const channelId of channelIds) {
|
||||
await shutdownStep(`channel/${channelId}`, () => params.stopChannel(channelId), warnings);
|
||||
}
|
||||
await shutdownStep("agent-harnesses", () => disposeRegisteredAgentHarnesses(), warnings);
|
||||
await Promise.all([
|
||||
|
||||
@@ -169,10 +169,10 @@ async function closeMcpLoopbackServerOnDemand(): Promise<void> {
|
||||
await closeMcpLoopbackServer();
|
||||
}
|
||||
|
||||
let gatewayCloseModulePromise: Promise<typeof import("./server-close.js")> | null = null;
|
||||
let gatewayCloseModulePromise: Promise<typeof import("./server-close.runtime.js")> | null = null;
|
||||
|
||||
function loadGatewayCloseModule(): Promise<typeof import("./server-close.js")> {
|
||||
gatewayCloseModulePromise ??= import("./server-close.js");
|
||||
function loadGatewayCloseModule(): Promise<typeof import("./server-close.runtime.js")> {
|
||||
gatewayCloseModulePromise ??= import("./server-close.runtime.js");
|
||||
return gatewayCloseModulePromise;
|
||||
}
|
||||
|
||||
@@ -925,6 +925,7 @@ export async function startGatewayServer(
|
||||
});
|
||||
const createCloseHandler =
|
||||
() => async (opts?: { reason?: string; restartExpectedMs?: number | null }) => {
|
||||
const channelIds = listLoadedChannelPlugins().map((plugin) => plugin.id as ChannelId);
|
||||
const { createGatewayCloseHandler } = await loadGatewayCloseModule();
|
||||
await createGatewayCloseHandler({
|
||||
bonjourStop: runtimeState.bonjourStop,
|
||||
@@ -932,6 +933,7 @@ export async function startGatewayServer(
|
||||
canvasHost,
|
||||
canvasHostServer,
|
||||
releasePluginRouteRegistry,
|
||||
channelIds,
|
||||
stopChannel,
|
||||
pluginServices: runtimeState.pluginServices,
|
||||
cron: runtimeState.cronState.cron,
|
||||
|
||||
@@ -197,6 +197,36 @@ describe("runtime postbuild static assets", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("rewrites gateway shutdown imports to stable runtime aliases", async () => {
|
||||
const rootDir = createTempDir("openclaw-runtime-postbuild-");
|
||||
const distDir = path.join(rootDir, "dist");
|
||||
await fs.mkdir(distDir, { recursive: true });
|
||||
await fs.writeFile(
|
||||
path.join(distDir, "server-close.runtime-AbCd1234.js"),
|
||||
"export const close = true;\n",
|
||||
"utf8",
|
||||
);
|
||||
await fs.writeFile(
|
||||
path.join(distDir, "server.impl-OldHash.js"),
|
||||
[
|
||||
'const closeModule = () => import("./server-close.runtime-AbCd1234.js");',
|
||||
'const ordinaryChunk = () => import("./server-close-OldHash.js");',
|
||||
"",
|
||||
].join("\n"),
|
||||
"utf8",
|
||||
);
|
||||
|
||||
rewriteRootRuntimeImportsToStableAliases({ rootDir });
|
||||
|
||||
expect(await fs.readFile(path.join(distDir, "server.impl-OldHash.js"), "utf8")).toBe(
|
||||
[
|
||||
'const closeModule = () => import("./server-close.runtime.js");',
|
||||
'const ordinaryChunk = () => import("./server-close-OldHash.js");',
|
||||
"",
|
||||
].join("\n"),
|
||||
);
|
||||
});
|
||||
|
||||
it("keeps hashed imports when a stable runtime alias would collide", async () => {
|
||||
const rootDir = createTempDir("openclaw-runtime-postbuild-");
|
||||
const distDir = path.join(rootDir, "dist");
|
||||
@@ -274,6 +304,26 @@ describe("runtime postbuild static assets", () => {
|
||||
).toBe('export * from "./runtime-plugins.runtime.js";\n');
|
||||
});
|
||||
|
||||
it("writes compatibility aliases for previous gateway shutdown chunk names", async () => {
|
||||
const rootDir = createTempDir("openclaw-runtime-postbuild-");
|
||||
const distDir = path.join(rootDir, "dist");
|
||||
await fs.mkdir(distDir, { recursive: true });
|
||||
await fs.writeFile(
|
||||
path.join(distDir, "server-close.runtime.js"),
|
||||
'export * from "./server-close.runtime-NewHash.js";\n',
|
||||
"utf8",
|
||||
);
|
||||
|
||||
writeLegacyRootRuntimeCompatAliases({ rootDir });
|
||||
|
||||
expect(await fs.readFile(path.join(distDir, "server-close-DsVPJDIx.js"), "utf8")).toBe(
|
||||
'export * from "./server-close.runtime.js";\n',
|
||||
);
|
||||
expect(await fs.readFile(path.join(distDir, "server-close-DvAvfgr8.js"), "utf8")).toBe(
|
||||
'export * from "./server-close.runtime.js";\n',
|
||||
);
|
||||
});
|
||||
|
||||
it("writes legacy CLI exit compatibility chunks", async () => {
|
||||
const rootDir = createTempDir("openclaw-runtime-postbuild-");
|
||||
|
||||
|
||||
@@ -204,6 +204,7 @@ function buildCoreDistEntries(): Record<string, string> {
|
||||
"agents/model-catalog.runtime": "src/agents/model-catalog.runtime.ts",
|
||||
"agents/models-config.runtime": "src/agents/models-config.runtime.ts",
|
||||
"cli/gateway-lifecycle.runtime": "src/cli/gateway-cli/lifecycle.runtime.ts",
|
||||
"server-close.runtime": "src/gateway/server-close.runtime.ts",
|
||||
"plugins/memory-state": "src/plugins/memory-state.ts",
|
||||
"subagent-registry.runtime": "src/agents/subagent-registry.runtime.ts",
|
||||
"task-registry-control.runtime": "src/tasks/task-registry-control.runtime.ts",
|
||||
|
||||
Reference in New Issue
Block a user