mempalace.integrations.hermes
Source: mempalace/integrations/hermes/__init__.py
MemPalace memory provider for Hermes.
Implements the Hermes MemoryProvider ABC (agent/memory_provider.py) so MemPalace can be selected as memory.provider: mempalace in ~/.hermes/config.yaml.
Design notes
ChromaDB access goes through
mempalace.backends.chroma.ChromaBackendrather than a rawchromadb.PersistentClient. This ensures the embedding function returned bymempalace.embedding.get_embedding_functionis bound to the collection, fixing the embedding-dimension mismatch that silently broke the three earlier Hermes-side PRs (NousResearch/hermes-agent #5671, #12203, #9761) on existing palaces.Per-turn writes go through a bounded background queue. The agent loop never blocks on ChromaDB or SQLite.
sync_turnis the sole filing path.on_session_endandon_pre_compressintentionally file nothing: re-filing the raw message list duplicates every turnsync_turnalready stored —filed_atis hashed into the drawer id, so upserts cannot collapse the copies. Any future safety net here must first scan what is already filed and add only what is missing.The provider is inactive under
agent_context in {"cron", "flush"}orplatform == "cron". Cron-context turns are system-generated and would otherwise corrupt the user's representation.Configuration precedence:
$HERMES_HOME/mempalace.jsonis read first, then env vars override (MEMPALACE_PALACE_PATH,MEMPALACE_IDENTITY_PATH,MEMPALACE_WING). An empty env var is ignored —export MEMPALACE_WING=is intent to unset. A palace still unset after that defers to mempalace's own config (~/.mempalace/config.json) before falling back to the default location. The resolved palace is then published toMEMPALACE_PALACE_PATH(see_bridge_palace_env) so themempalace.mcp_serverpassthrough tools operate on the same palace as live filing and search — never a config-file-vs-provider split.collection_namefollows mempalace's own config for the same reason: it is whatsearch_memories(used byprefetchand_tool_search) and the mcp_server passthrough read, so live writes land in the collection recall actually searches. It is intentionally not configurable on the Hermes side — a second knob would let the write and read sides diverge silently again.~/.mempalace/identity.txt(L0) and~/.mempalace/wing_config.jsonare loaded if present but never created here. Runmempalace init <project-dir>to generate them.All palace state (ChromaDB, knowledge graph, diary, identity) lives under
~/.mempalace/by design — the palace is the user's central memory shared across agents, not per-agent Hermes state. This meanshermes backup(which archives only$HERMES_HOME) does NOT cover it; users must back up~/.mempalace/separately. Hermes'MemoryProviderABC currently offers no hook for contributing external paths to its backup.
Classes
class MempalaceProvider(MemoryProvider)
Hermes memory provider backed by MemPalace.
__init__
def __init__(self) -> Nonename
def name(self) -> stris_available
def is_available(self) -> boolAlways True — module-level imports prove mempalace is installed.
If mempalace were missing, import of this module would have failed before Hermes' plugin loader called is_available. The check is kept for ABC conformance and so a future config flag can disable the provider here without surgery elsewhere.
initialize
def initialize(self, session_id: str, **kwargs: Any) -> Noneget_tool_schemas
def get_tool_schemas(self) -> List[Dict[str, Any]]system_prompt_block
def system_prompt_block(self) -> strprefetch
def prefetch(self, query: str, *, session_id: str = '') -> strsync_turn
def sync_turn(self, user_content: str, assistant_content: str, *, session_id: str = '', messages: Optional[List[Dict[str, Any]]] = None) -> Noneon_turn_start
def on_turn_start(self, turn_number: int, message: str, **kwargs: Any) -> Noneon_session_end
def on_session_end(self, messages: List[Dict[str, Any]]) -> Noneon_session_switch
def on_session_switch(self, new_session_id: str, *, parent_session_id: str = '', reset: bool = False, rewound: bool = False, **kwargs: Any) -> Noneon_pre_compress
def on_pre_compress(self, messages: List[Dict[str, Any]]) -> strIntentionally a no-op that returns no hint.
Blind-filing the compression window duplicates every turn sync_turn already filed. Returning "" keeps the summarizer on its default conservative discarding — a hint must never promise persistence this provider hasn't performed.
on_memory_write
def on_memory_write(self, action: str, target: str, content: str, metadata: Optional[Dict[str, Any]] = None) -> Noneon_delegation
def on_delegation(self, task: str, result: str, *, child_session_id: str = '', **kwargs: Any) -> Nonehandle_tool_call
def handle_tool_call(self, tool_name: str, args: Dict[str, Any], **kwargs: Any) -> strget_config_schema
def get_config_schema(self) -> List[Dict[str, Any]]save_config
def save_config(self, values: Dict[str, Any], hermes_home: str) -> Nonepost_setup
def post_setup(self, hermes_home: str, config: Dict[str, Any]) -> Noneshutdown
def shutdown(self) -> NoneFunctions
register
def register(ctx: Any) -> NoneRegister the MemPalace memory provider with Hermes.
