Skip to content

Commit 0c3385c

Browse files
committed
Update README.md and add MONKEYPATCH.md to describe problem and support for slash commands
1 parent d78ae3e commit 0c3385c

2 files changed

Lines changed: 272 additions & 0 deletions

File tree

MONKEYPATCH.md

Lines changed: 251 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,251 @@
1+
# Hermes slash-command monkeypatch
2+
3+
This document describes the Hermes Agent-side patch needed when `/bm-*` slash commands do not appear in the gateway even though the Basic Memory provider itself is installed and working.
4+
5+
Agents may use this file as a guide to add support for `/bm-*` slash commands.
6+
7+
## Problem
8+
9+
`hermes-basic-memory` is an **exclusive memory-provider plugin**. Hermes loads exclusive memory providers through `plugins.memory`, not through the normal `PluginManager` discovery path.
10+
11+
Gateway adapters register native slash commands during startup by calling Hermes's plugin command discovery APIs. In affected Hermes builds, that startup path only sees commands registered by normal plugins. The active memory provider has not been loaded yet, and the memory-provider loader uses a collector that captures only `register_memory_provider(...)`. As a result, commands registered by this plugin with `ctx.register_command(...)` never reach the central plugin command registry before Discord/native slash-command sync.
12+
13+
Symptoms:
14+
15+
- `hermes memory status` shows `Provider: basic-memory` and `Status: available`.
16+
- Agent tools such as `bm_search`, `bm_read`, and `bm_recent` work.
17+
- Native slash commands such as `/bm-search`, `/bm-read`, and `/bm-context` are missing after `hermes gateway restart`.
18+
19+
## Target behavior
20+
21+
When Hermes builds its plugin command list, it should also load the configured active memory provider once, allowing that provider to register commands and skills into the same central registries used by ordinary plugins.
22+
23+
After the patch, `get_plugin_commands()` should include commands such as:
24+
25+
```text
26+
bm-context
27+
bm-project
28+
bm-read
29+
bm-recent
30+
bm-remember
31+
bm-search
32+
bm-status
33+
bm-workspace
34+
```
35+
36+
## Files to patch in Hermes Agent
37+
38+
Patch these files in the Hermes Agent repository, not in this plugin repository:
39+
40+
```text
41+
hermes_cli/plugins.py
42+
plugins/memory/__init__.py
43+
```
44+
45+
Recommended tests to add/update:
46+
47+
```text
48+
tests/hermes_cli/test_plugin_cli_registration.py
49+
tests/hermes_cli/test_plugins.py
50+
```
51+
52+
## Implementation outline
53+
54+
### 1. Load active memory-provider commands from `get_plugin_commands()`
55+
56+
In `hermes_cli/plugins.py`, add module-level idempotency/recursion guards near the global plugin manager:
57+
58+
```python
59+
_plugin_manager: Optional[PluginManager] = None
60+
_memory_provider_command_loads: set[str] = set()
61+
_memory_provider_command_loading = False
62+
```
63+
64+
Update `get_plugin_commands()` so it first ensures normal plugin discovery, then best-effort loads the active memory provider before returning the command registry:
65+
66+
```python
67+
def get_plugin_commands() -> Dict[str, dict]:
68+
"""Return the full plugin commands dict (name → {handler, description, plugin}).
69+
70+
Triggers idempotent plugin discovery so callers can use plugin commands
71+
before any explicit discover_plugins() call. Also initializes the active
72+
memory provider once so exclusive memory-provider plugins can contribute
73+
gateway slash commands during startup discovery.
74+
"""
75+
manager = _ensure_plugins_discovered()
76+
_ensure_active_memory_provider_commands_loaded()
77+
return manager._plugin_commands
78+
```
79+
80+
Add the helper:
81+
82+
```python
83+
def _ensure_active_memory_provider_commands_loaded() -> None:
84+
"""Best-effort load of the active memory provider's slash commands."""
85+
global _memory_provider_command_loading
86+
if _memory_provider_command_loading:
87+
return
88+
try:
89+
from plugins import memory as memory_plugins
90+
91+
active = memory_plugins._get_active_memory_provider()
92+
if not active or active in _memory_provider_command_loads:
93+
return
94+
_memory_provider_command_loading = True
95+
try:
96+
memory_plugins.load_memory_provider(active)
97+
_memory_provider_command_loads.add(active)
98+
finally:
99+
_memory_provider_command_loading = False
100+
except Exception as exc:
101+
logger.debug(
102+
"Failed to load active memory-provider plugin commands: %s",
103+
exc,
104+
exc_info=_PLUGINS_DEBUG,
105+
)
106+
```
107+
108+
Notes:
109+
110+
- This must be best-effort; command discovery should not break Hermes startup if a memory provider is misconfigured.
111+
- The recursion guard prevents `load_memory_provider(...)` → provider `register(...)``ctx.register_command(...)` → plugin manager access from re-entering endlessly.
112+
- The load set prevents duplicate provider command registration work.
113+
114+
### 2. Make the memory-provider collector delegate commands and skills
115+
116+
In `plugins/memory/__init__.py`, import `Callable`:
117+
118+
```python
119+
from typing import Callable, List, Optional, Tuple
120+
```
121+
122+
When loading a provider directory, pass the plugin/provider name to the collector:
123+
124+
```python
125+
collector = _ProviderCollector(plugin_name=name)
126+
```
127+
128+
Replace the collector that only captures `register_memory_provider(...)` with a plugin-context shim that also delegates `register_command(...)` and `register_skill(...)` into the central `PluginManager` registries:
129+
130+
```python
131+
class _ProviderCollector:
132+
"""Plugin-context shim used while loading memory providers.
133+
134+
Memory providers are exclusive plugins and are loaded by this module
135+
instead of the general PluginManager. They still need access to the same
136+
slash-command and skill registries as normal plugins, otherwise active
137+
memory-provider commands are invisible during gateway startup discovery.
138+
"""
139+
140+
def __init__(self, plugin_name: str = "memory-provider"):
141+
self.provider = None
142+
self.plugin_name = plugin_name
143+
144+
def register_memory_provider(self, provider):
145+
self.provider = provider
146+
147+
def register_command(
148+
self,
149+
name: str,
150+
handler: Callable,
151+
description: str = "",
152+
args_hint: str = "",
153+
) -> None:
154+
"""Register a memory-provider slash command with PluginManager."""
155+
try:
156+
from hermes_cli.plugins import _ensure_plugins_discovered
157+
except Exception:
158+
return
159+
160+
clean = name.lower().strip().lstrip("/").replace(" ", "-")
161+
if not clean:
162+
return
163+
164+
try:
165+
manager = _ensure_plugins_discovered()
166+
except Exception:
167+
return
168+
169+
plugin_commands = getattr(manager, "_plugin_commands", None)
170+
if plugin_commands is None:
171+
return
172+
plugin_commands[clean] = {
173+
"handler": handler,
174+
"description": description or "Plugin command",
175+
"plugin": self.plugin_name,
176+
"args_hint": (args_hint or "").strip(),
177+
}
178+
179+
def register_skill(
180+
self,
181+
name: str,
182+
path: Path,
183+
description: str = "",
184+
) -> None:
185+
"""Register a memory-provider skill with PluginManager."""
186+
try:
187+
from agent.skill_utils import _NAMESPACE_RE
188+
from hermes_cli.plugins import _ensure_plugins_discovered
189+
except Exception:
190+
return
191+
192+
if ":" in name or not name or not _NAMESPACE_RE.match(name):
193+
raise ValueError(f"Invalid skill name '{name}'.")
194+
if not path.exists():
195+
raise FileNotFoundError(f"SKILL.md not found at {path}")
196+
197+
try:
198+
manager = _ensure_plugins_discovered()
199+
except Exception:
200+
return
201+
202+
plugin_skills = getattr(manager, "_plugin_skills", None)
203+
if plugin_skills is None:
204+
return
205+
plugin_skills[f"{self.plugin_name}:{name}"] = {
206+
"path": path,
207+
"plugin": self.plugin_name,
208+
"bare_name": name,
209+
"description": description,
210+
}
211+
```
212+
213+
Keep existing no-op methods such as `register_tool(...)` and `register_cli_command(...)` as no-ops unless the target Hermes version expects otherwise.
214+
215+
## Verification
216+
217+
From the Hermes Agent repository, run focused compile/tests:
218+
219+
```bash
220+
python -m py_compile hermes_cli/plugins.py plugins/memory/__init__.py
221+
python -m pytest \
222+
tests/hermes_cli/test_plugins.py::TestPluginCommands::test_get_plugin_commands_loads_active_memory_provider_commands \
223+
tests/hermes_cli/test_plugin_cli_registration.py::TestProviderCollectorRegistration \
224+
-q -o 'addopts='
225+
```
226+
227+
Then verify the active config sees the Basic Memory commands:
228+
229+
```bash
230+
python - <<'PY'
231+
import hermes_cli.plugins as p
232+
p._plugin_manager = None
233+
p._memory_provider_command_loads.clear()
234+
cmds = p.get_plugin_commands()
235+
print(sorted(k for k in cmds if k.startswith('bm-')))
236+
PY
237+
```
238+
239+
Expected output:
240+
241+
```text
242+
['bm-context', 'bm-project', 'bm-read', 'bm-recent', 'bm-remember', 'bm-search', 'bm-status', 'bm-workspace']
243+
```
244+
245+
Finally restart the gateway so native slash commands are synced:
246+
247+
```bash
248+
hermes gateway restart
249+
```
250+
251+
For Discord, global command propagation can lag briefly. If the commands do not show immediately, type `/bm` directly or reload the Discord client.

