mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 07:10:43 +00:00
docs: frame installed manifest cache as fallback
This commit is contained in:
@@ -84,6 +84,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Plugins/extractors: reuse one manifest registry pass while resolving bundled document and web-content extractor plugins instead of rereading manifests for compatibility and enablement filtering. Thanks @shakkernerd.
|
||||
- Plugins/providers: reuse one plugin registry snapshot and manifest registry while resolving provider discovery entries instead of rebuilding manifest metadata after provider owner discovery. Thanks @shakkernerd.
|
||||
- Plugins/registry: resolve lookup-table owner maps for providers, CLI backends, setup providers, command aliases, model catalogs, channel configs, and manifest contracts while preserving setup-only CLI backend ownership. Thanks @shakkernerd.
|
||||
- Plugins/registry: cache repeated installed-index manifest registry fallback rebuilds behind a bounded invalidating cache so cold provider-discovery paths avoid rereading unchanged manifests. Thanks @mcaxtr.
|
||||
- Plugins/web: reuse manifest records already loaded for bundled web provider candidate discovery when falling back to public artifact provider loading. Thanks @shakkernerd.
|
||||
- Mattermost: keep direct-message replies top-level by suppressing reply roots for DM delivery while preserving channel and group thread roots, and derive inbound chat kind from the trusted channel lookup instead of the websocket event channel type. Carries forward #60115, #55186, #72305, and #72659; refs #59758, #59981, #59791, and #57565. Thanks @vincentkoc, @jwchmodx, and @hnykda.
|
||||
- Process/Windows: decode command stdout and stderr from raw bytes with console-codepage awareness, while preserving valid UTF-8 output and multibyte characters split across chunks. Fixes #50519. Thanks @iready, @kevinten10, @zhangyongjie1997, @knightplat-blip, @heiqishi666, and @slepybear.
|
||||
|
||||
@@ -159,6 +159,8 @@ The lookup table keeps repeated startup decisions on the fast path:
|
||||
|
||||
The safety boundary is snapshot replacement, not mutation. Rebuild the table when config, plugin inventory, install records, or persisted index policy changes. Do not treat it as a broad mutable global registry, and do not keep unbounded historical tables. Runtime plugin loading remains separate from lookup-table metadata so stale runtime state cannot be hidden behind a metadata cache.
|
||||
|
||||
Some cold-path callers still reconstruct manifest registries directly from the persisted installed plugin index instead of receiving a Gateway `PluginLookUpTable`. That fallback path keeps a small bounded in-memory cache keyed by the installed index, request shape, config policy, runtime roots, and manifest/package file signatures. It is a fallback safety net for repeated index reconstruction, not the preferred Gateway hot path. Prefer passing the current lookup table or an explicit manifest registry through runtime flows when a caller already has one.
|
||||
|
||||
### Activation planning
|
||||
|
||||
Activation planning is part of the control plane. Callers can ask which plugins are relevant to a concrete command, provider, channel, route, agent harness, or capability before loading broader runtime registries.
|
||||
|
||||
@@ -137,6 +137,33 @@ describe("loadPluginManifestRegistryForInstalledIndex", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("bypasses the installed-index manifest registry cache when disabled", () => {
|
||||
const rootDir = makeTempDir();
|
||||
writePlugin(rootDir, "installed", "installed-");
|
||||
const index = createIndex(rootDir);
|
||||
const readFileSync = vi.spyOn(fs, "readFileSync");
|
||||
const env = {
|
||||
OPENCLAW_DISABLE_INSTALLED_PLUGIN_MANIFEST_REGISTRY_CACHE: "1",
|
||||
OPENCLAW_VERSION: "2026.4.25",
|
||||
VITEST: "true",
|
||||
};
|
||||
|
||||
const first = loadPluginManifestRegistryForInstalledIndex({
|
||||
index,
|
||||
env,
|
||||
includeDisabled: true,
|
||||
});
|
||||
const readsAfterFirstLoad = readFileSync.mock.calls.length;
|
||||
const second = loadPluginManifestRegistryForInstalledIndex({
|
||||
index,
|
||||
env,
|
||||
includeDisabled: true,
|
||||
});
|
||||
|
||||
expect(second).not.toBe(first);
|
||||
expect(readFileSync.mock.calls.length).toBeGreaterThan(readsAfterFirstLoad);
|
||||
});
|
||||
|
||||
it("loads manifest metadata only for plugins present in the installed index", () => {
|
||||
const installedRoot = makeTempDir();
|
||||
const unrelatedRoot = makeTempDir();
|
||||
|
||||
@@ -15,15 +15,18 @@ import {
|
||||
type PackageManifest,
|
||||
} from "./manifest.js";
|
||||
|
||||
const INSTALLED_MANIFEST_REGISTRY_CACHE_MAX_ENTRIES = 64;
|
||||
const INSTALLED_MANIFEST_REGISTRY_FALLBACK_CACHE_MAX_ENTRIES = 64;
|
||||
|
||||
type InstalledManifestRegistryCacheEntry = {
|
||||
registry: PluginManifestRegistry;
|
||||
lastUsed: number;
|
||||
};
|
||||
|
||||
const installedManifestRegistryCache = new Map<string, InstalledManifestRegistryCacheEntry>();
|
||||
let installedManifestRegistryCacheTick = 0;
|
||||
const installedManifestRegistryFallbackCache = new Map<
|
||||
string,
|
||||
InstalledManifestRegistryCacheEntry
|
||||
>();
|
||||
let installedManifestRegistryFallbackCacheTick = 0;
|
||||
|
||||
function normalizePluginIdFilter(pluginIds: readonly string[] | undefined): string[] | undefined {
|
||||
if (!pluginIds?.length) {
|
||||
@@ -131,11 +134,11 @@ function buildInstalledManifestRegistryCacheKey(params: {
|
||||
}
|
||||
|
||||
function getCachedInstalledManifestRegistry(cacheKey: string): PluginManifestRegistry | undefined {
|
||||
const cached = installedManifestRegistryCache.get(cacheKey);
|
||||
const cached = installedManifestRegistryFallbackCache.get(cacheKey);
|
||||
if (!cached) {
|
||||
return undefined;
|
||||
}
|
||||
cached.lastUsed = ++installedManifestRegistryCacheTick;
|
||||
cached.lastUsed = ++installedManifestRegistryFallbackCacheTick;
|
||||
return cached.registry;
|
||||
}
|
||||
|
||||
@@ -144,29 +147,31 @@ function setCachedInstalledManifestRegistry(
|
||||
registry: PluginManifestRegistry,
|
||||
): void {
|
||||
if (
|
||||
!installedManifestRegistryCache.has(cacheKey) &&
|
||||
installedManifestRegistryCache.size >= INSTALLED_MANIFEST_REGISTRY_CACHE_MAX_ENTRIES
|
||||
!installedManifestRegistryFallbackCache.has(cacheKey) &&
|
||||
installedManifestRegistryFallbackCache.size >=
|
||||
INSTALLED_MANIFEST_REGISTRY_FALLBACK_CACHE_MAX_ENTRIES
|
||||
) {
|
||||
let oldestKey: string | undefined;
|
||||
let oldestTick = Number.POSITIVE_INFINITY;
|
||||
for (const [key, entry] of installedManifestRegistryCache) {
|
||||
for (const [key, entry] of installedManifestRegistryFallbackCache) {
|
||||
if (entry.lastUsed < oldestTick) {
|
||||
oldestKey = key;
|
||||
oldestTick = entry.lastUsed;
|
||||
}
|
||||
}
|
||||
if (oldestKey) {
|
||||
installedManifestRegistryCache.delete(oldestKey);
|
||||
installedManifestRegistryFallbackCache.delete(oldestKey);
|
||||
}
|
||||
}
|
||||
installedManifestRegistryCache.set(cacheKey, {
|
||||
installedManifestRegistryFallbackCache.set(cacheKey, {
|
||||
registry,
|
||||
lastUsed: ++installedManifestRegistryCacheTick,
|
||||
lastUsed: ++installedManifestRegistryFallbackCacheTick,
|
||||
});
|
||||
}
|
||||
|
||||
export function clearInstalledManifestRegistryCache(): void {
|
||||
installedManifestRegistryCache.clear();
|
||||
installedManifestRegistryFallbackCache.clear();
|
||||
installedManifestRegistryFallbackCacheTick = 0;
|
||||
}
|
||||
|
||||
function resolveInstalledPluginRootDir(record: InstalledPluginIndexRecord): string {
|
||||
|
||||
Reference in New Issue
Block a user