mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 07:20:43 +00:00
fix(plugins): reuse scoped tool registries
This commit is contained in:
@@ -46,6 +46,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Plugins/TTS: keep bundled speech-provider discovery available on cold package Gateway paths and add bundled plugin matrix runtime probes for health, readiness, RPC, TTS discovery, and post-ready runtime-deps watchdog coverage. Refs #75283. Thanks @vincentkoc.
|
||||
- Google Meet/Twilio: show delegated voice call ID, DTMF, and intro-greeting state in `googlemeet doctor`, and avoid claiming DTMF was sent when no Meet PIN sequence was configured. Refs #72478. Thanks @DougButdorf.
|
||||
- Plugins/tools: prefer built bundled plugin code during tool discovery and skip channel runtime hydration while preserving companion provider registrations, reducing per-run plugin-tool prep cost without dropping executable plugin tools. Fixes #75290. Thanks @thanos-openclaw.
|
||||
- Plugins/loader: scope plugin-tool registry reuse to the enabled plugin plan and stored Gateway method keys, so embedded runner tool lookup can reuse compatible startup registries without hiding enabled non-startup plugin tools. Fixes #75520. Thanks @whtoo.
|
||||
- Voice Call/Twilio: send notify-mode initial TwiML directly in the outbound create-call request while keeping conversation and pre-connect DTMF calls webhook-driven, so one-shot notify calls do not depend on a first-answer webhook fetch. Supersedes #72758. Thanks @tyshepps.
|
||||
- Discord/Slack: defer status-reaction cleanup until run finalization so queued, thinking, tool, and terminal reactions no longer flicker during normal progress updates. (#75582)
|
||||
- Discord/voice: leave Discord voice off for text-only configs unless `channels.discord.voice` is explicitly configured, avoiding default `GuildVoiceStates` traffic and idle gateway CPU pressure for bots that do not use `/vc`. Fixes #73753; refs #74044. Thanks @sanchezm86 and @SecureCloudProjO.
|
||||
|
||||
@@ -164,6 +164,154 @@ describe("getCompatibleActivePluginRegistry", () => {
|
||||
}),
|
||||
).toBeUndefined();
|
||||
});
|
||||
|
||||
it("reuses a scoped gateway-bindable registry for an unscoped default-mode request", () => {
|
||||
const registry = createEmptyPluginRegistry();
|
||||
registry.plugins.push(
|
||||
{ id: "acpx" } as (typeof registry.plugins)[number],
|
||||
{ id: "telegram" } as (typeof registry.plugins)[number],
|
||||
);
|
||||
const startupOptions = {
|
||||
config: {
|
||||
plugins: {
|
||||
allow: ["acpx", "telegram"],
|
||||
},
|
||||
},
|
||||
workspaceDir: "/tmp/workspace-a",
|
||||
onlyPluginIds: ["acpx", "telegram"],
|
||||
runtimeOptions: {
|
||||
allowGatewaySubagentBinding: true,
|
||||
},
|
||||
};
|
||||
const { cacheKey } = __testing.resolvePluginLoadCacheContext(startupOptions);
|
||||
setActivePluginRegistry(registry, cacheKey, "gateway-bindable");
|
||||
|
||||
expect(
|
||||
__testing.getCompatibleActivePluginRegistry({
|
||||
config: startupOptions.config,
|
||||
workspaceDir: "/tmp/workspace-a",
|
||||
}),
|
||||
).toBe(registry);
|
||||
});
|
||||
|
||||
it("reuses a scoped gateway-bindable registry for an unscoped snapshot-mode request", () => {
|
||||
const registry = createEmptyPluginRegistry();
|
||||
registry.plugins.push(
|
||||
{ id: "acpx" } as (typeof registry.plugins)[number],
|
||||
{ id: "telegram" } as (typeof registry.plugins)[number],
|
||||
);
|
||||
const startupOptions = {
|
||||
config: {
|
||||
plugins: {
|
||||
allow: ["acpx", "telegram"],
|
||||
},
|
||||
},
|
||||
workspaceDir: "/tmp/workspace-a",
|
||||
onlyPluginIds: ["acpx", "telegram"],
|
||||
runtimeOptions: {
|
||||
allowGatewaySubagentBinding: true,
|
||||
},
|
||||
};
|
||||
const { cacheKey } = __testing.resolvePluginLoadCacheContext(startupOptions);
|
||||
setActivePluginRegistry(registry, cacheKey, "gateway-bindable");
|
||||
|
||||
expect(
|
||||
__testing.getCompatibleActivePluginRegistry({
|
||||
config: startupOptions.config,
|
||||
workspaceDir: "/tmp/workspace-a",
|
||||
activate: false,
|
||||
}),
|
||||
).toBe(registry);
|
||||
});
|
||||
|
||||
it("does not reuse a scoped registry when plugin IDs differ", () => {
|
||||
const registry = createEmptyPluginRegistry();
|
||||
registry.plugins.push({ id: "acpx" } as (typeof registry.plugins)[number]);
|
||||
const startupOptions = {
|
||||
config: {
|
||||
plugins: {
|
||||
allow: ["acpx", "telegram"],
|
||||
},
|
||||
},
|
||||
workspaceDir: "/tmp/workspace-a",
|
||||
onlyPluginIds: ["acpx", "telegram"],
|
||||
runtimeOptions: {
|
||||
allowGatewaySubagentBinding: true,
|
||||
},
|
||||
};
|
||||
const { cacheKey } = __testing.resolvePluginLoadCacheContext(startupOptions);
|
||||
setActivePluginRegistry(registry, cacheKey, "gateway-bindable");
|
||||
|
||||
expect(
|
||||
__testing.getCompatibleActivePluginRegistry({
|
||||
config: startupOptions.config,
|
||||
workspaceDir: "/tmp/workspace-a",
|
||||
}),
|
||||
).toBeUndefined();
|
||||
});
|
||||
|
||||
it("does not reuse a scoped gateway-bindable registry for an explicit subagent request", () => {
|
||||
const registry = createEmptyPluginRegistry();
|
||||
registry.plugins.push(
|
||||
{ id: "acpx" } as (typeof registry.plugins)[number],
|
||||
{ id: "telegram" } as (typeof registry.plugins)[number],
|
||||
);
|
||||
const startupOptions = {
|
||||
config: {
|
||||
plugins: {
|
||||
allow: ["acpx", "telegram"],
|
||||
},
|
||||
},
|
||||
workspaceDir: "/tmp/workspace-a",
|
||||
onlyPluginIds: ["acpx", "telegram"],
|
||||
runtimeOptions: {
|
||||
allowGatewaySubagentBinding: true,
|
||||
},
|
||||
};
|
||||
const { cacheKey } = __testing.resolvePluginLoadCacheContext(startupOptions);
|
||||
setActivePluginRegistry(registry, cacheKey, "gateway-bindable");
|
||||
|
||||
expect(
|
||||
__testing.getCompatibleActivePluginRegistry({
|
||||
config: startupOptions.config,
|
||||
workspaceDir: "/tmp/workspace-a",
|
||||
runtimeOptions: {
|
||||
subagent: {} as CreatePluginRuntimeOptions["subagent"],
|
||||
},
|
||||
}),
|
||||
).toBeUndefined();
|
||||
});
|
||||
|
||||
it("reuses a scoped startup registry when only the request omits gateway methods", () => {
|
||||
const registry = createEmptyPluginRegistry();
|
||||
registry.plugins.push(
|
||||
{ id: "acpx" } as (typeof registry.plugins)[number],
|
||||
{ id: "telegram" } as (typeof registry.plugins)[number],
|
||||
);
|
||||
registry.coreGatewayMethodNames = ["sessions.get", "sessions.list"];
|
||||
const startupOptions = {
|
||||
config: {
|
||||
plugins: {
|
||||
allow: ["acpx", "telegram"],
|
||||
},
|
||||
},
|
||||
workspaceDir: "/tmp/workspace-a",
|
||||
onlyPluginIds: ["acpx", "telegram"],
|
||||
coreGatewayMethodNames: ["sessions.get", "sessions.list"],
|
||||
runtimeOptions: {
|
||||
allowGatewaySubagentBinding: true,
|
||||
},
|
||||
};
|
||||
const { cacheKey } = __testing.resolvePluginLoadCacheContext(startupOptions);
|
||||
setActivePluginRegistry(registry, cacheKey, "gateway-bindable");
|
||||
|
||||
expect(
|
||||
__testing.getCompatibleActivePluginRegistry({
|
||||
config: startupOptions.config,
|
||||
workspaceDir: "/tmp/workspace-a",
|
||||
}),
|
||||
).toBe(registry);
|
||||
});
|
||||
});
|
||||
|
||||
describe("resolveRuntimePluginRegistry", () => {
|
||||
|
||||
@@ -347,6 +347,7 @@ type PluginRegistrySnapshot = {
|
||||
};
|
||||
gatewayHandlers: PluginRegistry["gatewayHandlers"];
|
||||
gatewayMethodScopes: NonNullable<PluginRegistry["gatewayMethodScopes"]>;
|
||||
coreGatewayMethodNames: NonNullable<PluginRegistry["coreGatewayMethodNames"]>;
|
||||
};
|
||||
|
||||
function snapshotPluginRegistry(registry: PluginRegistry): PluginRegistrySnapshot {
|
||||
@@ -387,6 +388,7 @@ function snapshotPluginRegistry(registry: PluginRegistry): PluginRegistrySnapsho
|
||||
},
|
||||
gatewayHandlers: { ...registry.gatewayHandlers },
|
||||
gatewayMethodScopes: { ...registry.gatewayMethodScopes },
|
||||
coreGatewayMethodNames: [...(registry.coreGatewayMethodNames ?? [])],
|
||||
};
|
||||
}
|
||||
|
||||
@@ -426,6 +428,7 @@ function restorePluginRegistry(registry: PluginRegistry, snapshot: PluginRegistr
|
||||
registry.diagnostics = snapshot.arrays.diagnostics;
|
||||
registry.gatewayHandlers = snapshot.gatewayHandlers;
|
||||
registry.gatewayMethodScopes = snapshot.gatewayMethodScopes;
|
||||
registry.coreGatewayMethodNames = snapshot.coreGatewayMethodNames;
|
||||
}
|
||||
|
||||
function createGuardedPluginRegistrationApi(api: OpenClawPluginApi): {
|
||||
@@ -1039,6 +1042,51 @@ function getCompatibleActivePluginRegistry(
|
||||
}
|
||||
}
|
||||
}
|
||||
if (loadContext.onlyPluginIds === undefined) {
|
||||
const scopedOptions = {
|
||||
...options,
|
||||
onlyPluginIds: activeRegistry.plugins.map((entry) => entry.id).toSorted(),
|
||||
coreGatewayMethodNames: activeRegistry.coreGatewayMethodNames ?? [],
|
||||
};
|
||||
if (pluginLoadOptionsMatchCacheKey(scopedOptions, activeCacheKey)) {
|
||||
return activeRegistry;
|
||||
}
|
||||
if (!loadContext.shouldActivate) {
|
||||
const activatingScopedOptions = {
|
||||
...scopedOptions,
|
||||
activate: true,
|
||||
};
|
||||
if (pluginLoadOptionsMatchCacheKey(activatingScopedOptions, activeCacheKey)) {
|
||||
return activeRegistry;
|
||||
}
|
||||
}
|
||||
if (
|
||||
loadContext.runtimeSubagentMode === "default" &&
|
||||
getActivePluginRuntimeSubagentMode() === "gateway-bindable"
|
||||
) {
|
||||
const gatewayBindableScopedOptions = {
|
||||
...scopedOptions,
|
||||
runtimeOptions: {
|
||||
...options.runtimeOptions,
|
||||
allowGatewaySubagentBinding: true,
|
||||
},
|
||||
};
|
||||
if (pluginLoadOptionsMatchCacheKey(gatewayBindableScopedOptions, activeCacheKey)) {
|
||||
return activeRegistry;
|
||||
}
|
||||
if (!loadContext.shouldActivate) {
|
||||
const activatingGatewayBindableScopedOptions = {
|
||||
...gatewayBindableScopedOptions,
|
||||
activate: true,
|
||||
};
|
||||
if (
|
||||
pluginLoadOptionsMatchCacheKey(activatingGatewayBindableScopedOptions, activeCacheKey)
|
||||
) {
|
||||
return activeRegistry;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
|
||||
@@ -26,6 +26,7 @@ export function createEmptyPluginRegistry(): PluginRegistry {
|
||||
memoryEmbeddingProviders: [],
|
||||
agentHarnesses: [],
|
||||
gatewayHandlers: {},
|
||||
coreGatewayMethodNames: [],
|
||||
gatewayMethodScopes: {},
|
||||
httpRoutes: [],
|
||||
cliRegistrars: [],
|
||||
|
||||
@@ -403,6 +403,7 @@ export type PluginRegistry = {
|
||||
memoryEmbeddingProviders: PluginMemoryEmbeddingProviderRegistration[];
|
||||
agentHarnesses: PluginAgentHarnessRegistration[];
|
||||
gatewayHandlers: GatewayRequestHandlers;
|
||||
coreGatewayMethodNames?: string[];
|
||||
gatewayMethodScopes?: Partial<Record<string, OperatorScope>>;
|
||||
httpRoutes: PluginHttpRouteRegistration[];
|
||||
cliRegistrars: PluginCliRegistration[];
|
||||
|
||||
@@ -277,10 +277,14 @@ function resolvePluginRegistrationCapabilities(
|
||||
|
||||
export function createPluginRegistry(registryParams: PluginRegistryParams) {
|
||||
const registry = createEmptyPluginRegistry();
|
||||
const coreGatewayMethods = new Set([
|
||||
...(registryParams.coreGatewayMethodNames ?? []),
|
||||
...Object.keys(registryParams.coreGatewayHandlers ?? {}),
|
||||
]);
|
||||
const coreGatewayMethodNames = Array.from(
|
||||
new Set([
|
||||
...(registryParams.coreGatewayMethodNames ?? []),
|
||||
...Object.keys(registryParams.coreGatewayHandlers ?? {}),
|
||||
]),
|
||||
).toSorted();
|
||||
registry.coreGatewayMethodNames = coreGatewayMethodNames;
|
||||
const coreGatewayMethods = new Set(coreGatewayMethodNames);
|
||||
const pluginHookRollback = new Map<string, HookRollbackEntry[]>();
|
||||
const pluginsWithChannelRegistrationConflict = new Set<string>();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user