From 353578faca0577ae5b222cb77cf3b6fbeca9240f Mon Sep 17 00:00:00 2001 From: Teknium <127238744+teknium1@users.noreply.github.com> Date: Tue, 28 Jul 2026 10:40:28 -0700 Subject: [PATCH] fix(config): persist runtime settings to HERMES_HOME/config.yaml, not the repo template MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The wake-word ear reverted to disabled after every restart even when closed enabled. Root cause is general, not wake-specific: save_config_value followed load_cli_config's precedence, which falls back to the repo's checked-in cli-config.yaml when HERMES_HOME/config.yaml doesn't exist yet. On such installs (managed/desktop first launch), the toggle's persist wrote wake_word.enabled=true into cli-config.yaml and returned success — but every config reader (load_config -> get_hermes_home()/config.yaml, including load_wake_word_config) reads only HERMES_HOME/config.yaml, so the setting was invisible on the next launch. Same silent loss hit any runtime persist (model switch, /reasoning, /fast, skin) on a config-less install. save_config_value now always targets get_hermes_home()/config.yaml, creating it if absent, and never writes the shipped repo template. Also resolves HERMES_HOME live instead of the import-time _hermes_home constant (profile-safe). E2E verified: persist -> fresh module reload -> load_wake_word_config sees enabled=true and wake_surface_enabled('gui') is True, for both a fresh (no config.yaml) and an existing-config install. - cli.py save_config_value: target user config, create if absent - tests: 2 regression tests (creates user config when absent; never writes repo cli-config.yaml); fixture now sets HERMES_HOME. 11 file + 206 adjacent config/model-switch tests green --- cli.py | 18 ++++++-- tests/cli/test_cli_save_config_value.py | 56 ++++++++++++++++++++++++- 2 files changed, 69 insertions(+), 5 deletions(-) diff --git a/cli.py b/cli.py index 1d22d893d052..16ddc1d6008b 100644 --- a/cli.py +++ b/cli.py @@ -4020,10 +4020,20 @@ def save_config_value(key_path: str, value: any) -> bool: Returns: True if successful, False otherwise """ - # Use the same precedence as load_cli_config: user config first, then project config - user_config_path = _hermes_home / 'config.yaml' - project_config_path = Path(__file__).parent / 'cli-config.yaml' - config_path = user_config_path if user_config_path.exists() else project_config_path + # Runtime persistence ALWAYS targets the user's HERMES_HOME config.yaml, + # creating it if needed. Resolve HERMES_HOME live (not the import-time + # _hermes_home constant) so profile switches and test isolation land right. + # + # We deliberately do NOT fall back to the repo's project cli-config.yaml: + # that file is a shipped default/template, and most config readers + # (load_config → get_hermes_home()/config.yaml, including + # load_wake_word_config) never read it. Writing a user setting there means + # the reader never sees it. This was the "wake-word ear reverts to disabled + # after restart" bug — the toggle's persist wrote to cli-config.yaml (which + # exists in the checkout) while startup read HERMES_HOME/config.yaml, so the + # setting silently vanished every restart on any install whose + # HERMES_HOME/config.yaml didn't exist yet. + config_path = get_hermes_home() / 'config.yaml' try: # Ensure parent directory exists (for ~/.hermes/config.yaml on first use) diff --git a/tests/cli/test_cli_save_config_value.py b/tests/cli/test_cli_save_config_value.py index a966217065b7..e820920a2b36 100644 --- a/tests/cli/test_cli_save_config_value.py +++ b/tests/cli/test_cli_save_config_value.py @@ -1,8 +1,10 @@ """Tests for save_config_value() in cli.py — atomic write behavior.""" -import yaml +from pathlib import Path from unittest.mock import MagicMock +import yaml + import pytest @@ -19,6 +21,10 @@ class TestSaveConfigValueAtomic: "model": {"default": "test-model", "provider": "openrouter"}, "display": {"skin": "default"}, })) + # save_config_value resolves the target live via get_hermes_home(), so + # point HERMES_HOME at the temp dir (the _hermes_home import-time + # constant is no longer consulted). + monkeypatch.setenv("HERMES_HOME", str(hermes_home)) monkeypatch.setattr("cli._hermes_home", hermes_home) return config_path @@ -144,3 +150,51 @@ class TestSaveConfigValueAtomic: assert result is False assert config_env.read_text() == original_content + + +class TestSaveConfigValueTargetsUserConfig: + """Regression: persisted runtime settings must land in HERMES_HOME/config.yaml + (which config readers actually read), never the repo's cli-config.yaml. + + This was the "wake-word ear reverts to disabled after restart" bug: on an + install whose HERMES_HOME/config.yaml did not exist yet, save_config_value + fell back to the checked-in cli-config.yaml. The toggle reported success, but + startup read HERMES_HOME/config.yaml and never saw the setting.""" + + def test_creates_user_config_when_absent(self, tmp_path, monkeypatch): + # Fresh HERMES_HOME with NO config.yaml (managed/desktop first launch). + hermes_home = tmp_path / ".hermes" + hermes_home.mkdir() + monkeypatch.setenv("HERMES_HOME", str(hermes_home)) + + from cli import save_config_value + + assert save_config_value("wake_word.enabled", True) is True + + config_path = hermes_home / "config.yaml" + assert config_path.exists(), "user config.yaml must be created, not skipped" + result = yaml.safe_load(config_path.read_text()) + assert result["wake_word"]["enabled"] is True + + def test_does_not_write_repo_cli_config(self, tmp_path, monkeypatch): + # Even when the repo's cli-config.yaml exists, the write goes to the + # user config, so a runtime setting is never buried in the shipped file. + import cli as cli_module + + repo_cli_config = Path(cli_module.__file__).parent / "cli-config.yaml" + before = repo_cli_config.read_text() if repo_cli_config.exists() else None + + hermes_home = tmp_path / ".hermes" + hermes_home.mkdir() + monkeypatch.setenv("HERMES_HOME", str(hermes_home)) + + from cli import save_config_value + + save_config_value("wake_word.enabled", True) + + # The repo template is untouched… + after = repo_cli_config.read_text() if repo_cli_config.exists() else None + assert after == before + # …and the value landed in the user config. + result = yaml.safe_load((hermes_home / "config.yaml").read_text()) + assert result["wake_word"]["enabled"] is True