Exclude Time Travel history from backups

Skip usr/.time_travel during self-update user-data backups and add the same Time Travel shadow-history exclusion to default BackupService metadata.\n\nAdd focused regressions proving regular user files stay in backup coverage while Time Travel shadow repositories are omitted.
This commit is contained in:
Alessandro 2026-07-09 13:27:03 +02:00
parent bc96386260
commit 3d87998821
6 changed files with 63 additions and 0 deletions

View file

@ -22,6 +22,7 @@
- Do not bake secrets, local `.env` values, or user data into the image.
- Runtime startup must ensure `/a0/usr/uploads` exists before supervised services start.
- Runtime startup raises the soft open-file limit toward `A0_NOFILE_LIMIT` (default `65535`) before supervisord starts, bounded by the container hard limit.
- Self-update user-data backups skip Time Travel shadow history under `usr/.time_travel/` and transient Desktop agent state.
## Work Guidance

View file

@ -405,6 +405,11 @@ def should_exclude_from_usr_backup(
logger: AttemptLogger,
) -> bool:
parts = relative_dir.parts
if parts and parts[0] == ".time_travel":
logger.log(
f"Skipping Time Travel history during usr backup: {Path('usr') / relative_dir}"
)
return True
if (
len(parts) >= 6
and parts[0] == "plugins"

View file

@ -63,6 +63,7 @@ class BackupService:
return f"""# User data
# All persistent user data is now centralized in /usr for easier backup and restore
{agent_root}/usr/**
!{agent_root}/usr/.time_travel/**
"""
def _get_agent_zero_version(self) -> str:

View file

@ -26,6 +26,7 @@
- Observed side-effect areas: filesystem reads, filesystem writes, filesystem deletion, settings/state persistence, secret handling.
- Imported dependency areas include: `datetime`, `helpers`, `helpers.localization`, `helpers.print_style`, `json`, `os`, `pathspec`, `platform`, `tempfile`, `typing`, `zipfile`.
- `test_patterns(..., max_files=None)` is the unlimited scan mode. UI preview and dry-run callers may pass bounded limits, but real backup creation and restore clean-before-restore must use unlimited matching so archives and cleanup are not silently truncated.
- Default backup metadata includes persistent `/usr` data but excludes Time Travel shadow history under `usr/.time_travel/**`.
## Key Concepts

View file

@ -16,6 +16,28 @@ class UploadedBackup:
shutil.copyfile(self.path, target)
@pytest.mark.asyncio
async def test_default_backup_patterns_exclude_time_travel_history(tmp_path):
root = tmp_path / "a0"
usr = root / "usr"
time_travel = usr / ".time_travel" / "workspaces" / "demo" / "repo.git"
time_travel.mkdir(parents=True)
(usr / "settings.json").write_text('{"ok": true}\n', encoding="utf-8")
(time_travel / "objects.pack").write_text("history\n", encoding="utf-8")
service = BackupService()
service.agent_zero_root = str(root)
service.base_paths = {str(root): str(root)}
metadata = service.get_default_backup_metadata()
files = await service.test_patterns(metadata, max_files=None)
paths = {item["real_path"] for item in files}
assert str(usr / "settings.json") in paths
assert str(time_travel / "objects.pack") not in paths
assert f"{root}/usr/.time_travel/**" in metadata["exclude_patterns"]
@pytest.mark.asyncio
async def test_pattern_scan_can_run_without_file_limit(tmp_path):
root = tmp_path / "a0"

View file

@ -829,6 +829,39 @@ def test_self_update_manager_usr_backup_skips_runtime_sockets():
)
def test_self_update_manager_usr_backup_skips_time_travel_history(tmp_path):
manager = load_self_update_manager()
repo_dir = tmp_path / "repo"
usr_dir = repo_dir / "usr"
time_travel = usr_dir / ".time_travel" / "workspaces" / "demo" / "repo.git"
time_travel.mkdir(parents=True)
(usr_dir / "settings.json").write_text('{"ok": true}\n', encoding="utf-8")
(time_travel / "objects.pack").write_text("history\n", encoding="utf-8")
messages = []
class ListLogger:
def log(self, message=""):
messages.append(message)
backup_path = manager.create_usr_backup(
repo_dir=repo_dir,
backup_path=str(tmp_path / "backups"),
backup_name="usr-backup.zip",
conflict_policy="rename",
logger=ListLogger(),
)
with zipfile.ZipFile(backup_path) as archive:
names = set(archive.namelist())
assert "usr/settings.json" in names
assert "usr/.time_travel/workspaces/demo/repo.git/objects.pack" not in names
assert any(
"Skipping Time Travel history during usr backup: usr/.time_travel" in message
for message in messages
)
def test_self_update_manager_usr_backup_skips_transient_desktop_ssh_agent_dir(tmp_path):
manager = load_self_update_manager()
repo_dir = tmp_path / "repo"