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

  1. Call unloadPlugin(name) before loadPlugin if a fresh instance is intended
  2. Use getPlugin(name) to fetch the already-loaded instance instead of loadPlugin
  3. 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

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


AI-assisted analysis of can1357/oh-my-pi@9690622007 (2026-08-31). Data as JSON: /api/errors/0f9497762cbae821. Report an issue: GitHub.