From acd2c186effca5193e44da0e25f3cdceb370203c Mon Sep 17 00:00:00 2001 From: nesquena-hermes Date: Sat, 20 Jun 2026 03:51:15 +0000 Subject: [PATCH 1/2] stage #4516: apply config defaults to non-default profile reads (#4465 / #4513) + v0.51.532 CHANGELOG --- CHANGELOG.md | 6 ++++ api/config.py | 9 ++++- tests/test_issue4465_builtin_personalities.py | 36 +++++++++++++++++++ 3 files changed, 50 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 70e075c2..1fc61be9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,12 @@ ## [Unreleased] +## [v0.51.532] — 2026-06-20 — Release SQ (built-in personalities on non-default profiles) + +### Fixed + +- **Non-default profiles now see the built-in personalities too (follow-up to #4465, closes the #4513 gap).** v0.51.525 hydrated the 14 built-in personalities for the default profile, but `get_config_for_profile_home()` — used by the streaming worker to resolve a non-default profile's config — was a pure file read with no defaults applied, so a fresh non-default profile still resolved `personalities: []`. It now applies the documented config defaults (including the built-ins) to the per-profile read, matching the ambient `get_config()` shape, without mutating any global cache. A non-existent profile home returns an empty dict. Thanks @franksong2702. + ## [v0.51.531] — 2026-06-20 — Release SP (plugins panel: correct active-provider badge) ### Fixed diff --git a/api/config.py b/api/config.py index 46860e5e..5028a92c 100644 --- a/api/config.py +++ b/api/config.py @@ -510,6 +510,8 @@ def get_config_for_profile_home(profile_home: "Path | str | None") -> dict: target = Path(profile_home).expanduser() except Exception: return get_config() + if not target.exists(): + return {} # If the ambient resolver already points at this profile home, defer to # get_config() so in-memory overrides (monkeypatched cfg) are honored. try: @@ -517,7 +519,12 @@ def get_config_for_profile_home(profile_home: "Path | str | None") -> dict: return get_config() except Exception: pass - return _load_yaml_config_file(target / "config.yaml") + # Read the profile file directly and apply documented defaults locally so the + # returned dict matches ambient get_config() shape (including built-in + # personalities) without mutating any global cache state. + profile_cfg = _load_yaml_config_file(target / "config.yaml") + _apply_config_defaults(profile_cfg) + return profile_cfg def _config_for_yaml_save(config_data: dict) -> dict: diff --git a/tests/test_issue4465_builtin_personalities.py b/tests/test_issue4465_builtin_personalities.py index 37c512f6..7e6ab148 100644 --- a/tests/test_issue4465_builtin_personalities.py +++ b/tests/test_issue4465_builtin_personalities.py @@ -69,6 +69,42 @@ def test_config_defaults_replace_non_dict_personality_section_with_builtins(): assert set(cfg["agent"]["personalities"]) == BUILTIN_NAMES +def test_profile_home_config_read_hydrates_builtin_personalities_without_writing(tmp_path): + profile_home = tmp_path / "profiles" / "research" + profile_home.mkdir(parents=True) + + cfg = config.get_config_for_profile_home(profile_home) + + personalities = cfg["agent"]["personalities"] + assert set(personalities) == BUILTIN_NAMES + assert personalities["helpful"] == "You are a helpful, friendly AI assistant." + assert not (profile_home / "config.yaml").exists() + + +def test_profile_home_config_read_merges_custom_personalities(tmp_path): + profile_home = tmp_path / "profiles" / "research" + profile_home.mkdir(parents=True) + (profile_home / "config.yaml").write_text( + "\n".join( + [ + "agent:", + " personalities:", + " helpful: Custom helpful prompt.", + " analyst:", + " system_prompt: Use careful evidence.", + ] + ), + encoding="utf-8", + ) + + cfg = config.get_config_for_profile_home(profile_home) + + personalities = cfg["agent"]["personalities"] + assert set(BUILTIN_NAMES).issubset(personalities) + assert personalities["helpful"] == "Custom helpful prompt." + assert personalities["analyst"]["system_prompt"] == "Use careful evidence." + + def test_config_save_strips_generated_builtin_personalities(tmp_path): cfg = {} config._apply_config_defaults(cfg) From e1e2bbc7ef8581124bc67dc7bcbae28250916c04 Mon Sep 17 00:00:00 2001 From: nesquena-hermes Date: Sat, 20 Jun 2026 03:57:56 +0000 Subject: [PATCH 2/2] fix(config): order ghost-home guard after ambient short-circuit (#4516 Codex gate) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Codex SILENT finding: the new 'if not target.exists(): return {}' guard ran BEFORE the ambient-resolver short-circuit, so a matching ambient profile home whose directory does not physically exist (fresh install / monkeypatched cfg) returned {} instead of deferring to get_config() — breaking in-memory overrides and changing the default-profile missing-home path. Move the guard below the ambient short-circuit; add a regression test (matching-but-nonexistent ambient home still defers to get_config()). --- api/config.py | 9 ++++++--- tests/test_issue3294_profile_toolset_scoping.py | 16 ++++++++++++++++ 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/api/config.py b/api/config.py index 5028a92c..2704d066 100644 --- a/api/config.py +++ b/api/config.py @@ -510,15 +510,18 @@ def get_config_for_profile_home(profile_home: "Path | str | None") -> dict: target = Path(profile_home).expanduser() except Exception: return get_config() - if not target.exists(): - return {} # If the ambient resolver already points at this profile home, defer to - # get_config() so in-memory overrides (monkeypatched cfg) are honored. + # get_config() so in-memory overrides (monkeypatched cfg) are honored. This + # MUST run before the nonexistent-home guard below: a matching ambient home + # whose directory doesn't physically exist yet (fresh install, monkeypatched + # cfg) must still resolve through get_config(), not return {} (#4516 gate). try: if _get_config_path().parent == target: return get_config() except Exception: pass + if not target.exists(): + return {} # Read the profile file directly and apply documented defaults locally so the # returned dict matches ambient get_config() shape (including built-in # personalities) without mutating any global cache state. diff --git a/tests/test_issue3294_profile_toolset_scoping.py b/tests/test_issue3294_profile_toolset_scoping.py index 7799f53b..d3e4b40d 100644 --- a/tests/test_issue3294_profile_toolset_scoping.py +++ b/tests/test_issue3294_profile_toolset_scoping.py @@ -108,6 +108,22 @@ def test_matching_home_defers_to_get_config(_two_profiles): ] +def test_matching_ambient_home_that_does_not_exist_still_defers_to_get_config(_two_profiles, monkeypatch, tmp_path): + """Regression (#4516 gate): the nonexistent-home guard must run AFTER the + ambient-resolver short-circuit. A home that matches the ambient config path + but whose directory doesn't physically exist (fresh install / monkeypatched + cfg with no dir on disk) must still defer to get_config() — NOT return {}.""" + cfg, default_home, profile_b_home = _two_profiles + ghost_ambient = tmp_path / "ghost-ambient" + monkeypatch.setenv("HERMES_CONFIG_PATH", str(ghost_ambient / "config.yaml")) + cfg.reload_config() + assert not ghost_ambient.exists() + # The requested home matches the (nonexistent) ambient home → must defer to + # get_config(), honoring the in-memory cfg, not short-circuit to {}. + result = cfg.get_config_for_profile_home(ghost_ambient) + assert result == cfg.get_config() + + def test_empty_or_none_home_falls_back_to_get_config(_two_profiles): """A missing/empty profile home (ImportError fallback path in the worker) must not crash — it falls back to the ambient get_config()."""