ruvnet/ruflo · error · PluginError

DEPENDENCY_NOT_FOUND

DEPENDENCY_NOT_FOUND

Error message

Cannot unload plugin '${pluginName}': depended on by ${dependents.join(', ')}

What it means

unloadPlugin() refuses to unload a plugin that other loaded plugins declare in their dependencies array; the error message lists the dependent plugin names, and the PluginError code is 'DEPENDENCY_NOT_FOUND' (a misleading label for 'has dependents'). Removing a dependency out from under a live plugin would leave it calling into freed code, so dependents must be unloaded first - i.e. reverse of the initialization order exposed by getInitializationOrder().

Source

Thrown at v3/@claude-flow/shared/src/plugin-loader.ts:195

  }

  /**
   * Unload a single plugin
   */
  async unloadPlugin(pluginName: string): Promise<void> {
    const pluginInfo = this.registry.getPlugin(pluginName);
    if (!pluginInfo) {
      throw new PluginError(
        `Plugin '${pluginName}' not found`,
        pluginName,
        'INVALID_PLUGIN'
      );
    }

    // Check for dependents
    const dependents = this.findDependents(pluginName);
    if (dependents.length > 0) {
      throw new PluginError(
        `Cannot unload plugin '${pluginName}': depended on by ${dependents.join(', ')}`,
        pluginName,
        'DEPENDENCY_NOT_FOUND'
      );
    }

    // Shutdown plugin
    await this.shutdownPlugin(pluginInfo.plugin);

    // Unregister plugin
    this.registry.unregisterPlugin(pluginName);

    // Remove from initialization order
    const index = this.initializationOrder.indexOf(pluginName);
    if (index !== -1) {
      this.initializationOrder.splice(index, 1);
    }
  }

View on GitHub (pinned to fa13ee4ad6)

Solutions

  1. Unload in reverse initialization order: [...loader.getInitializationOrder()].reverse()
  2. Or unload the dependents named in the error message first, then the target
  3. For 'remove the whole subtree' semantics, walk the dependents list transitively before the target

Example fix

// before
await loader.unloadPlugin('storage'); // 'auth' depends on it -> throws

// after
for (const name of [...loader.getInitializationOrder()].reverse()) {
  await loader.unloadPlugin(name); // dependents come off before their dependencies
}
Defensive patterns

Strategy: validation

Validate before calling

// unload in reverse init order so dependents always go before their dependencies
for (const name of [...loader.getInitializationOrder()].reverse()) {
  await loader.unloadPlugin(name);
}

Try / catch

try {
  await loader.unloadPlugin(target);
} catch (e) {
  if (e instanceof PluginError && e.code === 'DEPENDENCY_NOT_FOUND') {
    const dependents = e.message.match(/by (.+)$/)?.[1]?.split(', ') ?? [];
    for (const d of dependents) await loader.unloadPlugin(d);
    await loader.unloadPlugin(target); // now safe
    return;
  }
  throw e;
}

Prevention

When it happens

Trigger: Unloading 'storage' while 'auth' declares dependencies: ['storage']; teardown order hardcoded instead of reverse-init; a partial unload loop where one dependent failed to unload and the loop continues on to its dependency; dynamic unload triggered by an admin action.

Common situations: Layered plugin systems (core -> storage -> auth); tests unloading one plugin in isolation; runtime enable/disable features that ignore the dependency graph.

Related errors


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