can1357/oh-my-pi · error · ValueError

Plugin '${name}' is already registered

Error message

Plugin '${name}' is already registered

What it means

PluginManager keeps a registry map of plugin names to constructors and forbids duplicate names so loadPlugin always resolves unambiguously. The ValueError fires on a second registerPlugin call with a name already in the registry — including the built-in names registered in the constructor.

Source

Thrown at packages/mnemopi/src/core/plugins.ts:275

	override onInvalidate(_memoryId: string): void {}
}

export type PluginConstructor<T extends MnemopiPlugin = MnemopiPlugin> = new (config?: PluginConfig) => T;

export class PluginManager {
	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>> {

View on GitHub (pinned to 9690622007)

Solutions

  1. Pick a unique name for the new plugin before calling registerPlugin
  2. Call manager.hasPlugin(name) / listPlugins() first and only register when the name is absent
  3. If replacing a built-in, use a different name or the API's designated override path instead of re-registering

Example fix

// before
manager.registerPlugin("metrics", MyMetricsPlugin); // ValueError: built-in exists
// after
if (!manager.listPlugins().some(p => p.name === "metrics")) {
  manager.registerPlugin("metrics", MyMetricsPlugin);
}
Defensive patterns

Strategy: validation

Validate before calling

const taken = new Set(manager.listPlugins().map(p => p.name));
if (!taken.has(name)) manager.registerPlugin(name, pluginClass);

Try / catch

try {
  manager.registerPlugin(name, pluginClass);
} catch (err) {
  if (err instanceof ValueError && err.message.includes("already registered")) return; // idempotent re-init
  throw err;
}

Prevention

When it happens

Trigger: Calling registerPlugin("metrics", ...) or registerPlugin("filter", ...) or registerPlugin("compression", ...) after the manager constructor has already registered those built-ins; registering the same custom name twice.

Common situations: Re-initializing or hot-reloading a manager/module so the constructor runs twice; creating a second manager instance in the same process when the registry is shared; typo colliding with a built-in plugin name.

Related errors


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