mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 10:20:42 +00:00
fix(gateway): defer startup runtime imports
This commit is contained in:
@@ -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.
|
||||
|
||||
25
src/gateway/server-methods/nodes-wake-state.ts
Normal file
25
src/gateway/server-methods/nodes-wake-state.ts
Normal 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);
|
||||
}
|
||||
@@ -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)) {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
|
||||
@@ -336,6 +336,7 @@ export type PluginRegistry = {
|
||||
export type PluginRegistryParams = {
|
||||
logger: PluginLogger;
|
||||
coreGatewayHandlers?: GatewayRequestHandlers;
|
||||
coreGatewayMethodNames?: readonly string[];
|
||||
runtime: PluginRuntime;
|
||||
activateGlobalSideEffects?: boolean;
|
||||
};
|
||||
|
||||
@@ -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>();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user