diff --git a/CHANGELOG.md b/CHANGELOG.md index aae4ddfd4c3..7e3821ce06b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. diff --git a/src/gateway/server-channels.test.ts b/src/gateway/server-channels.test.ts index 01ec8e4fdb8..224adfa6acb 100644 --- a/src/gateway/server-channels.test.ts +++ b/src/gateway/server-channels.test.ts @@ -116,7 +116,7 @@ function installTestRegistry(...plugins: ChannelPlugin[]) { function createManager(options?: { channelRuntime?: PluginRuntime["channel"]; - resolveChannelRuntime?: () => PluginRuntime["channel"]; + resolveChannelRuntime?: () => PluginRuntime["channel"] | Promise; loadConfig?: () => Record; 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) => {}); + + 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({ diff --git a/src/gateway/server-channels.ts b/src/gateway/server-channels.ts index 765f170267e..56131c7575b 100644 --- a/src/gateway/server-channels.ts +++ b/src/gateway/server-channels.ts @@ -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; }; 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 => { + 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); } diff --git a/src/gateway/server.impl.ts b/src/gateway/server.impl.ts index ed2e4b993a7..4ea4d44fd4d 100644 --- a/src/gateway/server.impl.ts +++ b/src/gateway/server.impl.ts @@ -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 | null = null; function getChannelRuntime() { - cachedChannelRuntime ??= createRuntimeChannel(); - return cachedChannelRuntime; + cachedChannelRuntimePromise ??= import("../plugins/runtime/runtime-channel.js").then( + ({ createRuntimeChannel }) => createRuntimeChannel(), + ); + return cachedChannelRuntimePromise; } async function closeMcpLoopbackServerOnDemand(): Promise {