mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-21 14:11:26 +00:00
Plugins: extract loader cache control
This commit is contained in:
@@ -32,6 +32,7 @@ This is an implementation checklist, not a future-design spec.
|
||||
| Resolved static registry | flat rows in `src/plugins/manifest-registry.ts` | `src/extension-host/resolved-registry.ts` | `partial` | Manifest records now carry `resolvedExtension`; a host-owned resolved registry view exists for static consumers. |
|
||||
| Manifest/package metadata loading | `src/plugins/manifest.ts`, `src/plugins/discovery.ts`, `src/plugins/install.ts` | `src/extension-host/schema.ts` and `src/extension-host/manifest-registry.ts` | `partial` | Package metadata parsing is routed through host schema helpers; legacy loader flow still supplies the source manifests. |
|
||||
| Loader SDK alias compatibility | `src/plugins/loader.ts` | `src/extension-host/loader-compat.ts` | `partial` | Plugin-SDK alias candidate ordering, alias-file resolution, and scoped alias-map construction now live in host-owned loader compatibility helpers. |
|
||||
| Loader cache key and registry cache control | `src/plugins/loader.ts` | `src/extension-host/loader-cache.ts` | `partial` | Cache-key construction, LRU registry cache reads and writes, and cache clearing now delegate through host-owned loader-cache helpers while preserving the current cache shape and cap. |
|
||||
| Loader provenance and duplicate-order policy | `src/plugins/loader.ts` | `src/extension-host/loader-policy.ts` | `partial` | Plugin-record creation, duplicate precedence, provenance indexing, and allowlist/untracked warnings now live in host-owned loader-policy helpers. |
|
||||
| Loader initial candidate planning and record creation | `src/plugins/loader.ts` | `src/extension-host/loader-records.ts` | `partial` | Duplicate detection, initial record creation, manifest metadata attachment, and first-pass enable-state planning now delegate through host-owned loader-records helpers. |
|
||||
| Loader entry-path opening and module import | `src/plugins/loader.ts` | `src/extension-host/loader-import.ts` | `partial` | Boundary-checked entry opening and module import now delegate through host-owned loader-import helpers while preserving the current trusted in-process loading model. |
|
||||
@@ -85,14 +86,14 @@ That pattern has been used for:
|
||||
- active registry ownership
|
||||
- normalized extension schema and resolved-extension records
|
||||
- static consumers such as skills, validation, auto-enable, and config baseline generation
|
||||
- loader compatibility, initial candidate planning, entry-path import, policy, runtime decisions, post-import register flow, per-candidate orchestration, record-state transitions with explicit compatibility lifecycle mapping, and final cache plus activation finalization
|
||||
- loader compatibility, cache control, initial candidate planning, entry-path import, policy, runtime decisions, post-import register flow, per-candidate orchestration, record-state transitions with explicit compatibility lifecycle mapping, and final cache plus activation finalization
|
||||
|
||||
## Immediate Next Targets
|
||||
|
||||
These are the next lowest-risk cutover steps:
|
||||
|
||||
1. Replace remaining static-only manifest-registry injections with resolved-extension registry inputs where practical.
|
||||
2. Grow the compatibility `lifecycleState` mapping into an explicit lifecycle state machine and move any remaining cache-control or policy orchestration into `src/extension-host/*`.
|
||||
2. Grow the compatibility `lifecycleState` mapping into an explicit lifecycle state machine and move any remaining activation-state or policy orchestration into `src/extension-host/*`.
|
||||
3. Introduce explicit host-owned registration surfaces for runtime writes, starting with the least-coupled registries.
|
||||
4. Move minimal SDK compatibility and loader normalization into `src/extension-host/*` without breaking current `openclaw/plugin-sdk/*` loading.
|
||||
5. Start the first pilot on `extensions/thread-ownership` only after the host-side registry and lifecycle seams are explicit.
|
||||
|
||||
119
src/extension-host/loader-cache.test.ts
Normal file
119
src/extension-host/loader-cache.test.ts
Normal file
@@ -0,0 +1,119 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import type { PluginRegistry } from "../plugins/registry.js";
|
||||
import {
|
||||
buildExtensionHostRegistryCacheKey,
|
||||
clearExtensionHostRegistryCache,
|
||||
getCachedExtensionHostRegistry,
|
||||
MAX_EXTENSION_HOST_REGISTRY_CACHE_ENTRIES,
|
||||
setCachedExtensionHostRegistry,
|
||||
} from "./loader-cache.js";
|
||||
|
||||
function createRegistry(id: string): PluginRegistry {
|
||||
return {
|
||||
plugins: [
|
||||
{
|
||||
id,
|
||||
name: id,
|
||||
source: `/plugins/${id}.js`,
|
||||
origin: "workspace",
|
||||
enabled: true,
|
||||
status: "loaded",
|
||||
lifecycleState: "registered",
|
||||
toolNames: [],
|
||||
hookNames: [],
|
||||
channelIds: [],
|
||||
providerIds: [],
|
||||
gatewayMethods: [],
|
||||
cliCommands: [],
|
||||
services: [],
|
||||
commands: [],
|
||||
httpRoutes: 0,
|
||||
hookCount: 0,
|
||||
configSchema: false,
|
||||
},
|
||||
],
|
||||
tools: [],
|
||||
hooks: [],
|
||||
typedHooks: [],
|
||||
channels: [],
|
||||
providers: [],
|
||||
gatewayHandlers: {},
|
||||
httpRoutes: [],
|
||||
cliRegistrars: [],
|
||||
services: [],
|
||||
commands: [],
|
||||
diagnostics: [],
|
||||
};
|
||||
}
|
||||
|
||||
describe("extension host loader cache", () => {
|
||||
it("normalizes install paths into the cache key", () => {
|
||||
const env = { ...process.env, HOME: "/tmp/home" };
|
||||
|
||||
const first = buildExtensionHostRegistryCacheKey({
|
||||
workspaceDir: "/workspace",
|
||||
plugins: {
|
||||
enabled: true,
|
||||
allow: [],
|
||||
loadPaths: ["~/plugins"],
|
||||
entries: {},
|
||||
slots: {},
|
||||
},
|
||||
installs: {
|
||||
demo: {
|
||||
installPath: "~/demo-install",
|
||||
sourcePath: "~/demo-source",
|
||||
},
|
||||
},
|
||||
env,
|
||||
});
|
||||
const second = buildExtensionHostRegistryCacheKey({
|
||||
workspaceDir: "/workspace",
|
||||
plugins: {
|
||||
enabled: true,
|
||||
allow: [],
|
||||
loadPaths: ["/tmp/home/plugins"],
|
||||
entries: {},
|
||||
slots: {},
|
||||
},
|
||||
installs: {
|
||||
demo: {
|
||||
installPath: "/tmp/home/demo-install",
|
||||
sourcePath: "/tmp/home/demo-source",
|
||||
},
|
||||
},
|
||||
env,
|
||||
});
|
||||
|
||||
expect(first).toBe(second);
|
||||
});
|
||||
|
||||
it("evicts least recently used registries", () => {
|
||||
clearExtensionHostRegistryCache();
|
||||
|
||||
for (let index = 0; index < MAX_EXTENSION_HOST_REGISTRY_CACHE_ENTRIES + 1; index += 1) {
|
||||
setCachedExtensionHostRegistry(`cache-${index}`, createRegistry(`plugin-${index}`));
|
||||
}
|
||||
|
||||
expect(getCachedExtensionHostRegistry("cache-0")).toBeUndefined();
|
||||
expect(
|
||||
getCachedExtensionHostRegistry(`cache-${MAX_EXTENSION_HOST_REGISTRY_CACHE_ENTRIES}`),
|
||||
).toBeDefined();
|
||||
});
|
||||
|
||||
it("refreshes cache insertion order on reads", () => {
|
||||
clearExtensionHostRegistryCache();
|
||||
|
||||
for (let index = 0; index < MAX_EXTENSION_HOST_REGISTRY_CACHE_ENTRIES; index += 1) {
|
||||
setCachedExtensionHostRegistry(`cache-${index}`, createRegistry(`plugin-${index}`));
|
||||
}
|
||||
|
||||
const refreshed = getCachedExtensionHostRegistry("cache-0");
|
||||
expect(refreshed).toBeDefined();
|
||||
|
||||
setCachedExtensionHostRegistry("cache-new", createRegistry("plugin-new"));
|
||||
|
||||
expect(getCachedExtensionHostRegistry("cache-1")).toBeUndefined();
|
||||
expect(getCachedExtensionHostRegistry("cache-0")).toBe(refreshed);
|
||||
});
|
||||
});
|
||||
72
src/extension-host/loader-cache.ts
Normal file
72
src/extension-host/loader-cache.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
import type { PluginInstallRecord } from "../config/types.plugins.js";
|
||||
import type { NormalizedPluginsConfig } from "../plugins/config-state.js";
|
||||
import type { PluginRegistry } from "../plugins/registry.js";
|
||||
import { resolvePluginCacheInputs } from "../plugins/roots.js";
|
||||
import { resolveUserPath } from "../utils.js";
|
||||
|
||||
export const MAX_EXTENSION_HOST_REGISTRY_CACHE_ENTRIES = 32;
|
||||
|
||||
const extensionHostRegistryCache = new Map<string, PluginRegistry>();
|
||||
|
||||
export function clearExtensionHostRegistryCache(): void {
|
||||
extensionHostRegistryCache.clear();
|
||||
}
|
||||
|
||||
export function getCachedExtensionHostRegistry(cacheKey: string): PluginRegistry | undefined {
|
||||
const cached = extensionHostRegistryCache.get(cacheKey);
|
||||
if (!cached) {
|
||||
return undefined;
|
||||
}
|
||||
// Refresh insertion order so frequently reused registries survive eviction.
|
||||
extensionHostRegistryCache.delete(cacheKey);
|
||||
extensionHostRegistryCache.set(cacheKey, cached);
|
||||
return cached;
|
||||
}
|
||||
|
||||
export function setCachedExtensionHostRegistry(cacheKey: string, registry: PluginRegistry): void {
|
||||
if (extensionHostRegistryCache.has(cacheKey)) {
|
||||
extensionHostRegistryCache.delete(cacheKey);
|
||||
}
|
||||
extensionHostRegistryCache.set(cacheKey, registry);
|
||||
while (extensionHostRegistryCache.size > MAX_EXTENSION_HOST_REGISTRY_CACHE_ENTRIES) {
|
||||
const oldestKey = extensionHostRegistryCache.keys().next().value;
|
||||
if (!oldestKey) {
|
||||
break;
|
||||
}
|
||||
extensionHostRegistryCache.delete(oldestKey);
|
||||
}
|
||||
}
|
||||
|
||||
export function buildExtensionHostRegistryCacheKey(params: {
|
||||
workspaceDir?: string;
|
||||
plugins: NormalizedPluginsConfig;
|
||||
installs?: Record<string, PluginInstallRecord>;
|
||||
env: NodeJS.ProcessEnv;
|
||||
}): string {
|
||||
const { roots, loadPaths } = resolvePluginCacheInputs({
|
||||
workspaceDir: params.workspaceDir,
|
||||
loadPaths: params.plugins.loadPaths,
|
||||
env: params.env,
|
||||
});
|
||||
const installs = Object.fromEntries(
|
||||
Object.entries(params.installs ?? {}).map(([pluginId, install]) => [
|
||||
pluginId,
|
||||
{
|
||||
...install,
|
||||
installPath:
|
||||
typeof install.installPath === "string"
|
||||
? resolveUserPath(install.installPath, params.env)
|
||||
: install.installPath,
|
||||
sourcePath:
|
||||
typeof install.sourcePath === "string"
|
||||
? resolveUserPath(install.sourcePath, params.env)
|
||||
: install.sourcePath,
|
||||
},
|
||||
]),
|
||||
);
|
||||
return `${roots.workspace ?? ""}::${roots.global ?? ""}::${roots.stock ?? ""}::${JSON.stringify({
|
||||
...params.plugins,
|
||||
installs,
|
||||
loadPaths,
|
||||
})}`;
|
||||
}
|
||||
@@ -1,7 +1,13 @@
|
||||
import { createJiti } from "jiti";
|
||||
import type { OpenClawConfig } from "../config/config.js";
|
||||
import type { PluginInstallRecord } from "../config/types.plugins.js";
|
||||
import { activateExtensionHostRegistry } from "../extension-host/activation.js";
|
||||
import {
|
||||
buildExtensionHostRegistryCacheKey,
|
||||
clearExtensionHostRegistryCache,
|
||||
getCachedExtensionHostRegistry,
|
||||
MAX_EXTENSION_HOST_REGISTRY_CACHE_ENTRIES,
|
||||
setCachedExtensionHostRegistry,
|
||||
} from "../extension-host/loader-cache.js";
|
||||
import {
|
||||
listPluginSdkAliasCandidates,
|
||||
listPluginSdkExportedSubpaths,
|
||||
@@ -20,19 +26,13 @@ import {
|
||||
} from "../extension-host/loader-policy.js";
|
||||
import type { GatewayRequestHandler } from "../gateway/server-methods/types.js";
|
||||
import { createSubsystemLogger } from "../logging/subsystem.js";
|
||||
import { resolveUserPath } from "../utils.js";
|
||||
import { clearPluginCommands } from "./commands.js";
|
||||
import {
|
||||
applyTestPluginDefaults,
|
||||
normalizePluginsConfig,
|
||||
type NormalizedPluginsConfig,
|
||||
} from "./config-state.js";
|
||||
import { applyTestPluginDefaults, normalizePluginsConfig } from "./config-state.js";
|
||||
import { discoverOpenClawPlugins } from "./discovery.js";
|
||||
import { initializeGlobalHookRunner } from "./hook-runner-global.js";
|
||||
import { clearPluginInteractiveHandlers } from "./interactive.js";
|
||||
import { loadPluginManifestRegistry } from "./manifest-registry.js";
|
||||
import { createPluginRegistry, type PluginRecord, type PluginRegistry } from "./registry.js";
|
||||
import { resolvePluginCacheInputs } from "./roots.js";
|
||||
import { createPluginRuntime, type CreatePluginRuntimeOptions } from "./runtime/index.js";
|
||||
import type { PluginRuntime } from "./runtime/types.js";
|
||||
import { validateJsonSchemaValue } from "./schema-validator.js";
|
||||
@@ -60,12 +60,10 @@ export type PluginLoadOptions = {
|
||||
mode?: "full" | "validate";
|
||||
};
|
||||
|
||||
const MAX_PLUGIN_REGISTRY_CACHE_ENTRIES = 32;
|
||||
const registryCache = new Map<string, PluginRegistry>();
|
||||
const openAllowlistWarningCache = new Set<string>();
|
||||
|
||||
export function clearPluginLoaderCache(): void {
|
||||
registryCache.clear();
|
||||
clearExtensionHostRegistryCache();
|
||||
openAllowlistWarningCache.clear();
|
||||
}
|
||||
|
||||
@@ -216,7 +214,7 @@ export const __testing = {
|
||||
resolveExtensionApiAlias,
|
||||
resolvePluginSdkAliasCandidateOrder,
|
||||
resolvePluginSdkAliasFile,
|
||||
maxPluginRegistryCacheEntries: MAX_PLUGIN_REGISTRY_CACHE_ENTRIES,
|
||||
maxPluginRegistryCacheEntries: MAX_EXTENSION_HOST_REGISTRY_CACHE_ENTRIES,
|
||||
};
|
||||
|
||||
function getCachedPluginRegistry(cacheKey: string): PluginRegistry | undefined {
|
||||
@@ -655,7 +653,7 @@ export function loadOpenClawPlugins(options: PluginLoadOptions = {}): PluginRegi
|
||||
const logger = options.logger ?? defaultLogger();
|
||||
const validateOnly = options.mode === "validate";
|
||||
const normalized = normalizePluginsConfig(cfg.plugins);
|
||||
const cacheKey = buildCacheKey({
|
||||
const cacheKey = buildExtensionHostRegistryCacheKey({
|
||||
workspaceDir: options.workspaceDir,
|
||||
plugins: normalized,
|
||||
installs: cfg.plugins?.installs,
|
||||
@@ -663,7 +661,7 @@ export function loadOpenClawPlugins(options: PluginLoadOptions = {}): PluginRegi
|
||||
});
|
||||
const cacheEnabled = options.cache !== false;
|
||||
if (cacheEnabled) {
|
||||
const cached = getCachedPluginRegistry(cacheKey);
|
||||
const cached = getCachedExtensionHostRegistry(cacheKey);
|
||||
if (cached) {
|
||||
activateExtensionHostRegistry(cached, cacheKey);
|
||||
return cached;
|
||||
@@ -980,7 +978,7 @@ export function loadOpenClawPlugins(options: PluginLoadOptions = {}): PluginRegi
|
||||
env,
|
||||
cacheEnabled,
|
||||
cacheKey,
|
||||
setCachedRegistry: setCachedPluginRegistry,
|
||||
setCachedRegistry: setCachedExtensionHostRegistry,
|
||||
activateRegistry: activateExtensionHostRegistry,
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user