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.
Alessandro committed
Jul 9, 2026 at 13:27 UTC
3d87998821202dd5c9ac8ad18eb1782a8e2e247c
6 files changed
+63
docker/run/AGENTS.md
+1
@@ -22,6 +22,7 @@
22
- Do not bake secrets, local `.env` values, or user data into the image.
23
- Runtime startup must ensure `/a0/usr/uploads` exists before supervised services start.
24
- Runtime startup raises the soft open-file limit toward `A0_NOFILE_LIMIT` (default `65535`) before supervisord starts, bounded by the container hard limit.
25
+- Self-update user-data backups skip Time Travel shadow history under `usr/.time_travel/` and transient Desktop agent state.
26
27
## Work Guidance
28
docker/run/fs/exe/self_update_manager.py
+5
@@ -405,6 +405,11 @@ def should_exclude_from_usr_backup(
405
logger: AttemptLogger,
406
) -> bool:
407
parts = relative_dir.parts
408
+ if parts and parts[0] == ".time_travel":
409
+ logger.log(
410
+ f"Skipping Time Travel history during usr backup: {Path('usr') / relative_dir}"
411
+ )
412
+ return True
413
if (
414
len(parts) >= 6
415
and parts[0] == "plugins"
helpers/backup.py
+1
@@ -63,6 +63,7 @@ class BackupService:
63
return f"""# User data
64
# All persistent user data is now centralized in /usr for easier backup and restore
65
{agent_root}/usr/**
66
+!{agent_root}/usr/.time_travel/**
67
"""
68
69
def _get_agent_zero_version(self) -> str:
helpers/backup.py.dox.md
+1
@@ -26,6 +26,7 @@
26
- Observed side-effect areas: filesystem reads, filesystem writes, filesystem deletion, settings/state persistence, secret handling.
27
- Imported dependency areas include: `datetime`, `helpers`, `helpers.localization`, `helpers.print_style`, `json`, `os`, `pathspec`, `platform`, `tempfile`, `typing`, `zipfile`.
28
- `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.
29
+- Default backup metadata includes persistent `/usr` data but excludes Time Travel shadow history under `usr/.time_travel/**`.
30
31
## Key Concepts
32
tests/test_backup_large_archives.py
+22
@@ -16,6 +16,28 @@ class UploadedBackup:
16
shutil.copyfile(self.path, target)
17
18
19
+@pytest.mark.asyncio
20
+async def test_default_backup_patterns_exclude_time_travel_history(tmp_path):
21
+ root = tmp_path / "a0"
22
+ usr = root / "usr"
23
+ time_travel = usr / ".time_travel" / "workspaces" / "demo" / "repo.git"
24
+ time_travel.mkdir(parents=True)
25
+ (usr / "settings.json").write_text('{"ok": true}\n', encoding="utf-8")
26
+ (time_travel / "objects.pack").write_text("history\n", encoding="utf-8")
27
+
28
+ service = BackupService()
29
+ service.agent_zero_root = str(root)
30
+ service.base_paths = {str(root): str(root)}
31
+ metadata = service.get_default_backup_metadata()
32
+
33
+ files = await service.test_patterns(metadata, max_files=None)
34
+ paths = {item["real_path"] for item in files}
35
+
36
+ assert str(usr / "settings.json") in paths
37
+ assert str(time_travel / "objects.pack") not in paths
38
+ assert f"{root}/usr/.time_travel/**" in metadata["exclude_patterns"]
39
+
40
+
41
@pytest.mark.asyncio
42
async def test_pattern_scan_can_run_without_file_limit(tmp_path):
43
root = tmp_path / "a0"
tests/test_self_update_tag_filter.py
+33
@@ -829,6 +829,39 @@ def test_self_update_manager_usr_backup_skips_runtime_sockets():
829
)
830
831
832
+def test_self_update_manager_usr_backup_skips_time_travel_history(tmp_path):
833
+ manager = load_self_update_manager()
834
+ repo_dir = tmp_path / "repo"
835
+ usr_dir = repo_dir / "usr"
836
+ time_travel = usr_dir / ".time_travel" / "workspaces" / "demo" / "repo.git"
837
+ time_travel.mkdir(parents=True)
838
+ (usr_dir / "settings.json").write_text('{"ok": true}\n', encoding="utf-8")
839
+ (time_travel / "objects.pack").write_text("history\n", encoding="utf-8")
840
+ messages = []
841
+
842
+ class ListLogger:
843
+ def log(self, message=""):
844
+ messages.append(message)
845
+
846
+ backup_path = manager.create_usr_backup(
847
+ repo_dir=repo_dir,
848
+ backup_path=str(tmp_path / "backups"),
849
+ backup_name="usr-backup.zip",
850
+ conflict_policy="rename",
851
+ logger=ListLogger(),
852
+ )
853
+
854
+ with zipfile.ZipFile(backup_path) as archive:
855
+ names = set(archive.namelist())
856
+
857
+ assert "usr/settings.json" in names
858
+ assert "usr/.time_travel/workspaces/demo/repo.git/objects.pack" not in names
859
+ assert any(
860
+ "Skipping Time Travel history during usr backup: usr/.time_travel" in message
861
+ for message in messages
862
+ )
863
+
864
+
865
def test_self_update_manager_usr_backup_skips_transient_desktop_ssh_agent_dir(tmp_path):
866
manager = load_self_update_manager()
867
repo_dir = tmp_path / "repo"