fix(config): one bounded backups/config/ dir replaces four config.yaml.bak schemes

Four writers each dropped their own uniquely-named copy of config.yaml next to
the real file and none of them ever deleted anything: hermes setup
(config.yaml.bak.YYYYMMDD_HHMMSS, one per run even with no change), the
corrupt-YAML snapshot (config.yaml.corrupt.<ts>.bak), hermes migrate xai
(config.yaml.bak-pre-migrate-xai-<ts>) and the Docker boot migration
(config.yaml.bak-<ts>, .env.bak-<ts>). A home dir accumulated a dozen variants
with no way to tell which mattered.

hermes_cli/config_backups.py::backup_config is now the single writer:
backups/config/config.yaml.<reason>.<YYYYMMDD-HHMMSS>, skipped when the newest
copy for that reason is byte-identical, rotated to the newest five per reason.
backups/ is already excluded from full backups so nothing nests. Legacy
siblings written by the old schemes are moved into the dir on first use;
hand-named copies (config.yaml.bak-my-note) are left alone.

Live: three `hermes setup --non-interactive` runs against an unchanged config
went from three .bak files in HERMES_HOME to one pre-setup copy under
backups/config/; repeated loads of broken YAML produce one corrupt copy
instead of one per process (deduped by content).
This commit is contained in:
Teknium
2026-09-09 02:02:06 -07:00
parent 06dc51d62d
commit bf53ff00a7
12 changed files with 158 additions and 91 deletions
+2 -15
View File
@@ -580,20 +580,6 @@ def run_setup_wizard(args):
return None
def _backup_config_file(config_path: Path) -> Path | None:
"""Back up config.yaml before setup modifies it; None when absent or copy fails."""
if not config_path.exists():
return None
import shutil
from datetime import datetime
backup_path = config_path.with_suffix(f".yaml.bak.{datetime.now().strftime('%Y%m%d_%H%M%S')}")
try:
shutil.copy2(config_path, backup_path)
return backup_path
except Exception:
return None
def _run_setup_section(config: dict, section: str) -> None:
"""``hermes setup <section>``: run one SETUP_SECTIONS entry under the banner."""
entry = next(((label, func) for key, label, func in SETUP_SECTIONS if key == section), None)
@@ -672,7 +658,8 @@ def _run_setup_wizard_impl(args):
hermes_home = get_hermes_home()
# Back up existing config before setup modifies it (#3522)
config_path = get_config_path()
_backup_path = _backup_config_file(config_path)
from hermes_cli.config_backups import backup_config
_backup_path = backup_config(config_path, "pre-setup")
# Non-interactive environments (headless SSH, Docker, CI/CD)
if getattr(args, 'non_interactive', False) or not is_interactive_stdin():