fix(plugins): cache discovery registration snapshots

Co-authored-by: junpei.o <14040213+livingghost@users.noreply.github.com>
Co-authored-by: Yoshiaki Okuyama <okuyam2y@gmail.com>
Co-authored-by: Shion Eria <shioneria@foxmail.com>
Co-authored-by: Billy Shih <1472300+bbshih@users.noreply.github.com>
This commit is contained in:
Peter Steinberger
2026-04-24 23:52:51 +01:00
parent 9eeceaca43
commit 0c46e8000e
9 changed files with 345 additions and 83 deletions

View File

@@ -66,7 +66,7 @@ import {
restorePluginInteractiveHandlers,
} from "./interactive-registry.js";
import { getCachedPluginJitiLoader, type PluginJitiLoaderCache } from "./jiti-loader-cache.js";
import { loadPluginManifestRegistry } from "./manifest-registry.js";
import { loadPluginManifestRegistry, type PluginManifestRecord } from "./manifest-registry.js";
import type { PluginBundleFormat, PluginDiagnostic, PluginFormat } from "./manifest-types.js";
import type { PluginManifestContracts } from "./manifest.js";
import {
@@ -124,6 +124,7 @@ import type {
OpenClawPluginDefinition,
OpenClawPluginModule,
PluginLogger,
PluginRegistrationMode,
} from "./types.js";
export type PluginLoadResult = PluginRegistry;
@@ -808,6 +809,7 @@ function buildCacheKey(params: {
runtimeSubagentMode?: "default" | "explicit" | "gateway-bindable";
pluginSdkResolution?: PluginSdkResolutionPreference;
coreGatewayMethodNames?: string[];
activate?: boolean;
}): string {
const { roots, loadPaths } = resolvePluginCacheInputs({
workspaceDir: params.workspaceDir,
@@ -845,12 +847,13 @@ function buildCacheKey(params: {
params.installBundledRuntimeDeps === false ? "skip-runtime-deps" : "install-runtime-deps";
const runtimeSubagentMode = params.runtimeSubagentMode ?? "default";
const gatewayMethodsKey = JSON.stringify(params.coreGatewayMethodNames ?? []);
const activationMode = params.activate === false ? "snapshot" : "active";
return `${roots.workspace ?? ""}::${roots.global ?? ""}::${roots.stock ?? ""}::${JSON.stringify({
...params.plugins,
installs,
loadPaths,
activationMetadataKey: params.activationMetadataKey ?? "",
})}::${scopeKey}::${setupOnlyKey}::${setupOnlyModeKey}::${setupOnlyRequirementKey}::${startupChannelMode}::${moduleLoadMode}::${bundledRuntimeDepsMode}::${runtimeSubagentMode}::${params.pluginSdkResolution ?? "auto"}::${gatewayMethodsKey}`;
})}::${scopeKey}::${setupOnlyKey}::${setupOnlyModeKey}::${setupOnlyRequirementKey}::${startupChannelMode}::${moduleLoadMode}::${bundledRuntimeDepsMode}::${runtimeSubagentMode}::${params.pluginSdkResolution ?? "auto"}::${gatewayMethodsKey}::${activationMode}`;
}
function matchesScopedPluginRequest(params: {
@@ -933,6 +936,87 @@ function hasExplicitCompatibilityInputs(options: PluginLoadOptions): boolean {
);
}
type PluginRegistrationPlan = {
/** Public compatibility label passed to plugin register(api). */
mode: PluginRegistrationMode;
/** Load a setup entry instead of the normal runtime entry. */
loadSetupEntry: boolean;
/** Setup flow also needs the runtime channel entry for runtime setters/plugin shape. */
loadSetupRuntimeEntry: boolean;
/** Apply runtime capability policy such as memory-slot selection. */
runRuntimeCapabilityPolicy: boolean;
/** Register metadata that only belongs to live activation, not discovery snapshots. */
runFullActivationOnlyRegistrations: boolean;
};
/**
* Convert loader intent into explicit behavior flags.
*
* Registration modes are plugin-facing labels; this plan is the internal source
* of truth for which entrypoint to load and which activation-only policies run.
*/
function resolvePluginRegistrationPlan(params: {
canLoadScopedSetupOnlyChannelPlugin: boolean;
scopedSetupOnlyChannelPluginRequested: boolean;
requireSetupEntryForSetupOnlyChannelPlugins: boolean;
enableStateEnabled: boolean;
shouldLoadModules: boolean;
validateOnly: boolean;
shouldActivate: boolean;
manifestRecord: PluginManifestRecord;
cfg: OpenClawConfig;
env: NodeJS.ProcessEnv;
preferSetupRuntimeForChannelPlugins: boolean;
}): PluginRegistrationPlan | null {
if (params.canLoadScopedSetupOnlyChannelPlugin) {
return {
mode: "setup-only",
loadSetupEntry: true,
loadSetupRuntimeEntry: false,
runRuntimeCapabilityPolicy: false,
runFullActivationOnlyRegistrations: false,
};
}
if (
params.scopedSetupOnlyChannelPluginRequested &&
params.requireSetupEntryForSetupOnlyChannelPlugins
) {
return null;
}
if (!params.enableStateEnabled) {
return null;
}
const loadSetupRuntimeEntry =
params.shouldLoadModules &&
!params.validateOnly &&
shouldLoadChannelPluginInSetupRuntime({
manifestChannels: params.manifestRecord.channels,
setupSource: params.manifestRecord.setupSource,
startupDeferConfiguredChannelFullLoadUntilAfterListen:
params.manifestRecord.startupDeferConfiguredChannelFullLoadUntilAfterListen,
cfg: params.cfg,
env: params.env,
preferSetupRuntimeForChannelPlugins: params.preferSetupRuntimeForChannelPlugins,
});
if (loadSetupRuntimeEntry) {
return {
mode: "setup-runtime",
loadSetupEntry: true,
loadSetupRuntimeEntry: true,
runRuntimeCapabilityPolicy: false,
runFullActivationOnlyRegistrations: false,
};
}
const mode = params.shouldActivate ? "full" : "discovery";
return {
mode,
loadSetupEntry: false,
loadSetupRuntimeEntry: false,
runRuntimeCapabilityPolicy: true,
runFullActivationOnlyRegistrations: mode === "full",
};
}
function resolvePluginLoadCacheContext(options: PluginLoadOptions = {}) {
const env = options.env ?? process.env;
const cfg = applyTestPluginDefaults(options.config ?? {}, env);
@@ -976,6 +1060,7 @@ function resolvePluginLoadCacheContext(options: PluginLoadOptions = {}) {
runtimeSubagentMode,
pluginSdkResolution: options.pluginSdkResolution,
coreGatewayMethodNames,
activate: options.activate,
});
return {
env,
@@ -1053,6 +1138,15 @@ function getCompatibleActivePluginRegistry(
if (loadContext.cacheKey === activeCacheKey) {
return activeRegistry;
}
if (!loadContext.shouldActivate) {
const activatingCacheKey = resolvePluginLoadCacheContext({
...options,
activate: true,
}).cacheKey;
if (activatingCacheKey === activeCacheKey) {
return activeRegistry;
}
}
if (
loadContext.runtimeSubagentMode === "default" &&
getActivePluginRuntimeSubagentMode() === "gateway-bindable"
@@ -1067,6 +1161,19 @@ function getCompatibleActivePluginRegistry(
if (gatewayBindableCacheKey === activeCacheKey) {
return activeRegistry;
}
if (!loadContext.shouldActivate) {
const activatingGatewayBindableCacheKey = resolvePluginLoadCacheContext({
...options,
activate: true,
runtimeOptions: {
...options.runtimeOptions,
allowGatewaySubagentBinding: true,
},
}).cacheKey;
if (activatingGatewayBindableCacheKey === activeCacheKey) {
return activeRegistry;
}
}
}
return undefined;
}
@@ -1851,13 +1958,6 @@ function activatePluginRegistry(
}
export function loadOpenClawPlugins(options: PluginLoadOptions = {}): PluginRegistry {
// Snapshot (non-activating) loads must disable the cache to avoid storing a registry
// whose commands were never globally registered.
if (options.activate === false && options.cache !== false) {
throw new Error(
"loadOpenClawPlugins: activate:false requires cache:false to prevent command registry divergence",
);
}
const {
env,
cfg,
@@ -1882,21 +1982,21 @@ export function loadOpenClawPlugins(options: PluginLoadOptions = {}): PluginRegi
if (cacheEnabled) {
const cached = getCachedPluginRegistry(cacheKey);
if (cached) {
restoreRegisteredAgentHarnesses(cached.agentHarnesses);
restorePluginCommands(cached.commands ?? []);
restoreRegisteredCompactionProviders(cached.compactionProviders);
restoreDetachedTaskLifecycleRuntimeRegistration(cached.detachedTaskRuntimeRegistration);
restorePluginInteractiveHandlers(cached.interactiveHandlers ?? []);
restoreRegisteredMemoryEmbeddingProviders(cached.memoryEmbeddingProviders);
restoreMemoryPluginState({
capability: cached.memoryCapability,
corpusSupplements: cached.memoryCorpusSupplements,
promptBuilder: cached.memoryPromptBuilder,
promptSupplements: cached.memoryPromptSupplements,
flushPlanResolver: cached.memoryFlushPlanResolver,
runtime: cached.memoryRuntime,
});
if (shouldActivate) {
restoreRegisteredAgentHarnesses(cached.agentHarnesses);
restorePluginCommands(cached.commands ?? []);
restoreRegisteredCompactionProviders(cached.compactionProviders);
restoreDetachedTaskLifecycleRuntimeRegistration(cached.detachedTaskRuntimeRegistration);
restorePluginInteractiveHandlers(cached.interactiveHandlers ?? []);
restoreRegisteredMemoryEmbeddingProviders(cached.memoryEmbeddingProviders);
restoreMemoryPluginState({
capability: cached.memoryCapability,
corpusSupplements: cached.memoryCorpusSupplements,
promptBuilder: cached.memoryPromptBuilder,
promptSupplements: cached.memoryPromptSupplements,
flushPlanResolver: cached.memoryFlushPlanResolver,
runtime: cached.memoryRuntime,
});
activatePluginRegistry(
cached.registry,
cacheKey,
@@ -2178,33 +2278,27 @@ export function loadOpenClawPlugins(options: PluginLoadOptions = {}): PluginRegi
const scopedSetupOnlyChannelPluginRequested =
includeSetupOnlyChannelPlugins &&
!validateOnly &&
onlyPluginIdSet &&
Boolean(onlyPluginIdSet) &&
manifestRecord.channels.length > 0 &&
(!enableState.enabled || forceSetupOnlyChannelPlugins);
const canLoadScopedSetupOnlyChannelPlugin =
scopedSetupOnlyChannelPluginRequested &&
(!requireSetupEntryForSetupOnlyChannelPlugins || Boolean(manifestRecord.setupSource));
const registrationMode = canLoadScopedSetupOnlyChannelPlugin
? "setup-only"
: scopedSetupOnlyChannelPluginRequested && requireSetupEntryForSetupOnlyChannelPlugins
? null
: enableState.enabled
? shouldLoadModules &&
!validateOnly &&
shouldLoadChannelPluginInSetupRuntime({
manifestChannels: manifestRecord.channels,
setupSource: manifestRecord.setupSource,
startupDeferConfiguredChannelFullLoadUntilAfterListen:
manifestRecord.startupDeferConfiguredChannelFullLoadUntilAfterListen,
cfg,
env,
preferSetupRuntimeForChannelPlugins,
})
? "setup-runtime"
: "full"
: null;
const registrationPlan = resolvePluginRegistrationPlan({
canLoadScopedSetupOnlyChannelPlugin,
scopedSetupOnlyChannelPluginRequested,
requireSetupEntryForSetupOnlyChannelPlugins,
enableStateEnabled: enableState.enabled,
shouldLoadModules,
validateOnly,
shouldActivate,
manifestRecord,
cfg,
env,
preferSetupRuntimeForChannelPlugins,
});
if (!registrationMode) {
if (!registrationPlan) {
record.status = "disabled";
record.error = enableState.reason;
markPluginActivationDisabled(record, enableState.reason);
@@ -2212,6 +2306,7 @@ export function loadOpenClawPlugins(options: PluginLoadOptions = {}): PluginRegi
seenIds.set(pluginId, candidate.origin);
continue;
}
const registrationMode = registrationPlan.mode;
if (!enableState.enabled) {
record.status = "disabled";
record.error = enableState.reason;
@@ -2340,7 +2435,7 @@ export function loadOpenClawPlugins(options: PluginLoadOptions = {}): PluginRegi
// Exception: the dreaming engine (memory-core by default) must load alongside the
// selected memory slot plugin so dreaming can run even when lancedb holds the slot.
if (
registrationMode === "full" &&
registrationPlan.runRuntimeCapabilityPolicy &&
candidate.origin === "bundled" &&
hasKind(manifestRecord.kind, "memory")
) {
@@ -2368,7 +2463,7 @@ export function loadOpenClawPlugins(options: PluginLoadOptions = {}): PluginRegi
continue;
}
if (!shouldLoadModules && registrationMode === "full") {
if (!shouldLoadModules && registrationPlan.runRuntimeCapabilityPolicy) {
const memoryDecision = resolveMemorySlotDecision({
id: record.id,
kind: record.kind,
@@ -2414,8 +2509,7 @@ export function loadOpenClawPlugins(options: PluginLoadOptions = {}): PluginRegi
}
const loadSource =
(registrationMode === "setup-only" || registrationMode === "setup-runtime") &&
runtimeSetupSource
registrationPlan.loadSetupEntry && runtimeSetupSource
? runtimeSetupSource
: runtimeCandidateSource;
const moduleLoadSource = resolveCanonicalDistRuntimeSource(loadSource);
@@ -2461,10 +2555,7 @@ export function loadOpenClawPlugins(options: PluginLoadOptions = {}): PluginRegi
continue;
}
if (
(registrationMode === "setup-only" || registrationMode === "setup-runtime") &&
manifestRecord.setupSource
) {
if (registrationPlan.loadSetupEntry && manifestRecord.setupSource) {
const setupRegistration = resolveSetupChannelRegistration(mod, {
installRuntimeDeps:
shouldInstallBundledRuntimeDeps &&
@@ -2507,7 +2598,7 @@ export function loadOpenClawPlugins(options: PluginLoadOptions = {}): PluginRegi
let mergedSetupRegistration = setupRegistration;
let runtimeSetterApplied = false;
if (
registrationMode === "setup-runtime" &&
registrationPlan.loadSetupRuntimeEntry &&
setupRegistration.usesBundledSetupContract &&
runtimeCandidateSource !== safeSource
) {
@@ -2685,7 +2776,7 @@ export function loadOpenClawPlugins(options: PluginLoadOptions = {}): PluginRegi
memorySlotMatched = true;
}
if (registrationMode === "full") {
if (registrationPlan.runRuntimeCapabilityPolicy) {
if (pluginId !== dreamingEngineId) {
const memoryDecision = resolveMemorySlotDecision({
id: record.id,
@@ -2711,7 +2802,7 @@ export function loadOpenClawPlugins(options: PluginLoadOptions = {}): PluginRegi
}
}
if (registrationMode === "full") {
if (registrationPlan.runFullActivationOnlyRegistrations) {
if (definition?.reload) {
registerReload(record, definition.reload);
}