From 674d188153aee9547fa964bf961355b01ad786eb Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Sat, 25 Apr 2026 05:37:14 -0700 Subject: [PATCH] feat(plugins): plan gateway startup from registry --- CHANGELOG.md | 1 + src/cli/plugins-cli-test-helpers.ts | 2 +- .../shared/plugin-registry-migration.test.ts | 54 ++++++++- .../shared/plugin-registry-migration.ts | 16 ++- src/plugins/channel-plugin-ids.test.ts | 8 ++ src/plugins/gateway-startup-plugin-ids.ts | 105 +++++++----------- .../installed-plugin-index-store.test.ts | 8 +- src/plugins/installed-plugin-index-store.ts | 10 ++ src/plugins/installed-plugin-index.test.ts | 4 +- src/plugins/installed-plugin-index.ts | 40 ++++++- src/plugins/plugin-registry.test.ts | 8 +- 11 files changed, 175 insertions(+), 81 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 77c2c4c7167..5869353fe3e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ Docs: https://docs.openclaw.ai - Diagnostics/OTEL: align model-call GenAI span attributes with OpenTelemetry stability opt-in semantics, keeping legacy `gen_ai.system` by default while emitting `gen_ai.provider.name` under `OTEL_SEMCONV_STABILITY_OPT_IN=gen_ai_latest_experimental`. Thanks @vincentkoc. - Plugins/CLI: add `openclaw plugins registry` for explicit persisted-registry inspection and `--refresh` repair without making normal startup rescan plugin locations. Thanks @vincentkoc. - Plugins/CLI: make `openclaw plugins list` read the cold persisted registry snapshot by default, leaving module-aware diagnostics to `plugins doctor` and `plugins inspect`. Thanks @vincentkoc. +- Plugins/startup: move gateway startup plugin planning onto the versioned cold registry index, with postinstall repair for older registry files that predate startup metadata. Thanks @vincentkoc. - Diagnostics/OTEL: add bounded outbound message delivery lifecycle diagnostics and export them as low-cardinality delivery spans/metrics without message body, recipient, room, or media-path data. (#71471) Thanks @vincentkoc and @jlapenna. - Diagnostics/OTEL: emit bounded exec-process diagnostics and export them as `openclaw.exec` spans without exposing command text, working directories, or container identifiers. (#71451) Thanks @vincentkoc and @jlapenna. - Diagnostics/OTEL: support `OPENCLAW_OTEL_PRELOADED=1` so the plugin can reuse an already-registered OpenTelemetry SDK while keeping OpenClaw diagnostic listeners wired. (#71450) Thanks @vincentkoc and @jlapenna. diff --git a/src/cli/plugins-cli-test-helpers.ts b/src/cli/plugins-cli-test-helpers.ts index 56322d84f56..f7dfbf5d71f 100644 --- a/src/cli/plugins-cli-test-helpers.ts +++ b/src/cli/plugins-cli-test-helpers.ts @@ -502,7 +502,7 @@ export function resetPluginsCliTestState() { version: 1, hostContractVersion: "2026.4.25", compatRegistryVersion: "compat-v1", - migrationVersion: 1, + migrationVersion: 2, policyHash: "policy-v1", generatedAtMs: 1777118400000, plugins: [], diff --git a/src/commands/doctor/shared/plugin-registry-migration.test.ts b/src/commands/doctor/shared/plugin-registry-migration.test.ts index 641e6902d3a..7c972116bd8 100644 --- a/src/commands/doctor/shared/plugin-registry-migration.test.ts +++ b/src/commands/doctor/shared/plugin-registry-migration.test.ts @@ -2,7 +2,11 @@ import fs from "node:fs"; import path from "node:path"; import { afterEach, describe, expect, it, vi } from "vitest"; import type { PluginCandidate } from "../../../plugins/discovery.js"; -import { readPersistedInstalledPluginIndex } from "../../../plugins/installed-plugin-index-store.js"; +import { + readPersistedInstalledPluginIndex, + writePersistedInstalledPluginIndex, +} from "../../../plugins/installed-plugin-index-store.js"; +import type { InstalledPluginIndex } from "../../../plugins/installed-plugin-index.js"; import { cleanupTrackedTempDirs, makeTrackedTempDir, @@ -58,12 +62,24 @@ function createCandidate(rootDir: string, id = "demo"): PluginCandidate { }; } +function createCurrentIndex(): InstalledPluginIndex { + return { + version: 1, + hostContractVersion: "2026.4.25", + compatRegistryVersion: "compat-v1", + migrationVersion: 2, + policyHash: "policy-v1", + generatedAtMs: 1777118400000, + plugins: [], + diagnostics: [], + }; +} + describe("plugin registry install migration", () => { - it("short-circuits when a registry file already exists", async () => { + it("short-circuits when a current registry file already exists", async () => { const stateDir = makeTempDir(); const filePath = path.join(stateDir, "plugins", "installed-index.json"); - fs.mkdirSync(path.dirname(filePath), { recursive: true }); - fs.writeFileSync(filePath, "{}\n", "utf8"); + await writePersistedInstalledPluginIndex(createCurrentIndex(), { stateDir }); const readConfig = vi.fn(async () => ({})); await expect( @@ -83,6 +99,34 @@ describe("plugin registry install migration", () => { expect(readConfig).not.toHaveBeenCalled(); }); + it("migrates when an existing registry file is not current", async () => { + const stateDir = makeTempDir(); + const filePath = path.join(stateDir, "plugins", "installed-index.json"); + const pluginDir = path.join(stateDir, "plugins", "demo"); + fs.mkdirSync(path.dirname(filePath), { recursive: true }); + fs.mkdirSync(pluginDir, { recursive: true }); + fs.writeFileSync(filePath, JSON.stringify({ version: 1, migrationVersion: 1 }), "utf8"); + + await expect( + migratePluginRegistryForInstall({ + stateDir, + candidates: [createCandidate(pluginDir)], + readConfig: async () => ({}), + env: hermeticEnv(), + }), + ).resolves.toMatchObject({ + status: "migrated", + preflight: { + action: "migrate", + }, + }); + + await expect(readPersistedInstalledPluginIndex({ stateDir })).resolves.toMatchObject({ + migrationVersion: 2, + plugins: [expect.objectContaining({ pluginId: "demo" })], + }); + }); + it("persists only plugins enabled by the central config policy", async () => { const stateDir = makeTempDir(); const enabledDir = path.join(stateDir, "plugins", "enabled-demo"); @@ -172,7 +216,7 @@ describe("plugin registry install migration", () => { migrated: true, current: { refreshReason: "migration", - migrationVersion: 1, + migrationVersion: 2, plugins: [ expect.objectContaining({ pluginId: "demo", diff --git a/src/commands/doctor/shared/plugin-registry-migration.ts b/src/commands/doctor/shared/plugin-registry-migration.ts index 53411376af3..bdefe872a0f 100644 --- a/src/commands/doctor/shared/plugin-registry-migration.ts +++ b/src/commands/doctor/shared/plugin-registry-migration.ts @@ -2,6 +2,7 @@ import fs from "node:fs"; import type { OpenClawConfig } from "../../../config/types.openclaw.js"; import { inspectPersistedInstalledPluginIndex, + readPersistedInstalledPluginIndexSync, resolveInstalledPluginIndexStorePath, writePersistedInstalledPluginIndex, type InstalledPluginIndexStoreInspection, @@ -75,12 +76,15 @@ export function preflightPluginRegistryInstallMigration( } const pathExists = params.existsSync ?? fs.existsSync; if (!force && pathExists(filePath)) { - return { - action: "skip-existing", - filePath, - force, - deprecationWarnings, - }; + const currentRegistry = readPersistedInstalledPluginIndexSync(params); + if (currentRegistry) { + return { + action: "skip-existing", + filePath, + force, + deprecationWarnings, + }; + } } return { action: "migrate", diff --git a/src/plugins/channel-plugin-ids.test.ts b/src/plugins/channel-plugin-ids.test.ts index 3c66a73fafa..285e37e220d 100644 --- a/src/plugins/channel-plugin-ids.test.ts +++ b/src/plugins/channel-plugin-ids.test.ts @@ -31,6 +31,14 @@ vi.mock("./manifest-registry.js", async (importOriginal) => { }; }); +vi.mock("./installed-plugin-index-store.js", async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + readPersistedInstalledPluginIndexSync: vi.fn(() => null), + }; +}); + import { hasConfiguredChannelsForReadOnlyScope, listConfiguredAnnounceChannelIdsForConfig, diff --git a/src/plugins/gateway-startup-plugin-ids.ts b/src/plugins/gateway-startup-plugin-ids.ts index 1b65be49cf4..a3fe80362ce 100644 --- a/src/plugins/gateway-startup-plugin-ids.ts +++ b/src/plugins/gateway-startup-plugin-ids.ts @@ -8,7 +8,6 @@ import { resolveMemoryDreamingPluginId, } from "../memory-host-sdk/dreaming.js"; import { normalizeOptionalLowercaseString } from "../shared/string-coerce.js"; -import { resolveManifestActivationPluginIds } from "./activation-planner.js"; import { hasExplicitChannelConfig } from "./channel-presence-policy.js"; import { createPluginActivationSource, @@ -16,8 +15,8 @@ import { normalizePluginsConfig, resolveEffectivePluginActivationState, } from "./config-state.js"; -import { loadPluginManifestRegistry, type PluginManifestRecord } from "./manifest-registry.js"; -import { hasKind } from "./slots.js"; +import type { InstalledPluginIndexRecord } from "./installed-plugin-index.js"; +import { loadPluginRegistrySnapshot } from "./plugin-registry.js"; function listDisabledChannelIds(config: OpenClawConfig): Set { const channels = config.channels; @@ -46,30 +45,12 @@ function listPotentialEnabledChannelIds(config: OpenClawConfig, env: NodeJS.Proc .filter((id) => id && !disabled.has(id)); } -function hasRuntimeContractSurface(plugin: PluginManifestRecord): boolean { - return Boolean( - plugin.providers.length > 0 || - plugin.cliBackends.length > 0 || - plugin.contracts?.speechProviders?.length || - plugin.contracts?.mediaUnderstandingProviders?.length || - plugin.contracts?.documentExtractors?.length || - plugin.contracts?.imageGenerationProviders?.length || - plugin.contracts?.videoGenerationProviders?.length || - plugin.contracts?.musicGenerationProviders?.length || - plugin.contracts?.webContentExtractors?.length || - plugin.contracts?.webFetchProviders?.length || - plugin.contracts?.webSearchProviders?.length || - plugin.contracts?.memoryEmbeddingProviders?.length || - hasKind(plugin.kind, "memory"), - ); +function isGatewayStartupMemoryPlugin(plugin: InstalledPluginIndexRecord): boolean { + return plugin.startup.memory; } -function isGatewayStartupMemoryPlugin(plugin: PluginManifestRecord): boolean { - return hasKind(plugin.kind, "memory"); -} - -function isGatewayStartupSidecar(plugin: PluginManifestRecord): boolean { - return plugin.channels.length === 0 && !hasRuntimeContractSurface(plugin); +function isGatewayStartupSidecar(plugin: InstalledPluginIndexRecord): boolean { + return plugin.startup.sidecar; } function resolveGatewayStartupDreamingPluginIds(config: OpenClawConfig): Set { @@ -92,7 +73,7 @@ function resolveExplicitMemorySlotStartupPluginId(config: OpenClawConfig): strin } function shouldConsiderForGatewayStartup(params: { - plugin: PluginManifestRecord; + plugin: InstalledPluginIndexRecord; startupDreamingPluginIds: ReadonlySet; explicitMemorySlotStartupPluginId?: string; }): boolean { @@ -102,21 +83,23 @@ function shouldConsiderForGatewayStartup(params: { if (!isGatewayStartupMemoryPlugin(params.plugin)) { return false; } - if (params.startupDreamingPluginIds.has(params.plugin.id)) { + if (params.startupDreamingPluginIds.has(params.plugin.pluginId)) { return true; } - return params.explicitMemorySlotStartupPluginId === params.plugin.id; + return params.explicitMemorySlotStartupPluginId === params.plugin.pluginId; } function hasConfiguredStartupChannel(params: { - plugin: PluginManifestRecord; + plugin: InstalledPluginIndexRecord; configuredChannelIds: ReadonlySet; }): boolean { - return params.plugin.channels.some((channelId) => params.configuredChannelIds.has(channelId)); + return params.plugin.contributions.channels.some((channelId) => + params.configuredChannelIds.has(channelId), + ); } function canStartConfiguredChannelPlugin(params: { - plugin: PluginManifestRecord; + plugin: InstalledPluginIndexRecord; config: OpenClawConfig; pluginsConfig: ReturnType; activationSource: ReturnType; @@ -124,15 +107,15 @@ function canStartConfiguredChannelPlugin(params: { if (!params.pluginsConfig.enabled) { return false; } - if (params.pluginsConfig.deny.includes(params.plugin.id)) { + if (params.pluginsConfig.deny.includes(params.plugin.pluginId)) { return false; } - if (params.pluginsConfig.entries[params.plugin.id]?.enabled === false) { + if (params.pluginsConfig.entries[params.plugin.pluginId]?.enabled === false) { return false; } const explicitBundledChannelConfig = params.plugin.origin === "bundled" && - params.plugin.channels.some((channelId) => + params.plugin.contributions.channels.some((channelId) => hasExplicitChannelConfig({ config: params.activationSource.rootConfig ?? params.config, channelId, @@ -140,7 +123,7 @@ function canStartConfiguredChannelPlugin(params: { ); if ( params.pluginsConfig.allow.length > 0 && - !params.pluginsConfig.allow.includes(params.plugin.id) && + !params.pluginsConfig.allow.includes(params.plugin.pluginId) && !explicitBundledChannelConfig ) { return false; @@ -149,7 +132,7 @@ function canStartConfiguredChannelPlugin(params: { return true; } const activationState = resolveEffectivePluginActivationState({ - id: params.plugin.id, + id: params.plugin.pluginId, origin: params.plugin.origin, config: params.pluginsConfig, rootConfig: params.config, @@ -164,13 +147,14 @@ export function resolveChannelPluginIds(params: { workspaceDir?: string; env: NodeJS.ProcessEnv; }): string[] { - return loadPluginManifestRegistry({ + const index = loadPluginRegistrySnapshot({ config: params.config, workspaceDir: params.workspaceDir, env: params.env, - }) - .plugins.filter((plugin) => plugin.channels.length > 0) - .map((plugin) => plugin.id); + }); + return index.plugins + .filter((plugin) => plugin.contributions.channels.length > 0) + .map((plugin) => plugin.pluginId); } export function resolveConfiguredDeferredChannelPluginIds(params: { @@ -186,15 +170,16 @@ export function resolveConfiguredDeferredChannelPluginIds(params: { const activationSource = createPluginActivationSource({ config: params.config, }); - return loadPluginManifestRegistry({ + const index = loadPluginRegistrySnapshot({ config: params.config, workspaceDir: params.workspaceDir, env: params.env, - }) - .plugins.filter( + }); + return index.plugins + .filter( (plugin) => hasConfiguredStartupChannel({ plugin, configuredChannelIds }) && - plugin.startupDeferConfiguredChannelFullLoadUntilAfterListen === true && + plugin.startup.deferConfiguredChannelFullLoadUntilAfterListen && canStartConfiguredChannelPlugin({ plugin, config: params.config, @@ -202,7 +187,7 @@ export function resolveConfiguredDeferredChannelPluginIds(params: { activationSource, }), ) - .map((plugin) => plugin.id); + .map((plugin) => plugin.pluginId); } export function resolveGatewayStartupPluginIds(params: { @@ -219,33 +204,23 @@ export function resolveGatewayStartupPluginIds(params: { const activationSource = createPluginActivationSource({ config: params.activationSourceConfig ?? params.config, }); - const requiredAgentHarnessPluginIds = new Set( + const requiredAgentHarnessRuntimes = new Set( collectConfiguredAgentHarnessRuntimes( params.activationSourceConfig ?? params.config, params.env, - ).flatMap((runtime) => - resolveManifestActivationPluginIds({ - trigger: { - kind: "agentHarness", - runtime, - }, - config: params.config, - workspaceDir: params.workspaceDir, - env: params.env, - cache: true, - }), ), ); const startupDreamingPluginIds = resolveGatewayStartupDreamingPluginIds(params.config); const explicitMemorySlotStartupPluginId = resolveExplicitMemorySlotStartupPluginId( params.activationSourceConfig ?? params.config, ); - return loadPluginManifestRegistry({ + const index = loadPluginRegistrySnapshot({ config: params.config, workspaceDir: params.workspaceDir, env: params.env, - }) - .plugins.filter((plugin) => { + }); + return index.plugins + .filter((plugin) => { if (hasConfiguredStartupChannel({ plugin, configuredChannelIds })) { return canStartConfiguredChannelPlugin({ plugin, @@ -254,9 +229,11 @@ export function resolveGatewayStartupPluginIds(params: { activationSource, }); } - if (requiredAgentHarnessPluginIds.has(plugin.id)) { + if ( + plugin.startup.agentHarnesses.some((runtime) => requiredAgentHarnessRuntimes.has(runtime)) + ) { const activationState = resolveEffectivePluginActivationState({ - id: plugin.id, + id: plugin.pluginId, origin: plugin.origin, config: pluginsConfig, rootConfig: params.config, @@ -275,7 +252,7 @@ export function resolveGatewayStartupPluginIds(params: { return false; } const activationState = resolveEffectivePluginActivationState({ - id: plugin.id, + id: plugin.pluginId, origin: plugin.origin, config: pluginsConfig, rootConfig: params.config, @@ -290,5 +267,5 @@ export function resolveGatewayStartupPluginIds(params: { } return activationState.source === "explicit" || activationState.source === "default"; }) - .map((plugin) => plugin.id); + .map((plugin) => plugin.pluginId); } diff --git a/src/plugins/installed-plugin-index-store.test.ts b/src/plugins/installed-plugin-index-store.test.ts index e635ba0ff60..69c3541b2c4 100644 --- a/src/plugins/installed-plugin-index-store.test.ts +++ b/src/plugins/installed-plugin-index-store.test.ts @@ -27,7 +27,7 @@ function createIndex(overrides: Partial = {}): InstalledPl version: 1, hostContractVersion: "2026.4.25", compatRegistryVersion: "compat-v1", - migrationVersion: 1, + migrationVersion: 2, policyHash: "policy-v1", generatedAtMs: 1777118400000, plugins: [ @@ -48,6 +48,12 @@ function createIndex(overrides: Partial = {}): InstalledPl commandAliases: [], contracts: [], }, + startup: { + sidecar: false, + memory: false, + deferConfiguredChannelFullLoadUntilAfterListen: false, + agentHarnesses: [], + }, compat: [], }, ], diff --git a/src/plugins/installed-plugin-index-store.ts b/src/plugins/installed-plugin-index-store.ts index ebc6f934cc1..15bf63e76f3 100644 --- a/src/plugins/installed-plugin-index-store.ts +++ b/src/plugins/installed-plugin-index-store.ts @@ -47,6 +47,15 @@ const InstalledPluginIndexContributionsSchema = z }) .passthrough(); +const InstalledPluginIndexStartupSchema = z + .object({ + sidecar: z.boolean(), + memory: z.boolean(), + deferConfiguredChannelFullLoadUntilAfterListen: z.boolean(), + agentHarnesses: ContributionArraySchema, + }) + .passthrough(); + const InstalledPluginIndexRecordSchema = z .object({ pluginId: z.string(), @@ -68,6 +77,7 @@ const InstalledPluginIndexRecordSchema = z enabled: z.boolean(), enabledByDefault: z.boolean().optional(), contributions: InstalledPluginIndexContributionsSchema, + startup: InstalledPluginIndexStartupSchema, compat: z.array(z.string()), }) .passthrough(); diff --git a/src/plugins/installed-plugin-index.test.ts b/src/plugins/installed-plugin-index.test.ts index 0547d893711..99bfac77b47 100644 --- a/src/plugins/installed-plugin-index.test.ts +++ b/src/plugins/installed-plugin-index.test.ts @@ -154,7 +154,7 @@ describe("installed plugin index", () => { expect(index).toMatchObject({ version: 1, - migrationVersion: 1, + migrationVersion: 2, generatedAtMs: 1777118400000, plugins: [ { @@ -679,7 +679,7 @@ describe("installed plugin index", () => { env: hermeticEnv({ OPENCLAW_VERSION: "2026.4.26" }), }), compatRegistryVersion: "different-compat-registry", - migrationVersion: 2 as 1, + migrationVersion: 3 as 2, }; expect(diffInstalledPluginIndexInvalidationReasons(previous, current)).toEqual([ diff --git a/src/plugins/installed-plugin-index.ts b/src/plugins/installed-plugin-index.ts index 5c605c96762..817562375d2 100644 --- a/src/plugins/installed-plugin-index.ts +++ b/src/plugins/installed-plugin-index.ts @@ -18,9 +18,10 @@ import { type PluginManifestRegistry, } from "./manifest-registry.js"; import type { PluginDiagnostic } from "./manifest-types.js"; +import { hasKind } from "./slots.js"; export const INSTALLED_PLUGIN_INDEX_VERSION = 1; -export const INSTALLED_PLUGIN_INDEX_MIGRATION_VERSION = 1; +export const INSTALLED_PLUGIN_INDEX_MIGRATION_VERSION = 2; export type InstalledPluginIndexRefreshReason = | "missing" @@ -44,6 +45,13 @@ export type InstalledPluginIndexContributions = { contracts: readonly string[]; }; +export type InstalledPluginStartupInfo = { + sidecar: boolean; + memory: boolean; + deferConfiguredChannelFullLoadUntilAfterListen: boolean; + agentHarnesses: readonly string[]; +}; + export type InstalledPluginInstallRecordInfo = Pick< PluginInstallRecord, | "source" @@ -95,6 +103,7 @@ export type InstalledPluginIndexRecord = { enabled: boolean; enabledByDefault?: boolean; contributions: InstalledPluginIndexContributions; + startup: InstalledPluginStartupInfo; compat: readonly PluginCompatCode[]; }; @@ -199,6 +208,34 @@ function collectContractKeys(record: PluginManifestRecord): readonly string[] { ); } +function hasRuntimeContractSurface(record: PluginManifestRecord): boolean { + return Boolean( + record.providers.length > 0 || + record.cliBackends.length > 0 || + record.contracts?.speechProviders?.length || + record.contracts?.mediaUnderstandingProviders?.length || + record.contracts?.documentExtractors?.length || + record.contracts?.imageGenerationProviders?.length || + record.contracts?.videoGenerationProviders?.length || + record.contracts?.musicGenerationProviders?.length || + record.contracts?.webContentExtractors?.length || + record.contracts?.webFetchProviders?.length || + record.contracts?.webSearchProviders?.length || + record.contracts?.memoryEmbeddingProviders?.length || + hasKind(record.kind, "memory"), + ); +} + +function buildStartupInfo(record: PluginManifestRecord): InstalledPluginStartupInfo { + return { + sidecar: record.channels.length === 0 && !hasRuntimeContractSurface(record), + memory: hasKind(record.kind, "memory"), + deferConfiguredChannelFullLoadUntilAfterListen: + record.startupDeferConfiguredChannelFullLoadUntilAfterListen === true, + agentHarnesses: sortUnique(record.activation?.onAgentHarnesses ?? []), + }; +} + function collectCompatCodes(record: PluginManifestRecord): readonly PluginCompatCode[] { const codes: PluginCompatCode[] = []; if (record.providerAuthEnvVars && Object.keys(record.providerAuthEnvVars).length > 0) { @@ -463,6 +500,7 @@ function buildInstalledPluginIndex( origin: record.origin, enabled, contributions: buildContributions(record), + startup: buildStartupInfo(record), compat: collectCompatCodes(record), }; if (record.enabledByDefault === true) { diff --git a/src/plugins/plugin-registry.test.ts b/src/plugins/plugin-registry.test.ts index fd3a53b47ef..20e6d64e8a5 100644 --- a/src/plugins/plugin-registry.test.ts +++ b/src/plugins/plugin-registry.test.ts @@ -94,7 +94,7 @@ function createIndex(pluginId = "demo"): InstalledPluginIndex { version: 1, hostContractVersion: "2026.4.25", compatRegistryVersion: "compat-v1", - migrationVersion: 1, + migrationVersion: 2, policyHash: "policy-v1", generatedAtMs: 1777118400000, plugins: [ @@ -115,6 +115,12 @@ function createIndex(pluginId = "demo"): InstalledPluginIndex { commandAliases: [], contracts: [], }, + startup: { + sidecar: false, + memory: false, + deferConfiguredChannelFullLoadUntilAfterListen: false, + agentHarnesses: [], + }, compat: [], }, ],