fix: load default memory plugin at startup

This commit is contained in:
Peter Steinberger
2026-04-26 11:32:51 +01:00
parent 0e490a3c26
commit cd79e01be3
5 changed files with 88 additions and 31 deletions

View File

@@ -6,6 +6,7 @@ Docs: https://docs.openclaw.ai
### Fixes
- Plugins/startup: load the default `memory-core` slot during Gateway startup when permitted so active-memory recall can call `memory_search` and `memory_get` without requiring an explicit `plugins.slots.memory` entry, while preserving `plugins.slots.memory: "none"`. Thanks @codex.
- Plugins/CLI: prefer native require for compiled bundled plugin JavaScript before jiti so read-only config, status, device, and node commands avoid unnecessary transform overhead on slow hosts. Fixes #62842. Thanks @Effet.
- Plugins/compat: add missing dated compatibility records for legacy extension-api, memory registration, provider hook/type aliases, runtime aliases, channel SDK helpers, and approval/test utility shims. Thanks @vincentkoc.
- Plugins/CLI: refresh the persisted registry after managed plugin files are removed so ClawHub uninstall cannot leave stale `plugins list` entries. Thanks @codex.

View File

@@ -419,26 +419,26 @@ describe("resolveGatewayStartupPluginIds", () => {
enabledPluginIds: ["voice-call"],
modelId: "demo-cli/demo-model",
}),
["demo-channel", "browser", "voice-call"],
["demo-channel", "browser", "voice-call", "memory-core"],
],
[
"keeps bundled startup sidecars with enabledByDefault at idle startup",
{} as OpenClawConfig,
["demo-channel", "browser"],
["demo-channel", "browser", "memory-core"],
],
[
"keeps provider plugins out of idle startup when only provider config references them",
createStartupConfig({
providerIds: ["demo-provider"],
}),
["demo-channel", "browser"],
["demo-channel", "browser", "memory-core"],
],
[
"includes explicitly enabled non-channel sidecars in startup scope",
createStartupConfig({
enabledPluginIds: ["demo-global-sidecar", "voice-call"],
}),
["demo-channel", "browser", "voice-call", "demo-global-sidecar"],
["demo-channel", "browser", "voice-call", "memory-core", "demo-global-sidecar"],
],
[
"keeps default-enabled startup sidecars when a restrictive allowlist permits them",
@@ -453,7 +453,7 @@ describe("resolveGatewayStartupPluginIds", () => {
createStartupConfig({
channelIds: ["demo-channel", "demo-other-channel"],
}),
["demo-channel", "demo-other-channel", "browser"],
["demo-channel", "demo-other-channel", "browser", "memory-core"],
],
] as const)("%s", (_name, config, expected) => {
expectStartupPluginIdsCase({ config, expected });
@@ -501,7 +501,7 @@ describe("resolveGatewayStartupPluginIds", () => {
env: {
DEMO_CHANNEL_ANYTHING: "1",
} as NodeJS.ProcessEnv,
expected: ["demo-channel", "browser"],
expected: ["demo-channel", "browser", "memory-core"],
});
expect(
resolveConfiguredDeferredChannelPluginIds({
@@ -564,7 +564,7 @@ describe("resolveGatewayStartupPluginIds", () => {
},
} as OpenClawConfig,
env: {},
expected: ["browser"],
expected: ["browser", "memory-core"],
});
});
@@ -582,7 +582,7 @@ describe("resolveGatewayStartupPluginIds", () => {
env: {
OPENCLAW_STATE_DIR: "/tmp/openclaw-with-persisted-demo-channel",
} as NodeJS.ProcessEnv,
expected: ["browser"],
expected: ["browser", "memory-core"],
});
});
@@ -657,12 +657,22 @@ describe("resolveGatewayStartupPluginIds", () => {
});
});
it("includes the default memory slot plugin when the allowlist permits it", () => {
expectStartupPluginIdsCase({
config: createStartupConfig({
allowPluginIds: ["browser", "memory-core"],
noConfiguredChannels: true,
}),
expected: ["browser", "memory-core"],
});
});
it("does not include non-selected memory plugins only because they are enabled", () => {
expectStartupPluginIdsCase({
config: createStartupConfig({
enabledPluginIds: ["memory-lancedb"],
}),
expected: ["demo-channel", "browser"],
expected: ["demo-channel", "browser", "memory-core"],
});
});
@@ -672,7 +682,7 @@ describe("resolveGatewayStartupPluginIds", () => {
agentRuntimeId: "codex",
enabledPluginIds: ["codex"],
}),
expected: ["demo-channel", "browser", "codex"],
expected: ["demo-channel", "browser", "codex", "memory-core"],
});
});
@@ -682,7 +692,7 @@ describe("resolveGatewayStartupPluginIds", () => {
agentRuntimeIds: ["codex"],
enabledPluginIds: ["codex"],
}),
expected: ["demo-channel", "browser", "codex"],
expected: ["demo-channel", "browser", "codex", "memory-core"],
});
});
@@ -692,7 +702,7 @@ describe("resolveGatewayStartupPluginIds", () => {
enabledPluginIds: ["codex"],
}),
env: { OPENCLAW_AGENT_RUNTIME: "codex" },
expected: ["demo-channel", "browser", "codex"],
expected: ["demo-channel", "browser", "codex", "memory-core"],
});
});
@@ -702,7 +712,7 @@ describe("resolveGatewayStartupPluginIds", () => {
agentRuntimeId: "demo-cli",
enabledPluginIds: ["demo-provider-plugin"],
}),
expected: ["demo-channel", "browser", "demo-provider-plugin"],
expected: ["demo-channel", "browser", "demo-provider-plugin", "memory-core"],
});
});
@@ -715,7 +725,7 @@ describe("resolveGatewayStartupPluginIds", () => {
config: createStartupConfig({
agentRuntimeId: runtime,
}),
expected: ["demo-channel", "browser", pluginId],
expected: ["demo-channel", "browser", pluginId, "memory-core"],
});
});
@@ -738,7 +748,7 @@ describe("resolveGatewayStartupPluginIds", () => {
},
},
} as OpenClawConfig,
expected: ["demo-channel", "browser"],
expected: ["demo-channel", "browser", "memory-core"],
});
});
@@ -761,7 +771,7 @@ describe("resolveGatewayStartupPluginIds", () => {
},
},
} as OpenClawConfig,
expected: ["demo-channel", "browser"],
expected: ["demo-channel", "browser", "memory-core"],
});
});
});

