fix: invalidate config cache on file mtime change for MCP stdio server

The module-level _CONFIG_CACHE was never refreshed in long-lived processes
(e.g. MCP stdio server used by Claude Desktop), so bm project set-cloud
or set-local had no effect until the server was restarted.

Add a companion _CONFIG_CACHE_MTIME variable that records the config
file's mtime when the cache is populated.  load_config() now does a
cheap stat() call on each invocation and drops the cache when the mtime
has changed, allowing the updated routing mode to take effect immediately
without a server restart.

save_config() clears both cache variables so in-process saves still work
correctly.

Fixes #660

Co-authored-by: Drew Cain <groksrc@users.noreply.github.com>
Signed-off-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
This commit is contained in:
claude[bot]
2026-03-10 17:48:02 +00:00
parent 6e4bb72f10
commit 57c83b0072
2 changed files with 171 additions and 4 deletions
+32 -4
View File
@@ -629,6 +629,10 @@ class BasicMemoryConfig(BaseSettings):
# Module-level cache for configuration
_CONFIG_CACHE: Optional[BasicMemoryConfig] = None
# mtime of the config file when the cache was last populated.
# Used to detect out-of-process changes (e.g. CLI set-cloud/set-local while
# the MCP stdio server is running) so the long-lived process picks up updates.
_CONFIG_CACHE_MTIME: Optional[float] = None
class ConfigManager:
@@ -663,10 +667,29 @@ class ConfigManager:
following Pydantic Settings best practices.
Uses module-level cache for performance across ConfigManager instances.
The cache is validated against the config file's mtime on every call so
that long-lived processes (MCP stdio server) automatically pick up
changes made by external commands like `bm project set-cloud`.
"""
global _CONFIG_CACHE
global _CONFIG_CACHE, _CONFIG_CACHE_MTIME
# Return cached config if available
# --- Detect out-of-process config changes via mtime ---
# Trigger: another process (CLI set-cloud/set-local) wrote a new config
# while this process was running and has a cached copy.
# Why: _CONFIG_CACHE is never invalidated across process boundaries;
# a cheap stat() call lets us detect the change without polling.
# Outcome: stale cache is dropped and the file is re-read below.
if _CONFIG_CACHE is not None and self.config_file.exists():
current_mtime = self.config_file.stat().st_mtime
if current_mtime != _CONFIG_CACHE_MTIME:
logger.debug(
f"Config file modified since last load (mtime changed), "
f"invalidating cache: {self.config_file}"
)
_CONFIG_CACHE = None
_CONFIG_CACHE_MTIME = None
# Return cached config if still valid
if _CONFIG_CACHE is not None:
return _CONFIG_CACHE
@@ -722,6 +745,8 @@ class ConfigManager:
merged_data[field_name] = env_dict[field_name]
_CONFIG_CACHE = BasicMemoryConfig(**merged_data)
# Record mtime so we can detect future out-of-process changes.
_CONFIG_CACHE_MTIME = self.config_file.stat().st_mtime
# Re-save to normalize legacy config into current format
if needs_resave:
@@ -753,10 +778,13 @@ class ConfigManager:
def save_config(self, config: BasicMemoryConfig) -> None:
"""Save configuration to file and invalidate cache."""
global _CONFIG_CACHE
global _CONFIG_CACHE, _CONFIG_CACHE_MTIME
save_basic_memory_config(self.config_file, config)
# Invalidate cache so next load_config() reads fresh data
# Invalidate cache so next load_config() reads fresh data.
# Also reset mtime so the in-process re-read after save picks up the
# new file rather than seeing the newly-written mtime as "unchanged".
_CONFIG_CACHE = None
_CONFIG_CACHE_MTIME = None
@property
def projects(self) -> Dict[str, str]: