diff --git a/backend/packages/harness/deerflow/config/memory_config.py b/backend/packages/harness/deerflow/config/memory_config.py index b0ad30e73..17eb7111b 100644 --- a/backend/packages/harness/deerflow/config/memory_config.py +++ b/backend/packages/harness/deerflow/config/memory_config.py @@ -121,7 +121,36 @@ _memory_config: MemoryConfig = MemoryConfig() def get_memory_config() -> MemoryConfig: - """Get the current memory configuration.""" + """Get the current memory configuration. + + ``_memory_config`` is only refreshed as a side effect of ``get_app_config()`` + reloading (via ``_apply_singleton_configs`` -> ``load_memory_config_from_dict``). + A reader that reaches memory config without going through ``get_app_config()`` + first -- e.g. the agent factory deciding whether to bind the memory tools -- + would otherwise see a stale ``memory.mode`` after a ``config.yaml`` edit, even + though ``memory.*`` is documented as hot-reloadable. Trigger the same + signature-checked reload here so the singleton follows the config file. + + If ``get_app_config()`` has never been called (``_app_config`` is ``None``), + there is no stale config to refresh, so we keep the pre-existing behaviour + of returning the in-memory singleton. This avoids picking up a config file + as a side effect of the first access to ``get_memory_config()``, which would + break callers that expect module-level defaults (e.g. unit tests). + """ + # Lazy import: app_config imports this module, so a top-level import cycles. + from .app_config import _app_config, get_app_config + + if _app_config is not None: + try: + get_app_config() + except Exception: + # If the config file is transiently broken (invalid YAML, schema + # violation, missing env var, etc.), keep the last-good singleton + # so an in-flight turn completes normally instead of crashing. + logger.warning( + "Failed to reload app config from get_memory_config(); falling back to cached memory config.", + exc_info=True, + ) return _memory_config diff --git a/backend/tests/test_app_config_reload.py b/backend/tests/test_app_config_reload.py index febe3ac27..4ef30cced 100644 --- a/backend/tests/test_app_config_reload.py +++ b/backend/tests/test_app_config_reload.py @@ -611,3 +611,63 @@ def test_get_app_config_does_not_mutate_singletons_when_reload_validation_fails( assert get_store() is initial_store finally: _reset_config_singletons() + + +def test_get_memory_config_self_syncs_without_prior_get_app_config(tmp_path, monkeypatch): + """get_memory_config() triggers reload when file changes without prior get_app_config(). + + Background memory paths (updater/queue/storage) never call get_app_config() + directly. This test pins the fix: calling get_memory_config() in isolation + — after mutating the file, with no intervening get_app_config() — reflects + the new value. + """ + config_path = tmp_path / "config.yaml" + extensions_path = tmp_path / "extensions_config.json" + _write_extensions_config(extensions_path) + + _write_config_with_sections(config_path, {"memory": {"enabled": False}}) + + monkeypatch.setenv("DEER_FLOW_CONFIG_PATH", str(config_path)) + monkeypatch.setenv("DEER_FLOW_EXTENSIONS_CONFIG_PATH", str(extensions_path)) + reset_app_config() + + try: + get_app_config() + assert get_memory_config().enabled is False + + _write_config_with_sections(config_path, {"memory": {"enabled": True}}) + next_mtime = config_path.stat().st_mtime + 5 + os.utime(config_path, (next_mtime, next_mtime)) + + assert get_memory_config().enabled is True + finally: + _reset_config_singletons() + + +def test_get_memory_config_falls_back_on_broken_config(tmp_path, monkeypatch): + """get_memory_config() does not crash on transiently broken config.yaml. + + get_app_config() can raise yaml.YAMLError, ValidationError, or ValueError. + get_memory_config() catches them and returns the last-good singleton. + """ + config_path = tmp_path / "config.yaml" + extensions_path = tmp_path / "extensions_config.json" + _write_extensions_config(extensions_path) + + _write_config_with_sections(config_path, {"memory": {"enabled": False}}) + + monkeypatch.setenv("DEER_FLOW_CONFIG_PATH", str(config_path)) + monkeypatch.setenv("DEER_FLOW_EXTENSIONS_CONFIG_PATH", str(extensions_path)) + reset_app_config() + + try: + get_app_config() + assert get_memory_config().enabled is False + + config_path.write_text("memory: {enabled: true\n") + next_mtime = config_path.stat().st_mtime + 5 + os.utime(config_path, (next_mtime, next_mtime)) + + assert get_memory_config().enabled is False + finally: + _reset_config_singletons()