Skip to content
This repository was archived by the owner on May 31, 2026. It is now read-only.

Commit d436101

Browse files
committed
fix: lazily initialize slash command provider
1 parent b4fe75b commit d436101

4 files changed

Lines changed: 74 additions & 18 deletions

File tree

MONKEYPATCH.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
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.
44

5-
Agents may use this file as a guide to add support for `/bm-*` slash commands.
5+
Agents may use this file as a guide to add support for `/bm-*` slash commands in Hermes Agent builds that do not yet include the upstream fix.
66

77
## Problem
88

@@ -42,7 +42,7 @@ hermes_cli/plugins.py
4242
plugins/memory/__init__.py
4343
```
4444

45-
Recommended tests to add/update:
45+
Recommended tests to add/update in Hermes Agent:
4646

4747
```text
4848
tests/hermes_cli/test_plugin_cli_registration.py

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ In plugin v0.2.0 the commands above are registered by this plugin, but some Herm
102102

103103
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:
104104

105-
1. apply the Hermes-side patch described in [MONKEYPATCH.md](MONKEYPATCH.md), which loads the active memory provider during plugin command discovery; or
105+
1. apply the Hermes Agent-side patch described in [MONKEYPATCH.md](MONKEYPATCH.md), which loads the active memory provider during plugin command discovery; or
106106
2. use the agent tools directly (`bm_search`, `bm_read`, `bm_recent`, etc.) instead of native slash commands.
107107

108108
After applying an updated or patched Hermes build, restart the gateway so Discord/native slash commands are re-synced:

__init__.py

Lines changed: 50 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -682,6 +682,13 @@ def initialize(self, session_id: str, **kwargs: Any) -> None:
682682
self._capture_folder = cfg.get("capture_folder") or "hermes-sessions"
683683
self._remember_folder = cfg.get("remember_folder") or "bm-remember"
684684

685+
if not _MCP_AVAILABLE:
686+
logger.error(
687+
"basic-memory: MCP SDK unavailable; provider will not initialize: %s",
688+
_MCP_IMPORT_ERROR,
689+
)
690+
return
691+
685692
# Bootstrap-install bm via uv if it's not already on disk. One-time cost
686693
# on a fresh machine; idempotent no-op once basic-memory is installed.
687694
if not _bm_binary_path():
@@ -1229,10 +1236,39 @@ def _format_result_rows(items: List[Any], header: str, empty_msg: str) -> str:
12291236
return "\n".join(lines)
12301237

12311238

1239+
_slash_init_lock = threading.Lock()
1240+
1241+
12321242
def _slash_uninit(cmd: str) -> str:
12331243
return f"{cmd}: basic-memory provider not initialized. Run `hermes memory status` to diagnose."
12341244

12351245

