From 9eebc6a9e5f16e8c4717ecc5eabd9cf6b0fe5c04 Mon Sep 17 00:00:00 2001 From: qin-chenghan Date: Tue, 21 Jul 2026 10:14:29 +0800 Subject: [PATCH] fix(config): sync _memory_config with AppConfig auto-reload (#4208) * fix(config): sync _memory_config with AppConfig auto-reload get_memory_config() now calls get_app_config() before returning the cached _memory_config singleton. get_app_config() auto-reloads when the config file signature changes, and its _apply_singleton_configs() calls load_memory_config_from_dict() to populate _memory_config. Without this side effect, changing memory.mode in config.yaml (e.g. from "middleware" to "tool") would never take effect at runtime because _memory_config retained the stale value. Only call get_app_config() when _app_config has already been loaded to avoid 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). Catch FileNotFoundError for environments without a config file. Closes #4204. * fix(memory): broaden config-reload guard and add isolated-path tests Addresses two review findings from willem-bd: 1. Broken-config crash risk: broaden the except from FileNotFoundError to Exception so transiently broken config.yaml (YAMLError, ValidationError, ValueError) falls back to the last-good singleton instead of crashing per-turn hot paths (memory_middleware, summarization_hook, lead_agent/prompt.py). 2. Missing regression test: add test_get_memory_config_self_syncs_... (calls get_memory_config() after file mutation with no intervening get_app_config() call) and test_get_memory_config_falls_back_on_... (verifies broken config does not crash, returns cached value). --- .../harness/deerflow/config/memory_config.py | 31 +++++++++- backend/tests/test_app_config_reload.py | 60 +++++++++++++++++++ 2 files changed, 90 insertions(+), 1 deletion(-) 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()