README.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,27 @@ Examples:
9292

9393
`/bm-project` and `/bm-workspace` are read-only in 0.2.0 — mid-session switching is intentionally not supported because auto-capture would otherwise land in the wrong place. Tracked as a follow-up.
9494

95+
### Known issue: `/bm-*` commands may not appear in some Hermes gateway builds
96+
97+
In plugin v0.2.0 the commands above are registered by this plugin, but some Hermes Agent gateway builds do not discover slash commands contributed by an **exclusive memory-provider plugin** during startup. The symptoms are:
98+
99+
- the memory tools work for the agent (`bm_search`, `bm_read`, etc.);
100+
- `hermes memory status` shows `Provider: basic-memory` and `Status: available`; but
101+
- Discord/native slash command pickers do not show `/bm-search`, `/bm-read`, `/bm-context`, and the other `/bm-*` commands after `hermes gateway restart`.
102+
103+
This is a Hermes Agent plugin-discovery issue, not a Basic Memory runtime issue. It is tracked upstream in [NousResearch/hermes-agent#23603](https://github.com/NousResearch/hermes-agent/issues/23603). Until the upstream Hermes fix is available in your installed Hermes version, use one of these workarounds:
104+
105+
1. apply the Hermes-side patch described in [MONKEYPATCH.md](MONKEYPATCH.md), which loads the active memory provider during plugin command discovery; or
106+
2. use the agent tools directly (`bm_search`, `bm_read`, `bm_recent`, etc.) instead of native slash commands.
107+
108+
After applying an updated or patched Hermes build, restart the gateway so Discord/native slash commands are re-synced:
109+
110+
```bash
111+
hermes gateway restart
112+
```
113+
114+
If Discord still does not show the commands immediately, type `/bm` directly or reload the Discord client; global command propagation can lag briefly.
115+
95116
## Configuration
96117

97118
Defaults are reasonable for local use:

0 commit comments

Comments
 (0)