ruvnet/ruflo · error
Invalid plugin replacement
Error message
Invalid plugin replacement
What it means
Thrown by EnhancedPluginRegistry.reload() after the old plugin has been shut down and the replacement resolved: validatePlugin(newPlugin) failed, so the swap is aborted because the replacement does not satisfy the IPlugin contract (metadata, lifecycle methods, state). Note ordering: the old plugin is already shut down when this throws, so the registry slot is left without a running plugin.
Source
Thrown at v3/@claude-flow/plugins/src/registry/enhanced-plugin-registry.ts:555
if (!entry) {
throw new Error(`Plugin ${name} not found`);
}
// Capture state if preserving
let state: unknown;
if (options?.preserveState && (entry.plugin as any).getState) {
state = await (entry.plugin as any).getState();
}
// Shutdown old plugin
if (entry.plugin.state === 'initialized') {
await entry.plugin.shutdown();
}
// Resolve and validate new plugin
const resolved = typeof newPlugin === 'function' ? await newPlugin() : newPlugin;
if (!validatePlugin(resolved)) {
throw new Error('Invalid plugin replacement');
}
// Verify same name
if (resolved.metadata.name !== name) {
throw new Error(`Plugin name mismatch: expected ${name}, got ${resolved.metadata.name}`);
}
// Update dependency graph
const dependencies = this.parseDependencies(resolved.metadata.dependencies);
this.dependencyGraph.removePlugin(name);
this.dependencyGraph.addPlugin(name, resolved.metadata.version, dependencies);
// Initialize new plugin
const context = this.createPluginContext(entry);
const timeout = options?.timeout ?? this.config.loadTimeout ?? 30000;
await Promise.race([
resolved.initialize(context),View on GitHub (pinned to fa13ee4ad6)
Solutions
- Run validatePlugin (or an equivalent shape check) on the replacement BEFORE calling reload, so the old plugin is not shut down for a doomed swap
- Fix the replacement to implement the full IPlugin interface (metadata, init, shutdown, state)
- If the factory can fail, make it throw rather than return a partial object, and validate in a try/catch before reload
Example fix
// before
await registry.reload('logger', async () => ({
metadata: { name: 'logger', version: '2.0.0' },
})); // old plugin shut down, replacement lacks init/shutdown -> throws
// after
const replacement = await buildLoggerPlugin(); // full IPlugin
if (!isIPlugin(replacement)) {
throw new Error('replacement failed validation; keeping old plugin');
}
await registry.reload('logger', replacement); Defensive patterns
Strategy: type-guard
Validate before calling
// Validate the replacement BEFORE reload so the old plugin is never shut down for a doomed swap
const resolved = typeof replacement === 'function' ? await replacement() : replacement;
if (!isIPlugin(resolved)) {
throw new Error('replacement failed IPlugin validation; aborting hot-swap');
}
await registry.reload(name, resolved); Type guard
function isIPlugin(candidate: unknown): candidate is IPlugin {
if (typeof candidate !== 'object' || candidate === null) return false;
const p = candidate as Record<string, unknown>;
const meta = p.metadata as Record<string, unknown> | undefined;
return (
!!meta &&
typeof meta.name === 'string' &&
typeof meta.version === 'string' &&
typeof p.init === 'function' &&
typeof p.shutdown === 'function'
);
} Try / catch
try {
await registry.reload(name, newPlugin);
} catch (err) {
if (err instanceof Error && err.message === 'Invalid plugin replacement') {
// old plugin is already shut down: re-register the previous build or a safe fallback
await registry.register(previousGoodPlugin);
}
throw err;
} Prevention
- Run validatePlugin on the built artifact before initiating a hot-swap
- Keep the last-known-good plugin available to restore when a swap fails
- Type hot-swap payloads as IPlugin and compile-check them against the current SDK
When it happens
Trigger: Passing a replacement object or factory result that lacks required IPlugin members (metadata.name/version, init/shutdown); a factory that resolves to a partial or undefined value; swapping in a plugin built against an older SDK interface; module-namespace/default import confusion yielding a non-plugin object.
Common situations: Hot-reloading a freshly edited plugin whose export shape changed; the new plugin file having an export problem that still resolves to an object; SDK upgrade renaming lifecycle methods; dev workflow where a broken build is hot-swapped in.
Related errors
- Invalid plugin: does not implement IPlugin interface
- Plugin name mismatch: expected ${name}, got ${resolved.metad
- Invalid plugin: does not implement IPlugin interface
- Plugin ${name} already registered
- Maximum plugin limit (${this.config.maxPlugins}) reached
AI-assisted analysis of ruvnet/ruflo@fa13ee4ad6 (2026-08-18).
Data as JSON: /api/errors/8971308dd4fc5eec.
Report an issue: GitHub.