From a5dde2d176cbf6351512fd70d3d9dd1ba59d5478 Mon Sep 17 00:00:00 2001 From: Erosika Date: Mon, 10 Aug 2026 19:48:40 -0400 Subject: [PATCH] fix(memory): propagate contextvars through MemoryManager background lanes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit MemoryManager dispatches provider sync_turn/queue_prefetch work on a single-worker executor and hot prefetch on a plain thread. Neither carried the caller's contextvars, so in multi-profile processes the provider work ran outside the profile's ContextVar-scoped HERMES_HOME override — any ambient resolution inside a provider landed on the default profile. Wrap the submitted callable and the prefetch thread target with contextvars.copy_context().run, mirroring the gateway's _run_in_executor_with_context pattern. Provider-agnostic: benefits every external memory provider, not just Honcho. --- agent/memory_manager.py | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/agent/memory_manager.py b/agent/memory_manager.py index 6db0c60cf3ea9..27e90db579e63 100644 --- a/agent/memory_manager.py +++ b/agent/memory_manager.py @@ -559,8 +559,13 @@ class MemoryManager: except Exception as exc: # pragma: no cover - re-raised by caller error_box["value"] = exc + # Propagate the caller's contextvars (profile HERMES_HOME override) + # to the prefetch thread — see _submit_background. + import contextvars + + _ctx = contextvars.copy_context() thread = threading.Thread( - target=_run, + target=lambda: _ctx.run(_run), daemon=True, name=f"memory-prefetch-{provider.name}", ) @@ -728,7 +733,19 @@ class MemoryManager: # -- Background dispatch ------------------------------------------------- def _submit_background(self, fn, *, kind: str = "write") -> None: - """Queue ``fn`` on the serialized worker and track its durability class.""" + """Queue ``fn`` on the serialized worker and track its durability class. + + The submitted callable is wrapped with the CALLER's contextvars: + profile isolation in multi-profile processes (gateway multiplexer, + dashboard, cron) is a ContextVar-scoped HERMES_HOME override, and + executor worker threads start with empty contexts — without the + wrap, a provider resolving ambient state (config paths, secrets) + from the worker would silently land on the default profile. + """ + import contextvars + + ctx = contextvars.copy_context() + fn = (lambda inner: (lambda: ctx.run(inner)))(fn) executor = self._get_sync_executor() if executor is None: if self._shutting_down: