fix(gateway): defer startup runtime imports

This commit is contained in:
Vincent Koc
2026-04-26 21:50:29 -07:00
parent f4129cdd2b
commit dc8b881c11
12 changed files with 84 additions and 45 deletions

View File

@@ -25,6 +25,7 @@ Docs: https://docs.openclaw.ai
- Exec/node: synthesize a local approval plan when a paired node advertises `system.run` without `system.run.prepare`, unblocking approval-required `host=node` exec on current macOS companion nodes while preserving remote prepare for node hosts that support it. Fixes #37591 and duplicate #66839; carries forward #69725. Thanks @soloclz.
- Memory/QMD: prefer QMD's `--mask` collection pattern flag so root memory indexing stays scoped to `MEMORY.md` instead of widening to every markdown file in the workspace. Thanks @codex.
- Gateway/memory: defer QMD startup for implicit non-default agents and scope memory runtime loading to the selected memory slot so Gateway boot and first memory recall avoid broad plugin runtime fanout. Thanks @vincentkoc.
- Gateway/startup: keep core request handlers and channel runtime helpers off the boot path until the first matching request or channel start, reducing no-plugin Gateway ready RSS and avoidable startup 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

@@ -0,0 +1,25 @@
export const NODE_WAKE_RECONNECT_WAIT_MS = 3_000;
export const NODE_WAKE_RECONNECT_RETRY_WAIT_MS = 12_000;
export const NODE_WAKE_RECONNECT_POLL_MS = 150;
export type NodeWakeAttempt = {
available: boolean;
throttled: boolean;
path: "throttled" | "no-registration" | "no-auth" | "sent" | "send-error";
durationMs: number;
apnsStatus?: number;
apnsReason?: string;
};
export type NodeWakeState = {
lastWakeAtMs: number;
inFlight?: Promise<NodeWakeAttempt>;
};
export const nodeWakeById = new Map<string, NodeWakeState>();
export const nodeWakeNudgeById = new Map<string, number>();
export function clearNodeWakeState(nodeId: string): void {
nodeWakeById.delete(nodeId);
nodeWakeNudgeById.delete(nodeId);
}

View File

@@ -47,6 +47,14 @@ import {
validateNodePairVerifyParams,
validateNodeRenameParams,
} from "../protocol/index.js";
import {
NODE_WAKE_RECONNECT_POLL_MS,
NODE_WAKE_RECONNECT_RETRY_WAIT_MS,
NODE_WAKE_RECONNECT_WAIT_MS,
nodeWakeById,
nodeWakeNudgeById,
type NodeWakeAttempt,
} from "./nodes-wake-state.js";
import { handleNodeInvokeResult } from "./nodes.handlers.invoke-result.js";
import {
respondInvalidParams,
@@ -56,31 +64,18 @@ import {
} from "./nodes.helpers.js";
import type { GatewayRequestHandlers } from "./types.js";
export const NODE_WAKE_RECONNECT_WAIT_MS = 3_000;
export const NODE_WAKE_RECONNECT_RETRY_WAIT_MS = 12_000;
export const NODE_WAKE_RECONNECT_POLL_MS = 150;
export {
clearNodeWakeState,
NODE_WAKE_RECONNECT_POLL_MS,
NODE_WAKE_RECONNECT_RETRY_WAIT_MS,
NODE_WAKE_RECONNECT_WAIT_MS,
} from "./nodes-wake-state.js";
const NODE_WAKE_THROTTLE_MS = 15_000;
const NODE_WAKE_NUDGE_THROTTLE_MS = 10 * 60_000;
const NODE_PENDING_ACTION_TTL_MS = 10 * 60_000;
const NODE_PENDING_ACTION_MAX_PER_NODE = 64;
type NodeWakeState = {
lastWakeAtMs: number;
inFlight?: Promise<NodeWakeAttempt>;
};
const nodeWakeById = new Map<string, NodeWakeState>();
const nodeWakeNudgeById = new Map<string, number>();
type NodeWakeAttempt = {
available: boolean;
throttled: boolean;
path: "throttled" | "no-registration" | "no-auth" | "sent" | "send-error";
durationMs: number;
apnsStatus?: number;
apnsReason?: string;
};
type NodeWakeNudgeAttempt = {
sent: boolean;
throttled: boolean;
@@ -518,15 +513,6 @@ export async function waitForNodeReconnect(params: {
return Boolean(params.context.nodeRegistry.get(params.nodeId));
}
/**
* Remove cached wake/nudge state for a node that has disconnected.
* Called from the WS close handler to prevent unbounded growth.
*/
export function clearNodeWakeState(nodeId: string): void {
nodeWakeById.delete(nodeId);
nodeWakeNudgeById.delete(nodeId);
}
export const nodeHandlers: GatewayRequestHandlers = {
"node.pair.request": async ({ params, respond, context }) => {
if (!validateNodePairRequestParams(params)) {

View File

@@ -27,7 +27,8 @@ type GatewayPluginBootstrapParams = {
activationSourceConfig?: OpenClawConfig;
workspaceDir: string;
log: GatewayPluginBootstrapLog;
coreGatewayHandlers: Record<string, GatewayRequestHandler>;
coreGatewayHandlers?: Record<string, GatewayRequestHandler>;
coreGatewayMethodNames?: readonly string[];
baseMethods: string[];
pluginIds?: string[];
preferSetupRuntimeForChannelPlugins?: boolean;
@@ -78,7 +79,12 @@ export function prepareGatewayPluginLoad(params: GatewayPluginBootstrapParams) {
autoEnabledReasons: autoEnabled.autoEnabledReasons,
workspaceDir: params.workspaceDir,
log: params.log,
coreGatewayHandlers: params.coreGatewayHandlers,
...(params.coreGatewayHandlers !== undefined && {
coreGatewayHandlers: params.coreGatewayHandlers,
}),
...(params.coreGatewayMethodNames !== undefined && {
coreGatewayMethodNames: params.coreGatewayMethodNames,
}),
baseMethods: params.baseMethods,
pluginIds: params.pluginIds,
preferSetupRuntimeForChannelPlugins: params.preferSetupRuntimeForChannelPlugins,

View File

@@ -16,7 +16,6 @@ import { ADMIN_SCOPE, WRITE_SCOPE } from "./method-scopes.js";
import { GATEWAY_CLIENT_IDS, GATEWAY_CLIENT_MODES } from "./protocol/client-info.js";
import type { ErrorShape } from "./protocol/index.js";
import { PROTOCOL_VERSION } from "./protocol/index.js";
import { handleGatewayRequest } from "./server-methods.js";
import type {
GatewayRequestContext,
GatewayRequestHandler,
@@ -266,6 +265,7 @@ async function dispatchGatewayMethod<T>(
}
let result: { ok: boolean; payload?: unknown; error?: ErrorShape } | undefined;
const { handleGatewayRequest } = await import("./server-methods.js");
await handleGatewayRequest({
req: {
type: "req",
@@ -442,7 +442,8 @@ export function loadGatewayPlugins(params: {
error: (msg: string) => void;
debug: (msg: string) => void;
};
coreGatewayHandlers: Record<string, GatewayRequestHandler>;
coreGatewayHandlers?: Record<string, GatewayRequestHandler>;
coreGatewayMethodNames?: readonly string[];
baseMethods: string[];
pluginIds?: string[];
preferSetupRuntimeForChannelPlugins?: boolean;
@@ -499,7 +500,12 @@ export function loadGatewayPlugins(params: {
logger: createGatewayPluginRegistrationLogger({
suppressInfoLogs: params.suppressPluginInfoLogs,
}),
coreGatewayHandlers: params.coreGatewayHandlers,
...(params.coreGatewayHandlers !== undefined && {
coreGatewayHandlers: params.coreGatewayHandlers,
}),
...(params.coreGatewayMethodNames !== undefined && {
coreGatewayMethodNames: params.coreGatewayMethodNames,
}),
runtimeOptions: {
allowGatewaySubagentBinding: true,
},

View File

@@ -10,7 +10,6 @@ import {
import { createEmptyPluginRegistry } from "../plugins/registry.js";
import { getActivePluginRegistry, setActivePluginRegistry } from "../plugins/runtime.js";
import { listGatewayMethods } from "./server-methods-list.js";
import { coreGatewayHandlers } from "./server-methods.js";
import { loadGatewayStartupPlugins } from "./server-plugin-bootstrap.js";
import { runStartupSessionMigration } from "./server-startup-session-migration.js";
@@ -94,7 +93,7 @@ export async function prepareGatewayPluginBootstrap(params: {
activationSourceConfig: params.cfgAtStart,
workspaceDir: defaultWorkspaceDir,
log: params.log,
coreGatewayHandlers,
coreGatewayMethodNames: baseMethods,
baseMethods,
pluginIds: startupPluginIds,
preferSetupRuntimeForChannelPlugins: deferredConfiguredChannelPluginIds.length > 0,

View File

@@ -31,7 +31,8 @@ import type { VoiceWakeRoutingConfig } from "../infra/voicewake-routing.js";
import { startDiagnosticHeartbeat, stopDiagnosticHeartbeat } from "../logging/diagnostic.js";
import { createSubsystemLogger, runtimeForLogger } from "../logging/subsystem.js";
import { runGlobalGatewayStopSafely } from "../plugins/hook-runner-global.js";
import { createPluginRuntime } from "../plugins/runtime/index.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";
import {
@@ -54,7 +55,6 @@ import { buildGatewayCronService } from "./server-cron.js";
import { applyGatewayLaneConcurrency } from "./server-lanes.js";
import { createGatewayServerLiveState, type GatewayServerLiveState } from "./server-live-state.js";
import { GATEWAY_EVENTS } from "./server-methods-list.js";
import { coreGatewayHandlers } from "./server-methods.js";
import { loadGatewayModelCatalog } from "./server-model-catalog.js";
import { bootstrapGatewayNetworkRuntime } from "./server-network-runtime.js";
import { createGatewayNodeSessionRuntime } from "./server-node-session-runtime.js";
@@ -118,10 +118,10 @@ const logDiscovery = log.child("discovery");
const logTailscale = log.child("tailscale");
const logChannels = log.child("channels");
let cachedChannelRuntime: ReturnType<typeof createPluginRuntime>["channel"] | null = null;
let cachedChannelRuntime: PluginRuntime["channel"] | null = null;
function getChannelRuntime() {
cachedChannelRuntime ??= createPluginRuntime().channel;
cachedChannelRuntime ??= createRuntimeChannel();
return cachedChannelRuntime;
}
@@ -788,7 +788,7 @@ export async function startGatewayServer(
cfg: gatewayPluginConfigAtStart,
workspaceDir: defaultWorkspaceDir,
log,
coreGatewayHandlers,
coreGatewayMethodNames: baseMethods,
baseMethods,
pluginIds: startupPluginIds,
logDiagnostics: false,

View File

@@ -14,7 +14,7 @@ import type { ResolvedGatewayAuth } from "../auth.js";
import { getPreauthHandshakeTimeoutMsFromEnv } from "../handshake-timeouts.js";
import { isLoopbackAddress } from "../net.js";
import { MAX_PAYLOAD_BYTES, MAX_PREAUTH_PAYLOAD_BYTES } from "../server-constants.js";
import { clearNodeWakeState } from "../server-methods/nodes.js";
import { clearNodeWakeState } from "../server-methods/nodes-wake-state.js";
import type { GatewayRequestContext, GatewayRequestHandlers } from "../server-methods/types.js";
import { formatError } from "../server-utils.js";
import { logWs } from "../ws-log.js";

View File

@@ -103,7 +103,6 @@ import {
MAX_PREAUTH_PAYLOAD_BYTES,
TICK_INTERVAL_MS,
} from "../../server-constants.js";
import { handleGatewayRequest } from "../../server-methods.js";
import type { GatewayRequestContext, GatewayRequestHandlers } from "../../server-methods/types.js";
import { formatError } from "../../server-utils.js";
import { formatForLog, logWs } from "../../ws-log.js";
@@ -1561,6 +1560,7 @@ export function attachGatewayWsMessageHandler(params: {
};
void (async () => {
const { handleGatewayRequest } = await import("../../server-methods.js");
await handleGatewayRequest({
req,
respond,

View File

@@ -147,6 +147,7 @@ export type PluginLoadOptions = {
env?: NodeJS.ProcessEnv;
logger?: PluginLogger;
coreGatewayHandlers?: Record<string, GatewayRequestHandler>;
coreGatewayMethodNames?: readonly string[];
runtimeOptions?: CreatePluginRuntimeOptions;
pluginSdkResolution?: PluginSdkResolutionPreference;
cache?: boolean;
@@ -1200,7 +1201,12 @@ function resolvePluginLoadCacheContext(options: PluginLoadOptions = {}) {
const preferSetupRuntimeForChannelPlugins = options.preferSetupRuntimeForChannelPlugins === true;
const shouldInstallBundledRuntimeDeps = options.installBundledRuntimeDeps !== false;
const runtimeSubagentMode = resolveRuntimeSubagentMode(options.runtimeOptions);
const coreGatewayMethodNames = Object.keys(options.coreGatewayHandlers ?? {}).toSorted();
const coreGatewayMethodNames = Array.from(
new Set([
...(options.coreGatewayMethodNames ?? []),
...Object.keys(options.coreGatewayHandlers ?? {}),
]),
).toSorted();
const installRecords = {
...loadInstalledPluginIndexInstallRecordsSync({ env }),
...cfg.plugins?.installs,
@@ -2286,6 +2292,9 @@ export function loadOpenClawPlugins(options: PluginLoadOptions = {}): PluginRegi
logger,
runtime,
coreGatewayHandlers: options.coreGatewayHandlers as Record<string, GatewayRequestHandler>,
...(options.coreGatewayMethodNames !== undefined && {
coreGatewayMethodNames: options.coreGatewayMethodNames,
}),
activateGlobalSideEffects: shouldActivate,
});
@@ -3189,6 +3198,9 @@ export async function loadOpenClawPluginCliRegistry(
logger,
runtime: {} as PluginRuntime,
coreGatewayHandlers: options.coreGatewayHandlers as Record<string, GatewayRequestHandler>,
...(options.coreGatewayMethodNames !== undefined && {
coreGatewayMethodNames: options.coreGatewayMethodNames,
}),
activateGlobalSideEffects: false,
});

View File

@@ -336,6 +336,7 @@ export type PluginRegistry = {
export type PluginRegistryParams = {
logger: PluginLogger;
coreGatewayHandlers?: GatewayRequestHandlers;
coreGatewayMethodNames?: readonly string[];
runtime: PluginRuntime;
activateGlobalSideEffects?: boolean;
};

View File

@@ -224,7 +224,10 @@ function resolvePluginRegistrationCapabilities(
export function createPluginRegistry(registryParams: PluginRegistryParams) {
const registry = createEmptyPluginRegistry();
const coreGatewayMethods = new Set(Object.keys(registryParams.coreGatewayHandlers ?? {}));
const coreGatewayMethods = new Set([
...(registryParams.coreGatewayMethodNames ?? []),
...Object.keys(registryParams.coreGatewayHandlers ?? {}),
]);
const pluginHookRollback = new Map<string, HookRollbackEntry[]>();
const pluginsWithChannelRegistrationConflict = new Set<string>();