fix(profiles): share the runtime-tree trio with backup via hermes_constants
backup.py imports hermes_cli.profiles only lazily and profiles.py never imports backup, so there was no cycle to justify two literals. LOCAL_RUNTIME_ROOT_DIRS now feeds both backup._EXCLUDED_ROOT_DIRS and the clone-all root gate; one invariant test pins the identity.
This commit is contained in:
@@ -17,7 +17,8 @@ from pathlib import Path
|
||||
from typing import Any, Dict, List, Optional, Tuple
|
||||
|
||||
from hermes_constants import (
|
||||
_get_platform_default_hermes_home, get_default_hermes_root, get_hermes_home, display_hermes_home,
|
||||
LOCAL_RUNTIME_ROOT_DIRS, _get_platform_default_hermes_home, get_default_hermes_root, get_hermes_home,
|
||||
display_hermes_home,
|
||||
)
|
||||
from hermes_state_dbfile import RETIRED_GENERATION_DIR_SUFFIX
|
||||
from utils import (
|
||||
@@ -68,10 +69,10 @@ _EXCLUDED_DIRS = {
|
||||
".cache", ".tox", ".nox", ".pytest_cache", ".mypy_cache", ".ruff_cache",
|
||||
}
|
||||
|
||||
# Hermes-managed runtime downloads (GGUF models, llama.cpp runtimes, managed Node): re-downloaded
|
||||
# on demand and routinely tens to hundreds of GB. Matched ONLY at the root of HERMES_HOME and at
|
||||
# ``profiles/<name>/`` — a deeper dir of the same name (a skill's ``models/``) is user data.
|
||||
_EXCLUDED_ROOT_DIRS = {"models", "runtimes", "node"}
|
||||
# Hermes-managed runtime downloads (see ``LOCAL_RUNTIME_ROOT_DIRS``). Matched ONLY at the root of
|
||||
# HERMES_HOME and at ``profiles/<name>/`` — a deeper dir of the same name (a skill's ``models/``)
|
||||
# is user data.
|
||||
_EXCLUDED_ROOT_DIRS = LOCAL_RUNTIME_ROOT_DIRS
|
||||
|
||||
# ``cache/`` at those same roots mixes regenerable state (model/plugin catalogs, stamps, browser
|
||||
# profiles with locked SQLite, tool-output spill) with durable artifacts nothing can rebuild: media
|
||||
|
||||
@@ -17,7 +17,9 @@ from typing import Dict, List, Optional, Tuple
|
||||
|
||||
from agent.skill_utils import is_excluded_skill_path
|
||||
from hermes_cli.archive_safe import archive_root_dirs, make_targz, normalize_archive_parts, safe_extract_targz
|
||||
from hermes_constants import clear_named_profile_deleted, mark_named_profile_deleted, named_profile_is_deleted
|
||||
from hermes_constants import (
|
||||
LOCAL_RUNTIME_ROOT_DIRS, clear_named_profile_deleted, mark_named_profile_deleted, named_profile_is_deleted,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -45,12 +47,11 @@ _CLONE_ALL_STRIP: list[str] = ["gateway.pid", "gateway_state.json", "processes.j
|
||||
# and resolved from the default root only. Named profiles never hold these at root, so the
|
||||
# gate avoids silently dropping user data from a named-profile source. Export uses a root
|
||||
# allow-list instead (``_DEFAULT_EXPORT_INCLUDE_ROOT``): an archive is a portable snapshot,
|
||||
# a clone must run. The last three mirror ``hermes_cli.backup._EXCLUDED_ROOT_DIRS`` — kept
|
||||
# as separate literals because importing it here would be circular; change both together.
|
||||
# a clone must run. The runtime trio is ``LOCAL_RUNTIME_ROOT_DIRS``, shared with
|
||||
# ``hermes_cli.backup._EXCLUDED_ROOT_DIRS`` so the two lists cannot drift.
|
||||
_CLONE_ALL_DEFAULT_EXCLUDE_ROOT: frozenset[str] = frozenset({
|
||||
"hermes-agent", ".worktrees", "profiles", "bin", "node_modules",
|
||||
"models", "runtimes", "node",
|
||||
})
|
||||
}) | LOCAL_RUNTIME_ROOT_DIRS
|
||||
|
||||
# Per-profile history excluded from --clone-all for ANY source: SQLite session store
|
||||
# (+wal/shm, can reach many GB), session dirs, `hermes backup` archives, quick-backup
|
||||
|
||||
@@ -157,6 +157,12 @@ def get_process_hermes_home() -> Path:
|
||||
return Path(val) if val else _get_platform_default_hermes_home()
|
||||
|
||||
|
||||
# Hermes-managed runtime downloads at the root of a home (GGUF models, llama.cpp runtimes,
|
||||
# managed Node): re-downloadable on demand and routinely tens to hundreds of GB. Shared by
|
||||
# ``hermes backup`` (excludes them) and ``profile create --clone-all`` (skips them from the
|
||||
# default profile) so the two lists cannot drift apart.
|
||||
LOCAL_RUNTIME_ROOT_DIRS: frozenset[str] = frozenset({"models", "runtimes", "node"})
|
||||
|
||||
# get_default_hermes_root() memo keyed on (native home, HERMES_HOME) so it stays
|
||||
# fresh when a test or plugin mutates HERMES_HOME; saves ~80us/call at 31+ sites.
|
||||
_default_hermes_root_memo: "tuple[str, str, Path] | None" = None
|
||||
|
||||
@@ -1308,6 +1308,15 @@ class TestCloneAllExcludesRuntimeTrees:
|
||||
(source / tree).mkdir()
|
||||
assert not _clone_all_copytree_ignore(source)(str(source), [*self.RUNTIME_TREES, "SOUL.md"])
|
||||
|
||||
def test_runtime_trio_is_one_constant_shared_with_backup(self):
|
||||
"""backup's exclusion list and the clone-all root gate must be built from the same
|
||||
constant; two literals drifting apart is how the models/ copy of #111718 crept in."""
|
||||
from hermes_cli import backup, profiles
|
||||
from hermes_constants import LOCAL_RUNTIME_ROOT_DIRS
|
||||
assert LOCAL_RUNTIME_ROOT_DIRS == frozenset(self.RUNTIME_TREES)
|
||||
assert backup._EXCLUDED_ROOT_DIRS is LOCAL_RUNTIME_ROOT_DIRS
|
||||
assert LOCAL_RUNTIME_ROOT_DIRS <= profiles._CLONE_ALL_DEFAULT_EXCLUDE_ROOT
|
||||
|
||||
def test_clone_all_from_default_skips_runtime_trees_but_keeps_the_rest(self, profile_env):
|
||||
default_home = profile_env / ".hermes"
|
||||
self._seed(default_home)
|
||||
|
||||
Reference in New Issue
Block a user