You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Memory-provider plugins are loaded through plugins/memory/_load_provider_from_dir, which passes the plugin's register(ctx) a _ProviderCollector instance. That collector captures only register_memory_provider; every other registration method is missing or a no-op. As a result, memory-provider plugins that try to register slash commands (ctx.register_command(...)) or bundled skills (ctx.register_skill(...)) silently lose those registrations — even though PluginManager has fully working _plugin_commands and _plugin_skills registries that general plugins use successfully.
Where it happens
plugins/memory/__init__.py:288-305:
class_ProviderCollector:
"""Fake plugin context that captures register_memory_provider calls."""def__init__(self):
self.provider=Nonedefregister_memory_provider(self, provider):
self.provider=provider# No-op for other registration methodsdefregister_tool(self, *args, **kwargs):
passdefregister_hook(self, *args, **kwargs):
passdefregister_cli_command(self, *args, **kwargs):
pass
register_skill and register_command are not present at all — so hasattr(ctx, "register_skill") returns False, defensive guards skip the call entirely, and the plugin runs as if the bundled SKILL.md and slash commands never existed.
Impact
Any memory-provider plugin that wants in-session UX beyond the seven MemoryProvider hooks has no supported path. Two concrete cases we've hit in basicmachines-co/hermes-basic-memory:
register_skill has been silently no-opping since we added it in 0.1.5. The bundled SKILL.md was never reachable via skill:view basic-memory:basic-memory in real installs (manual symlink to ~/.hermes/skills/ was the only path that worked).
This is also masked by the most natural unit-test idiom: passing a MagicMock() as ctx makes every attribute exist, so the calls "succeed" in tests while skipping in production. We only caught it because a code review (Codex) flagged the loader-path mismatch.
Proposed fix
A small surgical change in plugins/memory/__init__.py — teach _ProviderCollector to delegate to PluginManager. The dicts and dispatch are already there; only the write side is missing for memory-provider plugins. Roughly:
With _load_provider_from_dir passing manifest_name=provider_dir.name when constructing the collector.
Recursion is safe: PluginManager.discover_and_load is idempotent (plugins.py:699) and already skips memory-provider plugins at the manifest-routing stage (plugins.py:792-802), so the inner discovery call cannot re-enter register().
Happy to send a PR if that fits the project's contribution norms.
Workaround in the wild
Until this lands, plugins can reach into PluginManager from inside their own register() — same logic as the proposed delegation, just on the other side of the import boundary. We shipped this in basicmachines-co/hermes-basic-memory#3 (_register_via_plugin_manager in __init__.py) as a documented workaround. When this issue is fixed, the reach-in becomes a redundant double-write of identical entries — safe to leave in place during the transition, simple to remove later.
Summary
Memory-provider plugins are loaded through
plugins/memory/_load_provider_from_dir, which passes the plugin'sregister(ctx)a_ProviderCollectorinstance. That collector captures onlyregister_memory_provider; every other registration method is missing or a no-op. As a result, memory-provider plugins that try to register slash commands (ctx.register_command(...)) or bundled skills (ctx.register_skill(...)) silently lose those registrations — even thoughPluginManagerhas fully working_plugin_commandsand_plugin_skillsregistries that general plugins use successfully.Where it happens
plugins/memory/__init__.py:288-305:register_skillandregister_commandare not present at all — sohasattr(ctx, "register_skill")returns False, defensive guards skip the call entirely, and the plugin runs as if the bundled SKILL.md and slash commands never existed.Impact
Any memory-provider plugin that wants in-session UX beyond the seven
MemoryProviderhooks has no supported path. Two concrete cases we've hit inbasicmachines-co/hermes-basic-memory:register_skillhas been silently no-opping since we added it in 0.1.5. The bundledSKILL.mdwas never reachable viaskill:view basic-memory:basic-memoryin real installs (manual symlink to~/.hermes/skills/was the only path that worked).register_commandfor our new/bm-*surface (issue Support passing morph snapshot id #2 in our repo) silently dropped all eight commands.This is also masked by the most natural unit-test idiom: passing a
MagicMock()asctxmakes every attribute exist, so the calls "succeed" in tests while skipping in production. We only caught it because a code review (Codex) flagged the loader-path mismatch.Proposed fix
A small surgical change in
plugins/memory/__init__.py— teach_ProviderCollectorto delegate toPluginManager. The dicts and dispatch are already there; only the write side is missing for memory-provider plugins. Roughly:With
_load_provider_from_dirpassingmanifest_name=provider_dir.namewhen constructing the collector.Recursion is safe:
PluginManager.discover_and_loadis idempotent (plugins.py:699) and already skips memory-provider plugins at the manifest-routing stage (plugins.py:792-802), so the inner discovery call cannot re-enterregister().Happy to send a PR if that fits the project's contribution norms.
Workaround in the wild
Until this lands, plugins can reach into
PluginManagerfrom inside their ownregister()— same logic as the proposed delegation, just on the other side of the import boundary. We shipped this inbasicmachines-co/hermes-basic-memory#3(_register_via_plugin_managerin__init__.py) as a documented workaround. When this issue is fixed, the reach-in becomes a redundant double-write of identical entries — safe to leave in place during the transition, simple to remove later.Link for reference: basicmachines-co/hermes-basic-memory#3