{"record":{"id":"9fd3ecc381feab76","repo":"ruvnet/ruflo","slug":"dependency-not-found","errorCode":"DEPENDENCY_NOT_FOUND","errorMessage":"Cannot unload plugin '${pluginName}': depended on by ${dependents.join(', ')}","messagePattern":"Cannot unload plugin '(.+?)': depended on by (.+?)","errorType":"exception","errorClass":"PluginError","httpStatus":null,"severity":"error","filePath":"v3/@claude-flow/shared/src/plugin-loader.ts","lineNumber":195,"sourceCode":"  }\n\n  /**\n   * Unload a single plugin\n   */\n  async unloadPlugin(pluginName: string): Promise<void> {\n    const pluginInfo = this.registry.getPlugin(pluginName);\n    if (!pluginInfo) {\n      throw new PluginError(\n        `Plugin '${pluginName}' not found`,\n        pluginName,\n        'INVALID_PLUGIN'\n      );\n    }\n\n    // Check for dependents\n    const dependents = this.findDependents(pluginName);\n    if (dependents.length > 0) {\n      throw new PluginError(\n        `Cannot unload plugin '${pluginName}': depended on by ${dependents.join(', ')}`,\n        pluginName,\n        'DEPENDENCY_NOT_FOUND'\n      );\n    }\n\n    // Shutdown plugin\n    await this.shutdownPlugin(pluginInfo.plugin);\n\n    // Unregister plugin\n    this.registry.unregisterPlugin(pluginName);\n\n    // Remove from initialization order\n    const index = this.initializationOrder.indexOf(pluginName);\n    if (index !== -1) {\n      this.initializationOrder.splice(index, 1);\n    }\n  }","sourceCodeStart":177,"sourceCodeEnd":213,"githubUrl":"https://github.com/ruvnet/ruflo/blob/fa13ee4ad60ac2090b1480656eb233521790d640/v3/@claude-flow/shared/src/plugin-loader.ts#L177-L213","documentation":"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().","triggerScenarios":"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.","commonSituations":"Layered plugin systems (core -> storage -> auth); tests unloading one plugin in isolation; runtime enable/disable features that ignore the dependency graph.","solutions":["Unload in reverse initialization order: [...loader.getInitializationOrder()].reverse()","Or unload the dependents named in the error message first, then the target","For 'remove the whole subtree' semantics, walk the dependents list transitively before the target"],"exampleFix":"// before\nawait loader.unloadPlugin('storage'); // 'auth' depends on it -> throws\n\n// after\nfor (const name of [...loader.getInitializationOrder()].reverse()) {\n  await loader.unloadPlugin(name); // dependents come off before their dependencies\n}","handlingStrategy":"validation","validationCode":"// unload in reverse init order so dependents always go before their dependencies\nfor (const name of [...loader.getInitializationOrder()].reverse()) {\n  await loader.unloadPlugin(name);\n}","typeGuard":null,"tryCatchPattern":"try {\n  await loader.unloadPlugin(target);\n} catch (e) {\n  if (e instanceof PluginError && e.code === 'DEPENDENCY_NOT_FOUND') {\n    const dependents = e.message.match(/by (.+)$/)?.[1]?.split(', ') ?? [];\n    for (const d of dependents) await loader.unloadPlugin(d);\n    await loader.unloadPlugin(target); // now safe\n    return;\n  }\n  throw e;\n}","preventionTips":["Always tear down plugins in reverse getInitializationOrder()","Model plugin dependencies explicitly and never hardcode teardown order","Parse the dependents list from the message when handling targeted unload requests"],"tags":["plugins","loader","dependencies","unload","ordering"],"backgroundTag":"plugin-dependency-conflict","analyzedSha":"fa13ee4ad60ac2090b1480656eb233521790d640","analyzedAt":"2026-08-18T21:34:22.708Z","contentChangedAt":"2026-08-18T21:34:22.708Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}