fix(gateway): defer channel runtime imports

This commit is contained in:
Vincent Koc
2026-04-27 00:06:56 -07:00
parent 49ce7fe90c
commit 5333b1e2cc
4 changed files with 34 additions and 12 deletions

View File

@@ -55,6 +55,7 @@ Docs: https://docs.openclaw.ai
- Gateway/startup: keep node session runtime on a lightweight JSON parser instead of importing gateway method validation helpers during boot. Thanks @vincentkoc.
- Gateway/startup: read embedded-run activity from a lightweight shared state module so restart deferral no longer imports the embedded runner during Gateway boot. Thanks @vincentkoc.
- Gateway/startup: defer MCP loopback server imports until Gateway shutdown so normal boot no longer loads the loopback HTTP/tool schema stack just to register close handlers. Thanks @vincentkoc.
- Gateway/startup: resolve channel runtime helpers asynchronously only when an enabled/configured channel starts, so no-channel Gateway boot skips auto-reply, media, pairing, and outbound channel helper imports. Thanks @vincentkoc.
- CLI/Gateway: use a parse-only config snapshot for plain `gateway status` reads and reuse same-path service config context so status no longer spends tens of seconds in full config validation before printing. Thanks @vincentkoc.
- Lobster/Gateway: memoize repeated Ajv schema compilation before loading the embedded Lobster runtime so scheduled workflows and `llm.invoke` loops stop growing gateway heap on content-identical schemas. Fixes #71148. Thanks @cmi525, @vsolaz, and @vincentkoc.
- Codex harness: normalize cached input tokens before session/context accounting so prompt cache reads are not double-counted in `/status`, `session_status`, or persisted `sessionEntry.totalTokens`. Fixes #69298. Thanks @richardmqq.

View File

@@ -116,7 +116,7 @@ function installTestRegistry(...plugins: ChannelPlugin<TestAccount>[]) {
function createManager(options?: {
channelRuntime?: PluginRuntime["channel"];
resolveChannelRuntime?: () => PluginRuntime["channel"];
resolveChannelRuntime?: () => PluginRuntime["channel"] | Promise<PluginRuntime["channel"]>;
loadConfig?: () => Record<string, unknown>;
channelIds?: ChannelId[];
}) {
@@ -375,6 +375,25 @@ describe("server-channels auto restart", () => {
expect(ctx?.channelRuntime).not.toBe(channelRuntime);
});
it("does not resolve channelRuntime for disabled accounts", async () => {
const channelRuntime = createRuntimeChannel();
const resolveChannelRuntime = vi.fn(() => channelRuntime);
const startAccount = vi.fn(async (_ctx: ChannelGatewayContext<TestAccount>) => {});
installTestRegistry(
createTestPlugin({
startAccount,
account: { enabled: false, configured: true },
}),
);
const manager = createManager({ resolveChannelRuntime });
await manager.startChannels();
expect(resolveChannelRuntime).not.toHaveBeenCalled();
expect(startAccount).not.toHaveBeenCalled();
});
it("fails fast when channelRuntime is not a full plugin runtime surface", async () => {
installTestRegistry(createTestPlugin({ startAccount: vi.fn(async () => {}) }));
const manager = createManager({

View File

@@ -160,7 +160,7 @@ type ChannelManagerOptions = {
* a channel account actually starts. The resolved value must be a real
* `createPluginRuntime().channel` surface.
*/
resolveChannelRuntime?: () => ChannelRuntimeSurface;
resolveChannelRuntime?: () => ChannelRuntimeSurface | Promise<ChannelRuntimeSurface>;
};
type StartChannelOptions = {
@@ -278,8 +278,8 @@ export function createChannelManager(opts: ChannelManagerOptions): ChannelManage
return next;
};
const getChannelRuntime = (): ChannelRuntimeSurface | undefined => {
return channelRuntime ?? resolveChannelRuntime?.();
const getChannelRuntime = async (): Promise<ChannelRuntimeSurface | undefined> => {
return channelRuntime ?? (await resolveChannelRuntime?.());
};
const evictStaleChannelAccountState = (
@@ -368,10 +368,6 @@ export function createChannelManager(opts: ChannelManagerOptions): ChannelManage
};
try {
scopedChannelRuntime = createTaskScopedChannelRuntime({
channelRuntime: getChannelRuntime(),
});
channelRuntimeForTask = scopedChannelRuntime.channelRuntime;
const account = plugin.config.resolveAccount(cfg, id);
const enabled = plugin.config.isEnabled
? plugin.config.isEnabled(account, cfg)
@@ -419,6 +415,11 @@ export function createChannelManager(opts: ChannelManagerOptions): ChannelManage
return;
}
scopedChannelRuntime = createTaskScopedChannelRuntime({
channelRuntime: await getChannelRuntime(),
});
channelRuntimeForTask = scopedChannelRuntime.channelRuntime;
if (!preserveRestartAttempts) {
restartAttempts.delete(rKey);
}

View File

@@ -32,7 +32,6 @@ import { startDiagnosticHeartbeat, stopDiagnosticHeartbeat } from "../logging/di
import { createSubsystemLogger, runtimeForLogger } from "../logging/subsystem.js";
import { getActiveBundledRuntimeDepsInstallCount } from "../plugins/bundled-runtime-deps-activity.js";
import { runGlobalGatewayStopSafely } from "../plugins/hook-runner-global.js";
import { createRuntimeChannel } from "../plugins/runtime/runtime-channel.js";
import type { PluginRuntime } from "../plugins/runtime/types.js";
import { getTotalQueueSize } from "../process/command-queue.js";
import type { RuntimeEnv } from "../runtime.js";
@@ -117,11 +116,13 @@ const logDiscovery = log.child("discovery");
const logTailscale = log.child("tailscale");
const logChannels = log.child("channels");
let cachedChannelRuntime: PluginRuntime["channel"] | null = null;
let cachedChannelRuntimePromise: Promise<PluginRuntime["channel"]> | null = null;
function getChannelRuntime() {
cachedChannelRuntime ??= createRuntimeChannel();
return cachedChannelRuntime;
cachedChannelRuntimePromise ??= import("../plugins/runtime/runtime-channel.js").then(
({ createRuntimeChannel }) => createRuntimeChannel(),
);
return cachedChannelRuntimePromise;
}
async function closeMcpLoopbackServerOnDemand(): Promise<void> {