fix(profiles): exclude session history, backups, and snapshots from --clone-all (#45246)
--clone-all copied the source profile's state.db, sessions/, backups/, state-snapshots/, and checkpoints/ into the new profile. These are per-profile history: a 49GB copy in practice (15GB snapshots + 11GB backup archives + 16GB state.db + 6.4GB sessions), and restoring a copied backup inside the clone would resurrect the SOURCE profile's state. A clone is a fresh workspace; history stays with the source. New _CLONE_ALL_HISTORY_EXCLUDE_ROOT set, applied at root level for ANY source profile (named profiles accumulate the same artifacts), unlike the default-gated infrastructure excludes. Nested same-name dirs still copy. Docs and the post-create CLI message updated to match; profile export / hermes backup remain the full-history paths.
This commit is contained in:
+4
-1
@@ -9849,7 +9849,10 @@ def cmd_profile(args):
|
||||
getattr(args, "clone_from", None) or get_active_profile_name()
|
||||
)
|
||||
if clone_all:
|
||||
print(f"Full copy from {source_label}.")
|
||||
print(
|
||||
f"Full copy from {source_label} "
|
||||
"(excluding session history, backups, and snapshots)."
|
||||
)
|
||||
else:
|
||||
print(
|
||||
f"Cloned config, .env, SOUL.md, and skills from {source_label}."
|
||||
|
||||
+48
-17
@@ -88,9 +88,9 @@ _CLONE_ALL_STRIP: list[str] = [
|
||||
# node_modules — npm packages (hundreds of MB)
|
||||
#
|
||||
# See ``_DEFAULT_EXPORT_EXCLUDE_ROOT`` below for the broader export-side
|
||||
# exclusion list (export drops state.db / logs / caches too because the
|
||||
# archive is a portable snapshot; clone-all keeps those because the cloned
|
||||
# profile is meant to keep working immediately).
|
||||
# exclusion list (export also drops logs / caches because the archive is a
|
||||
# portable snapshot; clone-all keeps those because the cloned profile is
|
||||
# meant to keep working immediately).
|
||||
_CLONE_ALL_DEFAULT_EXCLUDE_ROOT: frozenset[str] = frozenset({
|
||||
"hermes-agent",
|
||||
".worktrees",
|
||||
@@ -99,6 +99,30 @@ _CLONE_ALL_DEFAULT_EXCLUDE_ROOT: frozenset[str] = frozenset({
|
||||
"node_modules",
|
||||
})
|
||||
|
||||
# Per-profile history artifacts excluded from --clone-all regardless of the
|
||||
# source profile. A new profile is a fresh workspace — inheriting the source
|
||||
# profile's session history, backup archives, or quick-backup snapshots is
|
||||
# never useful (restoring one inside the clone would resurrect the SOURCE
|
||||
# profile's state) and can balloon the copy by tens of GB. Unlike
|
||||
# ``_CLONE_ALL_DEFAULT_EXCLUDE_ROOT`` this set is NOT gated on the default
|
||||
# profile: named profiles accumulate the same artifacts.
|
||||
#
|
||||
# Rationale per item:
|
||||
# state.db (+wal/shm) — SQLite session store (can reach many GB)
|
||||
# sessions — per-session transcript/data dirs
|
||||
# backups — `hermes backup` archives
|
||||
# state-snapshots — quick-backup snapshot trees
|
||||
# checkpoints — session checkpoint data
|
||||
_CLONE_ALL_HISTORY_EXCLUDE_ROOT: frozenset[str] = frozenset({
|
||||
"state.db",
|
||||
"state.db-wal",
|
||||
"state.db-shm",
|
||||
"sessions",
|
||||
"backups",
|
||||
"state-snapshots",
|
||||
"checkpoints",
|
||||
})
|
||||
|
||||
# Marker file written by `hermes profile create --no-skills`. When present in
|
||||
# a profile's root, callers of seed_profile_skills() (fresh-create, `hermes
|
||||
# update`'s all-profile sync, the web dashboard) skip bundled-skill seeding
|
||||
@@ -119,13 +143,16 @@ def has_bundled_skills_opt_out(profile_dir: Path) -> bool:
|
||||
def _clone_all_copytree_ignore(source_dir: Path):
|
||||
"""Exclude infrastructure artifacts when cloning a profile via --clone-all.
|
||||
|
||||
Two categories:
|
||||
1. Root-level entries in ``_CLONE_ALL_DEFAULT_EXCLUDE_ROOT`` — known
|
||||
Three categories:
|
||||
1. Root-level entries in ``_CLONE_ALL_HISTORY_EXCLUDE_ROOT`` — session
|
||||
history, backups, and snapshots that belong to the SOURCE profile
|
||||
and should never carry into a fresh clone. Applies to any source.
|
||||
2. Root-level entries in ``_CLONE_ALL_DEFAULT_EXCLUDE_ROOT`` — known
|
||||
Hermes infrastructure directories that only the default profile
|
||||
(``~/.hermes``) ever contains. Gated on ``source_dir`` actually
|
||||
being the default profile so a named-profile source never has its
|
||||
own data silently dropped.
|
||||
2. Universal exclusions at any depth — Python bytecode caches that
|
||||
3. Universal exclusions at any depth — Python bytecode caches that
|
||||
are stale or regenerable (``__pycache__``, ``*.pyc``, ``*.pyo``)
|
||||
and runtime sockets / temp files (``*.sock``, ``*.tmp``).
|
||||
|
||||
@@ -147,17 +174,21 @@ def _clone_all_copytree_ignore(source_dir: Path):
|
||||
):
|
||||
ignored.append(entry)
|
||||
continue
|
||||
# Root-level exclusions only apply when cloning the default profile.
|
||||
if is_default_source:
|
||||
try:
|
||||
if Path(directory).resolve() == source_resolved:
|
||||
if entry in _CLONE_ALL_DEFAULT_EXCLUDE_ROOT:
|
||||
ignored.append(entry)
|
||||
except (OSError, ValueError):
|
||||
# ``resolve()`` can fail on unusual FS layouts (broken
|
||||
# symlinks, missing parents). Fail open — better to
|
||||
# over-copy than silently drop user data.
|
||||
pass
|
||||
try:
|
||||
at_root = Path(directory).resolve() == source_resolved
|
||||
except (OSError, ValueError):
|
||||
# ``resolve()`` can fail on unusual FS layouts (broken
|
||||
# symlinks, missing parents). Fail open — better to
|
||||
# over-copy than silently drop user data.
|
||||
at_root = False
|
||||
if at_root:
|
||||
# History artifacts: excluded for ANY source profile.
|
||||
if entry in _CLONE_ALL_HISTORY_EXCLUDE_ROOT:
|
||||
ignored.append(entry)
|
||||
continue
|
||||
# Infrastructure: only the default profile contains these.
|
||||
if is_default_source and entry in _CLONE_ALL_DEFAULT_EXCLUDE_ROOT:
|
||||
ignored.append(entry)
|
||||
return ignored
|
||||
|
||||
return _ignore
|
||||
|
||||
Reference in New Issue
Block a user