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

View File

@@ -4,7 +4,6 @@ from __future__ import annotations
import shutil
import sys
from datetime import datetime, timezone
from pathlib import Path
from typing import Iterable
@@ -14,6 +13,7 @@ from hermes_cli.config import (
get_env_path,
migrate_config,
)
from hermes_cli.config_backups import backup_config, list_config_backups
from hermes_cli.config_migrations import (
SUPPORT_FLOOR_VERSION,
support_floor_message,
@@ -21,26 +21,14 @@ from hermes_cli.config_migrations import (
from utils import env_var_enabled
def _backup_path(path: Path, stamp: str) -> Path:
base = path.with_name(f"{path.name}.bak-{stamp}")
if not base.exists():
return base
for index in range(1, 1000):
candidate = path.with_name(f"{path.name}.bak-{stamp}.{index}")
if not candidate.exists():
return candidate
raise RuntimeError(f"could not choose a backup path for {path}")
def _backup_existing(paths: Iterable[Path]) -> dict[Path, Path]:
stamp = datetime.now(timezone.utc).strftime("%Y%m%dT%H%M%SZ")
"""Snapshot each file into backups/config/; an identical existing snapshot is reused."""
backups: dict[Path, Path] = {}
for path in paths:
if not path.is_file():
continue
dest = _backup_path(path, stamp)
shutil.copy2(path, dest)
backups[path] = dest
dest = backup_config(path, "pre-docker-migrate") or next(
iter(list_config_backups(path, "pre-docker-migrate")), None)
if dest is not None:
backups[path] = dest
return backups