1246+
def _ensure_slash_ready(provider: "BasicMemoryProvider", cmd: str) -> Optional[str]:
1247+
"""Lazily initialize the provider for human-invoked /bm-* commands.
1248+
1249+
Hermes gateway command discovery can load memory-provider plugins before an
1250+
agent session initializes memory. Slash handlers close over that discovered
1251+
provider instance, so they need a small on-demand initialization path before
1252+
touching the BM MCP actor.
1253+
"""
1254+
if provider._initialized and provider._actor is not None:
1255+
return None
1256+
with _slash_init_lock:
1257+
if provider._initialized and provider._actor is not None:
1258+
return None
1259+
try:
1260+
provider.initialize(
1261+
session_id=f"slash:{cmd}:{int(time.time())}",
1262+
hermes_home=os.path.expanduser("~/.hermes"),
1263+
)
1264+
except Exception as e:
1265+
logger.warning("basic-memory: slash init for /%s failed: %s", cmd, e)
1266+
return f"{cmd}: failed to initialize basic-memory provider: {e}"
1267+
if not provider._initialized or provider._actor is None:
1268+
return _slash_uninit(cmd)
1269+
return None
1270+
1271+
12361272
def _remember_title(text: str) -> str:
12371273
"""Derive a note title from free-form text. First non-empty line, ≤80 chars."""
12381274
for line in text.splitlines():
@@ -1255,8 +1291,8 @@ def _bm_search(raw_args: str) -> str:
12551291
args = raw_args.strip()
12561292
if not args or _is_help_arg(args):
12571293
return _SLASH_USAGE["bm-search"]
1258-
if not provider._initialized or provider._actor is None:
1259-
return _slash_uninit("bm-search")
1294+
if err := _ensure_slash_ready(provider, "bm-search"):
1295+
return err
12601296
try:
12611297
raw = provider._actor.call(
12621298
"search_notes",
@@ -1282,8 +1318,8 @@ def _bm_read(raw_args: str) -> str:
12821318
args = raw_args.strip()
12831319
if not args or _is_help_arg(args):
12841320
return _SLASH_USAGE["bm-read"]
1285-
if not provider._initialized or provider._actor is None:
1286-
return _slash_uninit("bm-read")
1321+
if err := _ensure_slash_ready(provider, "bm-read"):
1322+
return err
12871323
try:
12881324
raw = provider._actor.call(
12891325
"read_note",
@@ -1301,8 +1337,8 @@ def _bm_context(raw_args: str) -> str:
13011337
args = raw_args.strip()
13021338
if not args or _is_help_arg(args):
13031339
return _SLASH_USAGE["bm-context"]
1304-
if not provider._initialized or provider._actor is None:
1305-
return _slash_uninit("bm-context")
1340+
if err := _ensure_slash_ready(provider, "bm-context"):
1341+
return err
13061342
try:
13071343
raw = provider._actor.call(
13081344
"build_context",
@@ -1321,8 +1357,8 @@ def _bm_recent(raw_args: str) -> str:
13211357
if _is_help_arg(args):
13221358
return _SLASH_USAGE["bm-recent"]
13231359
timeframe = args or "7d"
1324-
if not provider._initialized or provider._actor is None:
1325-
return _slash_uninit("bm-recent")
1360+
if err := _ensure_slash_ready(provider, "bm-recent"):
1361+
return err
13261362
try:
13271363
raw = provider._actor.call(
13281364
"recent_activity",
@@ -1385,8 +1421,8 @@ def _bm_remember(raw_args: str) -> str:
13851421
text = raw_args.strip()
13861422
if not text or _is_help_arg(text):
13871423
return _SLASH_USAGE["bm-remember"]
1388-
if not provider._initialized or provider._actor is None:
1389-
return _slash_uninit("bm-remember")
1424+
if err := _ensure_slash_ready(provider, "bm-remember"):
1425+
return err
13901426
title = _remember_title(text)
13911427
folder = provider._remember_folder or "bm-remember"
13921428
try:
@@ -1410,8 +1446,8 @@ def _bm_remember(raw_args: str) -> str:
14101446
def _bm_project(raw_args: str) -> str:
14111447
if _is_help_arg(raw_args):
14121448
return _SLASH_USAGE["bm-project"]
1413-
if not provider._initialized or provider._actor is None:
1414-
return _slash_uninit("bm-project")
1449+
if err := _ensure_slash_ready(provider, "bm-project"):
1450+
return err
14151451
try:
14161452
raw = provider._actor.call(
14171453
"list_memory_projects",
@@ -1452,8 +1488,8 @@ def _bm_workspace(raw_args: str) -> str:
14521488
"Workspaces are a Basic Memory Cloud concept. "
14531489
f"This plugin is in '{provider._mode}' mode — no workspaces to list."
14541490
)
1455-
if not provider._initialized or provider._actor is None:
1456-
return _slash_uninit("bm-workspace")
1491+
if err := _ensure_slash_ready(provider, "bm-workspace"):
1492+
return err
14571493
try:
14581494
raw = provider._actor.call(
14591495
"list_workspaces",

tests/test_commands.py

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -353,14 +353,34 @@ def test_usage_returned_for_empty_or_help_args(bm, name, args):
353353
("bm-remember", "a thought"),
354354
("bm-project", ""),
355355
])
356-
def test_handler_uninitialized_returns_message(bm, name, args):
356+
def test_handler_init_failure_returns_message(bm, monkeypatch, name, args):
357357
provider = bm.BasicMemoryProvider()
358+
monkeypatch.setattr(provider, "initialize", lambda *a, **kw: None)
358359
handlers = _handlers_by_name(bm, provider)
359360
out = handlers[name](args)
360361
assert "not initialized" in out
361362
assert name in out # message includes command name
362363

363364

365+
def test_handler_lazily_initializes_provider_for_slash_command(bm, monkeypatch):
366+
provider = bm.BasicMemoryProvider()
367+
calls = []
368+
369+
def fake_initialize(*args, **kwargs):
370+
calls.append((args, kwargs))
371+
session = FakeSession(default_response={"results": []})
372+
actor = make_scripted_actor(bm, session=session)
373+
actor.start()
374+
provider._actor = actor
375+
provider._initialized = True
376+
377+
monkeypatch.setattr(provider, "initialize", fake_initialize)
378+
out = _handlers_by_name(bm, provider)["bm-search"]("widgets")
379+
assert "No results" in out
380+
assert len(calls) == 1
381+
assert calls[0][1]["session_id"].startswith("slash:bm-search:")
382+
383+
364384
# ---- /bm-status ----
365385

366386
def test_bm_status_renders_provider_state(bm, monkeypatch):

0 commit comments

Comments
 (0)