Skip to content

Commit 4a7a0f8

Browse files
committed
Fix v0.1.6: get_tool_schemas() must be unconditional
Hermes's MemoryManager captures the schema list at register-time, before the provider's initialize() runs. Our get_tool_schemas() was gated on self._initialized, returning [] until init completed. By that point Hermes had already built _tool_to_provider with zero entries for us, and every bm_* invocation for the rest of the session returned 'Unknown tool: bm_search' from MemoryManager dispatch. The asymmetry was the giveaway: prefetch (called per-turn, after init) worked, but tool calls (registered once at start) didn't. Felix reported recall injection working alongside 'Unknown tool: bm_search' on the same conversation. Schemas are static — return them unconditionally. handle_tool_call() already gates on _initialized + actor presence and returns a clean tool_error if the actor isn't ready, so user-visible failure modes stay clean. Regression test: test_get_tool_schemas_unconditional asserts a fresh uninitialized provider exposes all 7 schemas. Locks the bug shut.
1 parent c35af32 commit 4a7a0f8

4 files changed

Lines changed: 36 additions & 10 deletions

File tree

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,12 @@ All notable changes to this project will be documented in this file.
44

55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
66

7+
## [0.1.6] — 2026-05-10
8+
9+
### Fixed
10+
- **`bm_*` tools were never registered with Hermes's `MemoryManager._tool_to_provider`.** `get_tool_schemas()` was gated on `self._initialized`, but Hermes captures the schema list at *register* time — before `initialize()` runs. The gate caused every session to start with zero tools registered for our provider, so every LLM-issued `bm_search` (and friends) returned `"Unknown tool: bm_search"` from MemoryManager's dispatch. Symptoms were asymmetric: prefetch (recall injection) worked because it's invoked per-turn after init, but tool calls didn't. Schemas are static — they now return unconditionally, with `handle_tool_call()` doing the runtime "is the actor ready?" gate.
11+
- Regression test pins this so we don't reintroduce it: `test_get_tool_schemas_unconditional` asserts `get_tool_schemas()` returns all 7 schemas on a fresh, uninitialized provider.
12+
713
## [0.1.5] — 2026-05-10
814

915
### Added
@@ -63,6 +69,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6369
- Single-file plugin at `__init__.py`, AGPL-3.0-or-later.
6470
- 84-test pytest suite.
6571

72+
[0.1.6]: https://github.com/basicmachines-co/hermes-basic-memory/releases/tag/v0.1.6
6673
[0.1.5]: https://github.com/basicmachines-co/hermes-basic-memory/releases/tag/v0.1.5
6774
[0.1.4]: https://github.com/basicmachines-co/hermes-basic-memory/releases/tag/v0.1.4
6875
[0.1.3]: https://github.com/basicmachines-co/hermes-basic-memory/releases/tag/v0.1.3

__init__.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@
3838
from agent.memory_provider import MemoryProvider
3939
from tools.registry import tool_error
4040

41-
__version__ = "0.1.5"
41+
__version__ = "0.1.6"
4242

4343
logger = logging.getLogger("hermes.memory.basic-memory")
4444

@@ -791,8 +791,12 @@ def system_prompt_block(self) -> str:
791791
)
792792

793793
def get_tool_schemas(self) -> List[Dict[str, Any]]:
794-
if not self._initialized:
795-
return []
794+
# Static. Hermes captures the schema list at register time (before
795+
# initialize() has run), so gating on `_initialized` would mean the
796+
# tools never make it into MemoryManager._tool_to_provider, and every
797+
# `bm_*` invocation returns "Unknown tool: bm_*" for the rest of the
798+
# session. handle_tool_call() does the runtime gate on `_initialized`
799+
# and returns a clean tool error if the actor isn't ready yet.
796800
return [dict(s) for s in TOOL_SCHEMAS]
797801

798802
def handle_tool_call(self, tool_name: str, args: Dict[str, Any], **kwargs: Any) -> str:

plugin.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
name: basic-memory
2-
version: 0.1.5
2+
version: 0.1.6
33
description: "Basic Memory — persistent knowledge graph backed by the basic-memory MCP server"
44
pip_dependencies:
55
- mcp

tests/test_provider.py

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -44,17 +44,32 @@ def test_name(bm):
4444
assert bm.BasicMemoryProvider().name == bm.PROVIDER_NAME
4545

4646

47-
def test_get_tool_schemas_uninitialized(bm):
47+
def test_get_tool_schemas_unconditional(bm):
48+
"""
49+
Regression: Hermes captures the schema list at *register* time, before
50+
`initialize()` runs. If get_tool_schemas() returns [] when uninitialized,
51+
Hermes builds _tool_to_provider with no entries for us and every
52+
subsequent bm_* invocation returns "Unknown tool: bm_*" forever.
53+
Schemas are static — return them unconditionally.
54+
"""
55+
# Fresh provider, never initialized, should still expose all 7 schemas
4856
p = bm.BasicMemoryProvider()
49-
assert p.get_tool_schemas() == []
57+
assert p._initialized is False
58+
schemas = p.get_tool_schemas()
59+
assert len(schemas) == 7
60+
names = {s["name"] for s in schemas}
61+
assert names == {"bm_search", "bm_read", "bm_write", "bm_edit",
62+
"bm_context", "bm_delete", "bm_move"}
63+
64+
# Initialized provider also returns 7 (idempotent)
65+
p._initialized = True
66+
assert len(p.get_tool_schemas()) == 7
5067

5168

52-
def test_get_tool_schemas_initialized(bm):
69+
def test_get_tool_schemas_returns_independent_copies(bm):
70+
"""Mutating the returned list shouldn't affect the next call."""
5371
p = bm.BasicMemoryProvider()
54-
p._initialized = True
5572
schemas = p.get_tool_schemas()
56-
assert len(schemas) == 7
57-
# Returned list is a copy (mutating shouldn't affect class state)
5873
schemas.clear()
5974
assert len(p.get_tool_schemas()) == 7
6075

0 commit comments

Comments
 (0)