View File

@@ -65,21 +65,36 @@ function resolveGatewayStartupDreamingPluginIds(config: OpenClawConfig): Set<str
return new Set([DEFAULT_MEMORY_DREAMING_PLUGIN_ID, resolveMemoryDreamingPluginId(config)]);
}
function resolveExplicitMemorySlotStartupPluginId(
config: OpenClawConfig,
normalizePluginId: (pluginId: string) => string,
): string | undefined {
const configuredSlot = config.plugins?.slots?.memory?.trim();
if (!configuredSlot || configuredSlot.toLowerCase() === "none") {
function resolveMemorySlotStartupPluginId(params: {
activationSourceConfig: OpenClawConfig;
activationSourcePlugins: ReturnType<typeof normalizePluginsConfigWithRegistry>;
normalizePluginId: (pluginId: string) => string;
}): string | undefined {
const { activationSourceConfig, activationSourcePlugins, normalizePluginId } = params;
const configuredSlot = activationSourceConfig.plugins?.slots?.memory?.trim();
if (configuredSlot?.toLowerCase() === "none") {
return undefined;
}
if (!configuredSlot) {
const defaultSlot = activationSourcePlugins.slots.memory;
if (typeof defaultSlot !== "string") {
return undefined;
}
if (
activationSourcePlugins.allow.length > 0 &&
!activationSourcePlugins.allow.includes(defaultSlot)
) {
return undefined;
}
return defaultSlot;
}
return normalizePluginId(configuredSlot);
}
function shouldConsiderForGatewayStartup(params: {
plugin: InstalledPluginIndexRecord;
startupDreamingPluginIds: ReadonlySet<string>;
explicitMemorySlotStartupPluginId?: string;
memorySlotStartupPluginId?: string;
}): boolean {
if (isGatewayStartupSidecar(params.plugin)) {
return true;
@@ -90,7 +105,7 @@ function shouldConsiderForGatewayStartup(params: {
if (params.startupDreamingPluginIds.has(params.plugin.pluginId)) {
return true;
}
return params.explicitMemorySlotStartupPluginId === params.plugin.pluginId;
return params.memorySlotStartupPluginId === params.plugin.pluginId;
}
function hasConfiguredStartupChannel(params: {
@@ -246,18 +261,23 @@ export function resolveGatewayStartupPluginIds(params: {
// not the auto-enabled effective snapshot, or configured-only channels can be
// misclassified as explicit enablement.
const activationSourceConfig = params.activationSourceConfig ?? params.config;
const activationSourcePlugins = normalizePluginsConfigWithRegistry(
activationSourceConfig.plugins,
index,
);
const activationSource = {
plugins: normalizePluginsConfigWithRegistry(activationSourceConfig.plugins, index),
plugins: activationSourcePlugins,
rootConfig: activationSourceConfig,
};
const requiredAgentHarnessRuntimes = new Set(
collectConfiguredAgentHarnessRuntimes(activationSourceConfig, params.env),
);
const startupDreamingPluginIds = resolveGatewayStartupDreamingPluginIds(params.config);
const explicitMemorySlotStartupPluginId = resolveExplicitMemorySlotStartupPluginId(
const memorySlotStartupPluginId = resolveMemorySlotStartupPluginId({
activationSourceConfig,
createPluginRegistryIdNormalizer(index),
);
activationSourcePlugins,
normalizePluginId: createPluginRegistryIdNormalizer(index),
});
return index.plugins
.filter((plugin) => {
if (hasConfiguredStartupChannel({ plugin, manifestRegistry, configuredChannelIds })) {
@@ -286,7 +306,7 @@ export function resolveGatewayStartupPluginIds(params: {
!shouldConsiderForGatewayStartup({
plugin,
startupDreamingPluginIds,
explicitMemorySlotStartupPluginId,
memorySlotStartupPluginId,
})
) {
return false;

View File

@@ -65,14 +65,14 @@ describe("resolveManifestDeclaredWebProviderCandidatePluginIds", () => {
expect(mocks.loadPluginManifestRegistryForInstalledIndex).not.toHaveBeenCalled();
});
it("keeps runtime fallback for scoped plugins with no declared web candidates", () => {
it("keeps scoped plugins with no declared web candidates scoped-empty", () => {
expect(
resolveManifestDeclaredWebProviderCandidatePluginIds({
contract: "webSearchProviders",
configKey: "webSearch",
onlyPluginIds: ["missing-plugin"],
}),
).toBeUndefined();
).toEqual([]);
expect(mocks.loadPluginManifestRegistryForInstalledIndex).toHaveBeenCalledWith(
expect.objectContaining({
pluginIds: ["missing-plugin"],
@@ -80,6 +80,29 @@ describe("resolveManifestDeclaredWebProviderCandidatePluginIds", () => {
);
});
it("keeps origin filters with no declared web candidates scoped-empty", () => {
mocks.loadPluginManifestRegistryForInstalledIndex.mockReturnValue({
plugins: [
{
id: "workspace-tool",
origin: "workspace",
configSchema: {
properties: {},
},
},
],
diagnostics: [],
});
expect(
resolveManifestDeclaredWebProviderCandidatePluginIds({
contract: "webSearchProviders",
configKey: "webSearch",
origin: "bundled",
}),
).toEqual([]);
});
it("derives provider candidates from a single manifest-registry read", () => {
expect(
resolveManifestDeclaredWebProviderCandidatePluginIds({

View File

@@ -105,6 +105,9 @@ export function resolveManifestDeclaredWebProviderCandidatePluginIds(params: {
if (ids.length > 0) {
return ids;
}
if (params.origin || scopedPluginIds !== undefined) {
return [];
}
return undefined;
}