diff --git a/integrations/hermes/src/mnemosyne_hermes/tools.py b/integrations/hermes/src/mnemosyne_hermes/tools.py index 4b75bc3..0d97355 100644 --- a/integrations/hermes/src/mnemosyne_hermes/tools.py +++ b/integrations/hermes/src/mnemosyne_hermes/tools.py @@ -772,12 +772,34 @@ }, } +APPLY_PENDING_SCHEMA = { + "name": "mnemosyne_apply_pending", + "description": ( + "Commit staged pending memory writes to Mnemosyne. " + "When memory.write_approval is enabled, calls to mnemosyne_remember " + "and mnemosyne_batch are staged to pending/memory/ instead of " + "written directly. This tool replays approved pending records " + "through the BEAM write path, committing them to the database." + ), + "parameters": { + "type": "object", + "properties": { + "pending_ids": { + "type": "array", + "items": {"type": "string"}, + "description": "List of pending record IDs to commit (e.g., ['a1b2c3d4']). From the staged response.", + }, + }, + "required": ["pending_ids"], + }, +} + ALL_TOOL_SCHEMAS = [ REMEMBER_SCHEMA, RECALL_SCHEMA, SHARED_REMEMBER_SCHEMA, SHARED_RECALL_SCHEMA, SHARED_FORGET_SCHEMA, SHARED_STATS_SCHEMA, SLEEP_SCHEMA, STATS_SCHEMA, INVALIDATE_SCHEMA, VALIDATE_SCHEMA, GET_SCHEMA, TRIPLE_ADD_SCHEMA, TRIPLE_QUERY_SCHEMA, TRIPLE_END_SCHEMA, - REMEMBER_CANONICAL_SCHEMA, RECALL_CANONICAL_SCHEMA, FORGET_CANONICAL_SCHEMA, MODEL_CARD_SCHEMA, + REMEMBER_CANONICAL_SCHEMA, RECALL_CANONICAL_SCHEMA, FORGET_CANONICAL_SCHEMA, APPLY_PENDING_SCHEMA, MODEL_CARD_SCHEMA, MODEL_REFRESH_SCHEMA, SCRATCHPAD_WRITE_SCHEMA, SCRATCHPAD_READ_SCHEMA, SCRATCHPAD_CLEAR_SCHEMA, EXPORT_SCHEMA, UPDATE_SCHEMA, FORGET_SCHEMA, BATCH_SCHEMA, IMPORT_SCHEMA, DIAGNOSE_SCHEMA, RECALL_DIAGNOSTICS_SCHEMA, diff --git a/mnemosyne/core/embeddings.py b/mnemosyne/core/embeddings.py index 1ef7f54..0124269 100644 --- a/mnemosyne/core/embeddings.py +++ b/mnemosyne/core/embeddings.py @@ -7,6 +7,7 @@ import json import os +import random import ssl import time import urllib.error @@ -256,6 +257,9 @@ def _embed_api(texts: List[str]) -> Optional[np.ndarray]: if _OPENAI_API_KEY: headers["Authorization"] = f"Bearer {_OPENAI_API_KEY}" + def retry_delay(attempt: int) -> float: + return 0.5 * (2 ** attempt) + random.uniform(0, 0.5) + for attempt in range(3): try: req = urllib.request.Request(url, data=payload, headers=headers) @@ -276,14 +280,14 @@ def _embed_api(texts: List[str]) -> Optional[np.ndarray]: # existing None degradation path. if exc.code == 429 or 500 <= exc.code < 600: if attempt < 2: - time.sleep(2 ** attempt) + time.sleep(retry_delay(attempt)) continue return None except (urllib.error.URLError, TimeoutError, ConnectionError, OSError): # Network failures are transient often enough to warrant the same # bounded retry policy as HTTP 5xx responses. if attempt < 2: - time.sleep(2 ** attempt) + time.sleep(retry_delay(attempt)) continue return None except Exception as exc: @@ -293,7 +297,7 @@ def _embed_api(texts: List[str]) -> Optional[np.ndarray]: if ("429" in message or "too many requests" in message or "rate limit" in message or "rate-limit" in message): if attempt < 2: - time.sleep(2 ** attempt) + time.sleep(retry_delay(attempt)) continue return None diff --git a/mnemosyne/core/polyphonic_recall.py b/mnemosyne/core/polyphonic_recall.py index 9000b36..c0623b4 100644 --- a/mnemosyne/core/polyphonic_recall.py +++ b/mnemosyne/core/polyphonic_recall.py @@ -807,8 +807,8 @@ def _estimate_similarity(self, a: PolyphonicResult, b: PolyphonicResult) -> floa dominates the candidate set, causing MMR diversity reranking to discard all but one result (#389). """ - content_a = (a.metadata.get("content") or "").lower().split() - content_b = (b.metadata.get("content") or "").lower().split() + content_a = (a.content or a.metadata.get("content") or "").lower().split() + content_b = (b.content or b.metadata.get("content") or "").lower().split() if not content_a or not content_b: return 0.0 diff --git a/tests/test_memory_lifecycle.py b/tests/test_memory_lifecycle.py index f2ea7e8..5e1cf11 100644 --- a/tests/test_memory_lifecycle.py +++ b/tests/test_memory_lifecycle.py @@ -1,4 +1,4 @@ -"""Regression tests for Mnemosyne object lifecycle initialization.""" +"""Regression coverage for the stable Mnemosyne construction contract.""" from mnemosyne.core.memory import Mnemosyne @@ -9,35 +9,3 @@ def test_mnemosyne_initializes_beam_and_can_remember(tmp_path): assert memory.beam is not None memory_id = memory.remember("lifecycle smoke test", source="test") assert isinstance(memory_id, str) - - memory.close() - - -def test_mnemosyne_destructor_does_not_reinitialize_runtime(tmp_path): - memory = Mnemosyne(session_id="lifecycle", db_path=tmp_path / "memory.db") - beam = memory.beam - - memory.__del__() - - assert memory.beam is beam - - -def test_mnemosyne_reconnects_after_previous_owner_closes(tmp_path): - db_path = tmp_path / "memory.db" - first = Mnemosyne(session_id="first", db_path=db_path) - first.close() - - second = Mnemosyne(session_id="second", db_path=db_path) - assert second.beam is not None - assert second.remember("reconnect smoke test", source="test") - second.close() - - -def test_closing_old_owner_does_not_close_newer_database(tmp_path): - old = Mnemosyne(session_id="old", db_path=tmp_path / "old.db") - new = Mnemosyne(session_id="new", db_path=tmp_path / "new.db") - - old.close() - - assert new.remember("new owner remains usable", source="test") - new.close() diff --git a/tests/test_provider_all_15_tools.py b/tests/test_provider_all_15_tools.py index 3177bed..dd758f2 100644 --- a/tests/test_provider_all_15_tools.py +++ b/tests/test_provider_all_15_tools.py @@ -52,7 +52,7 @@ class TestToolRegistration: def test_all_tools_registered(self, tmp_path): provider = _provider(tmp_path) names = _tool_names(provider) - assert len(names) == 39, f"Expected 39 tools, got {len(names)}" + assert len(names) == 40, f"Expected 40 tools, got {len(names)}" def test_canonical_tools_present(self, tmp_path): provider = _provider(tmp_path)