diff --git a/mem0/vector_stores/configs.py b/mem0/vector_stores/configs.py index 32459dddc3..0e5ef77316 100644 --- a/mem0/vector_stores/configs.py +++ b/mem0/vector_stores/configs.py @@ -1,8 +1,40 @@ +import os +import sys from typing import Dict, Optional from pydantic import BaseModel, Field, model_validator +def _default_data_dir() -> str: + """Return a writable user-data directory suitable for embedded vector stores. + + Resolution order: + 1. ``MEM0_DATA_DIR`` environment variable (explicit override). + 2. Platform convention: + - macOS: ``~/Library/Application Support/mem0`` + - Windows: ``%LOCALAPPDATA%/mem0`` + - Linux/BSD: ``$XDG_DATA_HOME/mem0`` or ``~/.local/share/mem0`` + + The previous default of ``/tmp/{provider}`` broke macOS LaunchAgents, systemd + services with ``noexec`` ``/tmp``, Windows (no ``/tmp``), and Docker (ephemeral + ``/tmp``). See #4279. + """ + env_dir = os.environ.get("MEM0_DATA_DIR") + if env_dir: + return env_dir + if sys.platform == "darwin": + return os.path.expanduser("~/Library/Application Support/mem0") + if sys.platform == "win32": + return os.path.join( + os.environ.get("LOCALAPPDATA") or os.path.expanduser("~/AppData/Local"), + "mem0", + ) + return os.path.join( + os.environ.get("XDG_DATA_HOME") or os.path.expanduser("~/.local/share"), + "mem0", + ) + + class VectorStoreConfig(BaseModel): provider: str = Field( description="Provider of the vector store (e.g., 'qdrant', 'chroma', 'upstash_vector')", @@ -61,7 +93,7 @@ def validate_and_create_config(self) -> "VectorStoreConfig": # also check if path in allowed kays for pydantic model, and whether config extra fields are allowed if "path" not in config and "path" in config_class.__annotations__: - config["path"] = f"/tmp/{provider}" + config["path"] = os.path.join(_default_data_dir(), provider) self.config = config_class(**config) return self diff --git a/tests/vector_stores/test_default_path.py b/tests/vector_stores/test_default_path.py new file mode 100644 index 0000000000..dfa9940171 --- /dev/null +++ b/tests/vector_stores/test_default_path.py @@ -0,0 +1,49 @@ +"""Regression test for #4279: vector store path defaults to /tmp/{provider}, which +fails or silently loses data in macOS LaunchAgents, systemd services, Docker, etc. +""" +import os +import sys +from unittest.mock import patch + +import pytest + +from mem0.vector_stores.configs import VectorStoreConfig + + +def test_default_path_is_not_tmp(): + """The default path must not be /tmp/{provider} on any platform.""" + cfg = VectorStoreConfig(provider="faiss", config={}) + path = cfg.config.path + assert path is not None + assert not path.startswith("/tmp/"), f"default path still falls back to /tmp: {path}" + assert path.endswith("faiss") + + +def test_env_var_override(): + """MEM0_DATA_DIR must override the platform default.""" + with patch.dict(os.environ, {"MEM0_DATA_DIR": "/var/lib/mem0_test"}): + cfg = VectorStoreConfig(provider="faiss", config={}) + assert cfg.config.path == os.path.join("/var/lib/mem0_test", "faiss") + + +def test_explicit_path_wins(): + """An explicit path in the user config must beat both the env var and the default.""" + with patch.dict(os.environ, {"MEM0_DATA_DIR": "/should/not/leak"}): + cfg = VectorStoreConfig(provider="faiss", config={"path": "/explicit/user/path"}) + assert cfg.config.path == "/explicit/user/path" + + +@pytest.mark.skipif(sys.platform != "darwin", reason="macOS-specific default") +def test_macos_default_uses_application_support(): + with patch.dict(os.environ, {}, clear=False): + os.environ.pop("MEM0_DATA_DIR", None) + cfg = VectorStoreConfig(provider="faiss", config={}) + assert "Application Support/mem0" in cfg.config.path + + +@pytest.mark.skipif(sys.platform != "linux", reason="Linux-specific default") +def test_linux_default_respects_xdg_data_home(): + with patch.dict(os.environ, {"XDG_DATA_HOME": "/custom/xdg"}): + os.environ.pop("MEM0_DATA_DIR", None) + cfg = VectorStoreConfig(provider="faiss", config={}) + assert cfg.config.path == os.path.join("/custom/xdg", "mem0", "faiss")