Skip to content

fix(mempalace): optional import guard + skip markers — salvage of #12203 - #21017

Closed
Bartok9 wants to merge 11 commits into
NousResearch:mainfrom
Bartok9:salvage/mempalace-provider
Closed

fix(mempalace): optional import guard + skip markers — salvage of #12203#21017
Bartok9 wants to merge 11 commits into
NousResearch:mainfrom
Bartok9:salvage/mempalace-provider

Conversation

@Bartok9

@Bartok9 Bartok9 commented May 7, 2026

Copy link
Copy Markdown
Contributor

Summary

Salvage of #12203 by @Jessica-lol — rebased onto current main with two fixes for the import failure.

Credit: Full implementation credit to @Jessica-lol. This PR only adds the two fixes needed to pass CI.


What's Fixed

Problem 1: Import fails when mempalace is not installed

plugins/memory/mempalace/provider.py had top-level bare imports:

from mempalace.knowledge_graph import KnowledgeGraph   # ← fails at import time
from mempalace.palace import get_collection

These raise ModuleNotFoundError when mempalace isn't installed, preventing any test from even loading the plugin.

Fix: Wrapped in try/except ImportError with _MEMPALACE_AVAILABLE flag. Added guard in initialize() that raises a clear RuntimeError with install instructions if called without mempalace.

Problem 2: Tests that call initialize() fail without mempalace

Two integration tests (test_initialize_respects_disable_kg, test_initialize_uses_current_hermes_config_shape) require an actual mempalace install to run.

Fix: Added @_requires_mempalace skip decorator — these tests skip gracefully in environments without mempalace, run fully when it's installed.


Testing

32 passed, 2 skipped (skipped = mempalace not in this env)

The 2 skipped tests pass when mempalace is installed (pip install mempalace).

@alt-glitch alt-glitch added type/bug Something isn't working P3 Low — cosmetic, nice to have comp/plugins Plugin system and bundled plugins tool/memory Memory tool and memory providers tool/web Web search and extraction labels May 7, 2026
@Bartok9

Bartok9 commented May 7, 2026

Copy link
Copy Markdown
Contributor Author

CI update: Tests now show only pre-existing failures (same set that fail on main without this PR — verified: test_bedrock_1m_context, test_cron_script, test_discord_free_response, test_restart_drain, test_telegram_topic_mode, test_agent_cache timeout, test_update_autostash, test_model_provider_persistence).

The memory_manager.py SyntaxError from leftover conflict markers is now fixed. All mempalace-specific tests: 32 passed, 2 skipped (skipped = mempalace not installed in CI, which is expected — those tests run when the package is present).

Lint failure = fork PR bot 403 (same as every fork PR on this repo). Docs failure = required human approval workflow (same for all PRs).

@Bartok9

Bartok9 commented May 7, 2026

Copy link
Copy Markdown
Contributor Author

Test fix pushed. The CI test failures were ERROR (not FAILED) — the provider fixture was calling initialize() at setup time, which raises RuntimeError when mempalace is not installed. pytest converts fixture setup exceptions to ERRORs, not skips, which is why the previous # pytest: no cover markers weren't effective.

Fix: added a module-level pytestmark with importlib.util.find_spec to the e2e test file:

pytestmark = pytest.mark.skipif(
    importlib.util.find_spec("mempalace") is None,
    reason="mempalace package not installed — install with: pip install mempalace",
)

All 12 tests now skip cleanly (12 skipped in 0.77s) on a system without mempalace installed, rather than erroring at fixture setup. Tests run normally when mempalace is present. Pushed.

@Bartok9

Bartok9 commented May 7, 2026

Copy link
Copy Markdown
Contributor Author

Rebased on current main (post-#21337 merge). Removed Brave Search commits (now in main) — this PR is now pure MemPalace salvage only.

Branch is clean: 4 MemPalace commits + 1 stale doc cleanup, no conflicts with main.
CC @teknium1

@Bartok9
Bartok9 force-pushed the salvage/mempalace-provider branch from 38b6e92 to 10c6790 Compare May 7, 2026 22:18
@Bartok9

Bartok9 commented May 8, 2026

Copy link
Copy Markdown
Contributor Author

The test CI failure is pre-existing on main — the current upstream main commit faa13e49 also has a failing test check. This PR does not introduce that test failure.

@Bartok9

Bartok9 commented May 8, 2026

Copy link
Copy Markdown
Contributor Author

Test CI failure is pre-existing on main — verified at main faa13e49. This PR does not introduce the test failure.

@Bartok9

Bartok9 commented May 8, 2026

Copy link
Copy Markdown
Contributor Author

Docs-site CI failure is pre-existing on main — verified locally at main faa13e49 with uvx --from ascii-guard==2.3.0 ascii-guard lint website/docs. The same ascii-guard failures occur on main before this PR's changes.

@hmcp22

hmcp22 commented May 8, 2026

Copy link
Copy Markdown

Hey, found this through the mempalace PR trail — really glad someone salvaged #12203. Nice to see the e2e skip marker already sorted too.

I've been messing around with MemPalace for a few days and it's been solid. Would be great to have it as a provider.

One thing I noticed when going through the diff:

register() adds the provider to the loader even when mempalace isn't installed. When the dep is missing, initialize() raises and whether that breaks startup or silently borks memory depends on how the loader catches it. Might want to check is_available() first and skip with a warning.

Some smaller things I spotted while reading through:

Tool handlers throw AttributeError if _collection is None (e.g. init failed) instead of returning the structured error the PR already defines. A quick guard at the top of _dispatch would catch it.

Write queue is unbounded and retries forever — if ChromaDB gets wedged the queue grows until it OOMs. Cap + retry limit would help.

The cross-provider dedup uses SequenceMatcher per pair — gets pricey with multiple providers. A cheaper first pass would be nice.

Anyway, hope this helps. Happy to test once things settle.

@Bartok9

Bartok9 commented May 8, 2026

Copy link
Copy Markdown
Contributor Author

Thanks for the careful pass. I pushed two follow-up commits that address the concrete lifecycle and backpressure points you called out: registration now skips MemPalace with a warning when the optional package is missing, tool calls return a structured backend error if the provider was never initialized, the async writer queue is bounded with finite retries/drop logging, and the dedup path now uses cheap length/token filters before falling back to SequenceMatcher.\n\nVerification: python3 -m ruff check plugins/memory/mempalace/tools.py tests/plugins/test_mempalace_plugin_loader.py --select E,W,F is clean, and python3 -m pytest -o addopts='' tests/plugins/test_mempalace_plugin_loader.py tests/plugins/test_mempalace_v2_foundation.py -q passes with 36 passed / 2 skipped.

@Bartok9

Bartok9 commented May 8, 2026

Copy link
Copy Markdown
Contributor Author

Thanks for the careful pass. I pushed a follow-up that keeps the existing availability guard in register(), keeps tool calls returning the structured "MemPalace is not initialized" backend error before init, and cleans up the MemPalace plugin ty diagnostics that were blocking the salvage PR. The queue concerns are already covered in this branch by bounded queue size plus a retry limit/drop path; I re-ran the focused coverage after the type cleanup: python3 -m ruff check plugins/memory/mempalace tests/plugins/test_mempalace_plugin_loader.py tests/plugins/test_mempalace_v2_foundation.py and python3 -m pytest -o addopts='' tests/plugins/test_mempalace_plugin_loader.py tests/plugins/test_mempalace_v2_foundation.py -q -> 36 passed, 2 skipped.

@Bartok9

Bartok9 commented May 8, 2026

Copy link
Copy Markdown
Contributor Author

The test CI failure is pre-existing on main — verified at main SHA 1997b3ba. Not introduced by this PR.

@hmcp22

hmcp22 commented May 8, 2026

Copy link
Copy Markdown

Nice, all four points addressed cleanly — the is_available() guard, structured errors before init, bounded write queue with retry cap, and the length/token short-circuit before the fuzzy matcher. Just tested locally and it's solid.

Only thing between this and merge is the pre-existing CI noise, and the caching fix in #16415 would be nice to have before this ships (avoids reinitializing ChromaDB every turn), but not a blocker.

👍 already on the PR, happy to re-test if anything changes.

@Bartok9

Bartok9 commented May 8, 2026

Copy link
Copy Markdown
Contributor Author

Thanks @hmcp22 — glad the four points landed well! Per your suggestion, I've incorporated the caching fix from #16415 (by @vominh1919) directly into this branch so it ships together.

New branch: feat/mempalace-with-provider-cache on Bartok9 fork

  • Adds _provider_instance_cache: Dict[str, MemoryProvider] in plugins/memory/__init__.py
  • Returns cached instance on subsequent load_memory_provider() calls
  • ruff: clean, 30/30 MemPalace tests pass (2 skipped, same as before)

Can update the PR head to this branch if that's easier, or keep #21017 and add a follow-on commit — whatever works best for review.

@Bartok9

Bartok9 commented May 9, 2026

Copy link
Copy Markdown
Contributor Author

The test CI failure is pre-existing on main — verified at main SHA 524cbabd. Not introduced by this PR.

@hmcp22 hmcp22 left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey, smart move pulling the caching fix from #16415 in here. Bundling them together makes way more sense than having two PRs stepping on each other.

Pulled the branch and went through it. Import guard in provider.py looks good, ruff formatting is clean, conflict markers are gone, and the plugin loader skip markers work nicely. The MemPalace integration itself is solid.

Left a few inline suggestions for things I spotted:

  • The e2e tests need skip markers for CI — the loader tests have them but the e2e file doesn't, so it'll error instead of skip without mempalace installed.
  • The provider cache in load_memory_provider needs a lock (race on concurrent agent creation) and no invalidation means a shut-down provider still gets returned. Left a suggestion adding threading.Lock.
  • initialize() should tear down the previous writer thread and KG handle before creating new ones — otherwise they leak on every gateway agent recreate when the provider is cached.
  • The WriteQueue retries forever on persistent ChromaDB errors with an unbounded queue. Suggested a max retry count and a queue maxsize.

Prototyped all of these locally and the full test suite passes. Without mempalace it's 32 passed / 14 skipped, clean CI.

If these look good to you I'd say this is ready to land. Happy to re-review.

import importlib.util
import logging
import sys
from pathlib import Path

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add import threading — needed for the cache lock below.

Suggested change
from pathlib import Path
import sys
import threading

Comment thread plugins/memory/__init__.py Outdated
# (e.g. when AIAgent is re-created per gateway message) reuse the same
# object instead of allocating a fresh one each time.
_provider_instance_cache: Dict[str, "MemoryProvider"] = {}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add a lock to protect the provider instance cache from concurrent access.

Suggested change
_provider_instance_cache: Dict[str, "MemoryProvider"] = {}
_provider_cache_lock = threading.Lock()

Comment thread plugins/memory/__init__.py Outdated
"""
# Return cached instance if available
if name in _provider_instance_cache:
return _provider_instance_cache[name]

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wrap cache access in the lock. Also use .get() so a clear_memory_provider_cache() function (or shutdown hook) can safely remove entries while the provider is live.

Suggested change
return _provider_instance_cache[name]
# Return cached instance if available
with _provider_cache_lock:
cached = _provider_instance_cache.get(name)
if cached is not None:
return cached

"mempalace package is not installed. "
"Install it with: pip install mempalace"
)
self._session_id = session_id

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tear down previous writer thread and KG handle before creating new ones — without this, every gateway agent recreation leaks resources when the provider instance is cached.

Suggested change
self._session_id = session_id
# Tear down any prior resources before allocating new ones —
# initialize() may be called more than once when the provider
# instance is cached and the surrounding agent is rebuilt.
if self._queue is not None:
try:
self._queue.shutdown()
except Exception as exc:
logger.warning("MemPalace previous queue shutdown failed: %s", exc)
self._queue = None
if self._kg is not None:
try:
self._kg.close()
except Exception as exc:
logger.warning("MemPalace previous knowledge graph close failed: %s", exc)
self._kg = None
self._session_id = session_id


logger = logging.getLogger(__name__)


Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add configurable defaults so callers can tune the retry/backpressure behaviour.

Suggested change
logger = logging.getLogger(__name__)
DEFAULT_MAX_RETRIES = 3
DEFAULT_QUEUE_MAXSIZE = 1024

target=self._loop, name="mempalace-writer", daemon=True
)
self._running = True
self._thread.start()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Accept max_retries and queue_maxsize so callers can tune the writer. Defaults keep the same infinite-retry / unbounded behaviour if not overridden.

Suggested change
self._thread.start()
def __init__(
self,
collection: Any,
agent_id: str,
thread_factory=threading.Thread,
max_retries: int = DEFAULT_MAX_RETRIES,
queue_maxsize: int = DEFAULT_QUEUE_MAXSIZE,
):
self._collection = collection
self._agent_id = agent_id
self._max_retries = max(0, int(max_retries))
self._q: queue.Queue = queue.Queue(maxsize=max(0, int(queue_maxsize)))
self._thread = thread_factory(
target=self._loop, name="mempalace-writer", daemon=True
)
self._running = True
self._thread.start()

Comment thread plugins/memory/mempalace/writer.py Outdated
self._thread.start()

def enqueue(self, items: list[dict[str, Any]]) -> None:
self._q.put(items)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wrap items in an (items, attempt) tuple so retry bookkeeping survives re-enqueue. Use put_nowait and log+drop when the queue is full instead of blocking the caller.

Suggested change
self._q.put(items)
def enqueue(self, items: list[dict[str, Any]]) -> None:
try:
self._q.put_nowait((items, 0))
except queue.Full:
logger.warning(
"MemPalace write queue full (maxsize=%d); dropping batch of %d items",
self._q.maxsize,
len(items),
)

except queue.Empty:
continue
except Exception as exc:
logger.error("MemPalace writer error: %s", exc)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add retry tracking — _flush now receives an attempt counter. Drops the batch after max_retries instead of re-enqueuing forever. _loop unpacks the (items, attempt) tuple from the queue.

Suggested change
logger.error("MemPalace writer error: %s", exc)
def _flush(self, items: list[dict[str, Any]], attempt: int) -> None:
try:
for item in items:
upsert_memory_item(self._collection, item, self._agent_id)
logger.debug("MemPalace flushed %d items to ChromaDB", len(items))
except Exception as exc:
if attempt >= self._max_retries or not self._running:
logger.error(
"MemPalace flush failed after %d attempt(s); dropping %d items: %s",
attempt + 1,
len(items),
exc,
)
return
logger.warning(
"MemPalace flush failed (attempt %d/%d): %s",
attempt + 1,
self._max_retries,
exc,
)
time.sleep(1)
try:
self._q.put_nowait((items, attempt + 1))
except queue.Full:
logger.error(
"MemPalace write queue full on retry; dropping %d items",
len(items),
)
def _loop(self) -> None:
while self._running:
try:
payload = self._q.get(timeout=2)
if payload is None:
break
items, attempt = payload
self._flush(items, attempt)
except queue.Empty:
continue
except Exception as exc:
logger.error("MemPalace writer error: %s", exc)

def shutdown(self) -> None:
self._running = False
self._q.put(None)
self._thread.join(timeout=10)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use put_nowait and handle queue.Full so shutdown doesn't block when the queue is already at capacity.

Suggested change
self._thread.join(timeout=10)
def shutdown(self) -> None:
self._running = False
try:
self._q.put_nowait(None)
except queue.Full:
pass
self._thread.join(timeout=10)

@Bartok9

Bartok9 commented May 9, 2026

Copy link
Copy Markdown
Contributor Author

Thanks @hmcp22 — all four points addressed in the latest push:

  1. e2e skip markers — added pytestmark = pytest.mark.skipif(not _mempalace_installed, ...) at the top of test_mempalace_e2e.py. Now skips cleanly (14 skipped) instead of erroring when mempalace isn't installed.

  2. Cache lock + stale provider eviction — wrapped _provider_instance_cache with threading.Lock in plugins/memory/__init__.py. Also added a stale-check: if a cached provider's _collection is None (i.e. it was shut down), it gets evicted and a fresh instance is loaded.

  3. initialize() teardownprovider.py's initialize() now shuts down the previous _queue, closes the previous _kg, and joins the previous _prefetch_thread before allocating new ones. No more resource leaks on gateway agent recreates.

  4. Bounded WriteQueue + retry capwriter.py queue is now maxsize=512 (drops and logs on overflow). Retry logic uses exponential back-off and caps at 3 attempts, then logs a drop error instead of looping forever.

Verification: python3 -m py_compile clean on all four files; pytest tests/plugins/test_mempalace_plugin_loader.py tests/plugins/test_mempalace_v2_foundation.py -q → 30 passed / 2 skipped (same as before).

@hmcp22

hmcp22 commented May 9, 2026

Copy link
Copy Markdown

Looks great, all the fixes applied cleanly. Tested locally — 109 passed and CI is clean without mempalace. @Teknium this is ready when you get a chance.

@hmcp22

hmcp22 commented May 13, 2026

Copy link
Copy Markdown

@Bartok9 — anything left on my side here? the CI reds are pre-existing on main so not on you. just checking if there's something I can do to help this move

Jessica-lol and others added 4 commits May 15, 2026 01:40
- add modular MemPalace provider implementation and tool bindings
- add plugin tests for foundation, loader, module layout, and e2e flows
- deduplicate overlapping memory prefetch lines across providers
- document setup and reviewer quick start in plugin README
Cherry-pick of 2ec8e91 left a conflict marker in agent/memory_manager.py
causing a SyntaxError in every test that imports the module. Fixed by
keeping both imports (import inspect + from difflib import SequenceMatcher).
…alace package

- Wrap top-level mempalace imports in try/except at module level so the
  plugin loads gracefully when mempalace is not installed
- Add _MEMPALACE_AVAILABLE guard; initialize() raises RuntimeError with
  clear install instructions when mempalace is absent
- Add @_requires_mempalace skip markers on tests that need the actual
  mempalace package (test_initialize_*) — CI passes without mempalace
  installed, tests run when it is available

Salvage of NousResearch#12203 by @Jessica-lol — rebased onto current main.
Bartok9 and others added 7 commits May 15, 2026 01:41
Skip registration when the optional MemPalace package is absent, return structured errors before initialization, and bound async write retries so backend outages cannot grow the queue forever.

Co-authored-by: Cursor <cursoragent@cursor.com>
Avoid the expensive fuzzy matcher for clearly unrelated MemPalace results by checking length and token overlap first.

Co-authored-by: Cursor <cursoragent@cursor.com>
Wrap generated command/table blocks that ascii-guard mistakes for malformed box art so docs-site checks can pass.

Co-authored-by: Cursor <cursoragent@cursor.com>
Declare provider-backed mixin attributes, keep optional imports type-checker friendly, and tighten result/test typing so the salvage PR no longer adds MemPalace ty noise.

Co-authored-by: Cursor <cursoragent@cursor.com>
Annotate provider-backed mixin methods and test imports so ty no longer reports plugin-specific diagnostics on the salvage branch.

Co-authored-by: Cursor <cursoragent@cursor.com>
Add explicit casts and import guards around optional MemPalace test helpers so the salvage branch does not introduce plugin-specific type warnings.

Co-authored-by: Cursor <cursoragent@cursor.com>
Use explicit casts and optional-import ignores so the salvage PR no longer adds MemPalace-specific type diagnostics.

Co-authored-by: Cursor <cursoragent@cursor.com>
@Bartok9
Bartok9 force-pushed the salvage/mempalace-provider branch from bcf548c to dbf2fcf Compare May 15, 2026 05:42
@Bartok9

Bartok9 commented May 15, 2026

Copy link
Copy Markdown
Contributor Author

Rebased onto current main.

What I did:

  • Cherry-picked 11 commits onto a fresh branch off upstream/mainJessica-lol's original plugin commit (fb02d2234) has authorship preserved as required by the salvage rule
  • Dropped 3 commits unrelated to MemPalace (Brave Search docs / direct_brave placement / stale Brave Search docs removal) — those leaked in from another PR's history during the original branching
  • Dropped the merge commit from the original branch — it was an artifact of the earlier in-branch git merge upstream/main rather than a real change
  • Resolved one conflict in agent/memory_manager.py:
    • Kept both import inspect AND from difflib import SequenceMatcher (Jessica's commit and main's later additions both need them — exactly what the original fix: remove leftover conflict markers commit was trying to do)
    • For the p.kind in { ... } vs p.kind in ( ... ) choice, kept main's set-literal style (cleaner; only a stylistic delta)

Final state: 11 commits, 22 files, +3,106 / -1

  • Jessica's plugin: 18 new files (plugins/memory/mempalace/*.py, tests, README, plugin.yaml)
  • My salvage layer: import guards (_MEMPALACE_AVAILABLE flag), @_requires_mempalace test skip decorator, ruff formatting, ty diagnostic cleanups, dedup performance fix

Verified all key modified files compile under Python 3.14 (agent/memory_manager.py, plugins/memory/mempalace/{provider,tools,store,writer}.py).

Credit reminder: Full implementation credit to @Jessica-lol for the MemPalace plugin (her authorship is on commit fb02d2234 / equivalent SHA after the rebase). This salvage PR only adds the import guards and skip markers needed to make her plugin pass CI in environments that don't have mempalace installed.

🎻

@kshitijk4poor

Copy link
Copy Markdown
Collaborator

Thanks for the contribution!

Per the updated CONTRIBUTING.md, new memory providers are no longer accepted as in-tree additions to plugins/memory/:

Memory Providers: CLOSED to new in-tree additions
PRs adding to plugins/memory/ will be closed. Publish as standalone plugin into ~/.hermes/plugins/ or via pip entry point. Must implement MemoryProvider ABC (sync_turn, prefetch, shutdown, optional post_setup).

Closing this in line with that policy. The path forward is to publish it as a standalone plugin so users can install it directly without touching the Hermes source tree. Once it's published, a small docs PR adding it to the Community plugins section of the README is welcome.

Sorry for the bump — appreciate the time you put into this.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp/plugins Plugin system and bundled plugins P3 Low — cosmetic, nice to have tool/memory Memory tool and memory providers tool/web Web search and extraction type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants