From 16919f7c52f411ed78a26272eaa19c07eb2b5f78 Mon Sep 17 00:00:00 2001 From: Xinmin Zeng <135568692+fancyboi999@users.noreply.github.com> Date: Wed, 15 Jul 2026 19:56:09 +0800 Subject: [PATCH] fix(skills): reuse the resolved app config in the no-arg skills prompt section (#4160) get_skills_prompt_section() without app_config resolved get_app_config() only to read container_path, then let the enabled-skills load fall back to the warm cache. On a cold start the cache is empty and the first call returns an empty skills list while the synchronously-loaded disabled section is populated, so manually assembled agents (create_deerflow_agent style integrations) got a prompt with no enabled skills. Rebind the resolved config so the storage and enabled-skills loads below use it too; when no config is resolvable the cache-only fallback is unchanged. Adds a cold-cache regression test. Fixes #4144 Co-authored-by: fancyboi999 --- .../deerflow/agents/lead_agent/prompt.py | 12 +++++-- backend/tests/test_lead_agent_skills.py | 34 +++++++++++++++++-- 2 files changed, 41 insertions(+), 5 deletions(-) diff --git a/backend/packages/harness/deerflow/agents/lead_agent/prompt.py b/backend/packages/harness/deerflow/agents/lead_agent/prompt.py index 915118138..a2b661cd5 100644 --- a/backend/packages/harness/deerflow/agents/lead_agent/prompt.py +++ b/backend/packages/harness/deerflow/agents/lead_agent/prompt.py @@ -805,10 +805,16 @@ def get_skills_prompt_section( try: from deerflow.config import get_app_config - config = get_app_config() - container_base_path = config.skills.container_path - skill_evolution_enabled = config.skill_evolution.enabled + # Rebind so the storage/enabled-skills loads below use this resolved + # config too. Reading only container_path here and then letting + # get_enabled_skills_for_config(None) fall back to the warm cache + # rendered an empty enabled-skills list on a cold start while the + # synchronously-loaded disabled section was populated (#4144). + app_config = get_app_config() + container_base_path = app_config.skills.container_path + skill_evolution_enabled = app_config.skill_evolution.enabled except Exception: + app_config = None container_base_path = DEFAULT_SKILLS_CONTAINER_PATH skill_evolution_enabled = False else: diff --git a/backend/tests/test_lead_agent_skills.py b/backend/tests/test_lead_agent_skills.py index 40591c2c8..86e9925d1 100644 --- a/backend/tests/test_lead_agent_skills.py +++ b/backend/tests/test_lead_agent_skills.py @@ -11,7 +11,7 @@ class NamedTool: self.name = name -def _make_skill(name: str, allowed_tools: list[str] | None = None) -> Skill: +def _make_skill(name: str, allowed_tools: list[str] | None = None, *, enabled: bool = True) -> Skill: return Skill( name=name, description=f"Description for {name}", @@ -21,7 +21,7 @@ def _make_skill(name: str, allowed_tools: list[str] | None = None) -> Skill: relative_path=Path(name), category="public", allowed_tools=tuple(allowed_tools) if allowed_tools is not None else None, - enabled=True, + enabled=enabled, ) @@ -80,6 +80,36 @@ def test_get_skills_prompt_section_returns_all_when_available_skills_is_none(mon assert "skill2" in result +def test_get_skills_prompt_section_no_arg_cold_cache_loads_enabled_skills(monkeypatch): + """#4144: a fresh process calling the no-arg helper must not render an empty + enabled-skills list while the synchronously-loaded disabled section is populated.""" + import threading + + from deerflow.agents.lead_agent import prompt as prompt_mod + + skills = [_make_skill("skill1"), _make_skill("skill2", enabled=False)] + mock_storage = SimpleNamespace(load_skills=lambda *, enabled_only: [s for s in skills if s.enabled or not enabled_only]) + monkeypatch.setattr("deerflow.agents.lead_agent.prompt.get_or_new_skill_storage", lambda **kwargs: mock_storage) + monkeypatch.setattr("deerflow.agents.lead_agent.prompt.get_or_new_user_skill_storage", lambda user_id, **kwargs: mock_storage) + monkeypatch.setattr( + "deerflow.config.get_app_config", + lambda: SimpleNamespace( + skills=SimpleNamespace(container_path="/mnt/skills", use="deerflow.skills.storage.local_skill_storage:LocalSkillStorage", get_skills_path=lambda: Path("/tmp/skills")), + skill_evolution=SimpleNamespace(enabled=False), + ), + ) + # Cold cache: no warmed enabled-skills list, and the background refresh must + # not fill it mid-test — the reporter's cold start loses exactly this race. + monkeypatch.setattr(prompt_mod, "_enabled_skills_cache", None) + monkeypatch.setattr(prompt_mod, "_ensure_enabled_skills_cache", lambda: threading.Event()) + + result = get_skills_prompt_section(available_skills=None) + + assert "" in result + assert "skill1" in result + assert "" in result + + def test_get_skills_prompt_section_includes_slash_activation_guidance(monkeypatch): skills = [_make_skill("data-analysis")] monkeypatch.setattr("deerflow.agents.lead_agent.prompt._get_enabled_skills", lambda: skills)