can1357/oh-my-pi · error · Error
Plugin '${name}' is already loaded
Error message
Plugin '${name}' is already loaded What it means
loadPlugin enforces one live instance per plugin name; the instances map is the source of truth. Calling loadPlugin a second time for an already-loaded plugin throws instead of returning the existing instance or resetting it.
Source
Thrown at packages/mnemopi/src/core/plugins.ts:281
private readonly registry = new Map<string, PluginConstructor>();
private readonly instances = new Map<string, MnemopiPlugin>();
constructor(private readonly pluginDir = DEFAULT_PLUGIN_DIR) {
this.registerPlugin("logging", LoggingPlugin);
this.registerPlugin("metrics", MetricsPlugin);
this.registerPlugin("filter", FilterPlugin);
this.registerPlugin("compression", CompressionPlugin);
}
registerPlugin(name: string, pluginClass: PluginConstructor): void {
if (typeof pluginClass !== "function" || !(pluginClass.prototype instanceof MnemopiPlugin)) {
throw new TypeError("pluginClass must be a MnemopiPlugin subclass");
}
if (this.registry.has(name)) throw new ValueError(`Plugin '${name}' is already registered`);
this.registry.set(name, pluginClass);
}
loadPlugin(name: string, config: PluginConfig = {}): MnemopiPlugin {
const pluginClass = this.registry.get(name);
if (pluginClass === undefined) throw new ValueError(`Plugin '${name}' is not registered`);
if (this.instances.has(name)) throw new Error(`Plugin '${name}' is already loaded`);
const instance = new pluginClass(config);
instance.initialize();
this.instances.set(name, instance);
return instance;
}
unloadPlugin(name: string): void {
const instance = this.instances.get(name);
if (instance === undefined) throw new ValueError(`Plugin '${name}' is not loaded`);
this.instances.delete(name);
instance.shutdown();
}
listPlugins(): Array<Record<string, unknown>> {
const result: Array<Record<string, unknown>> = [];
for (const [name, pluginClass] of this.registry)
result.push({
name,
class: pluginClass.name,
loaded: this.instances.has(name),View on GitHub (pinned to 9690622007)
Solutions
- Call unloadPlugin(name) before loadPlugin if a fresh instance is intended
- Use getPlugin(name) to fetch the already-loaded instance instead of loadPlugin
- Guard loads: only call loadPlugin when the instance is not yet present
Example fix
// before
const a = manager.loadPlugin("metrics");
const b = manager.loadPlugin("metrics"); // Error: already loaded
// after
const b = manager.getPlugin("metrics") ?? manager.loadPlugin("metrics"); Defensive patterns
Strategy: try-catch
Validate before calling
// fetch-or-load instead of blind load
const plugin = manager.getPlugin("metrics") ?? manager.loadPlugin("metrics"); Try / catch
function loadOnce(name: string, config?: PluginConfig) {
try {
return manager.loadPlugin(name, config);
} catch (err) {
if (err instanceof Error && err.message.includes("already loaded")) {
return manager.getPlugin(name);
}
throw err;
}
} Prevention
- Centralize plugin loading in one accessor (get-or-load)
- Never call loadPlugin twice for the same name; use unloadPlugin first when a fresh instance is needed
- Track loaded state in your bootstrap to avoid double init paths
When it happens
Trigger: Calling manager.loadPlugin("metrics") twice without an intervening unloadPlugin("metrics"); application startup code and a getPlugin/plugin accessor both invoking loadPlugin for the same name.
Common situations: Double initialization paths (bootstrap plus lazy accessor); retrying startup after a partial failure that already loaded the plugin; re-running an init script against a long-lived manager.
Related errors
- Plugin '${name}' is already registered
- Plugin '${name}' is not registered
- Plugin '${name}' is not loaded
- DAP adapter ${this.adapter.name} is not running
- Debug session ${root.id} is still active. Terminate it befor
AI-assisted analysis of can1357/oh-my-pi@9690622007 (2026-08-31).
Data as JSON: /api/errors/0f9497762cbae821.
Report an issue: GitHub.