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

  1. 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
  2. Fix the replacement to implement the full IPlugin interface (metadata, init, shutdown, state)
  3. 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

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


AI-assisted analysis of ruvnet/ruflo@fa13ee4ad6 (2026-08-18). Data as JSON: /api/errors/8971308dd4fc5eec. Report an issue: GitHub.