Clean uv cache during self-update
Run a best-effort uv cache clean when the durable self-update manager consumes an update request. Cover uv-present, uv-missing, and cleanup-failure paths with focused tests, and document the new self-update step.
Alessandro committed
May 8, 2026 at 00:56 UTC
1af5344f3c6f85c9dac1136af2c910db137cb6f0
3 files changed
+91
-5
docker/run/fs/exe/self_update_manager.py
+19
@@ -408,6 +408,24 @@ def run_command(
408
return completed
409
410
411
+def clean_uv_cache(logger: AttemptLogger) -> None:
412
+ uv_path = shutil.which("uv")
413
+ if not uv_path:
414
+ logger.log("uv executable not found, skipping uv cache clean.")
415
+ return
416
+
417
+ logger.log("Cleaning uv cache before continuing self-update startup.")
418
+ try:
419
+ run_command(
420
+ [uv_path, "cache", "clean"],
421
+ cwd=None,
422
+ logger=logger,
423
+ error_message="Failed to clean uv cache during self-update.",
424
+ )
425
+ except Exception as exc:
426
+ logger.log(f"uv cache clean skipped after error: {exc}")
427
+
428
+
429
def has_local_rollback_changes(repo_dir: Path) -> bool:
430
status = git_output(repo_dir, "status", "--porcelain=v1", "--untracked-files=all")
431
return bool(status.strip())
@@ -1180,6 +1198,7 @@ def docker_run_ui() -> int:
1198
logger.reset()
1199
logger.log(f"Consumed update file at {TRIGGER_FILE}")
1200
logger.log_block("Trigger file content", raw_text)
1201
+ clean_uv_cache(logger)
1202
1203
try:
1204
current = get_repo_version_info(REPO_DIR)
docs/guides/self-update.md
+6
-5
@@ -21,11 +21,12 @@ Agent Zero includes a Docker-oriented self-update flow for switching to a specif
21
1. The WebUI writes a YAML request file outside `/a0` so the request survives upgrades and downgrades.
22
2. Agent Zero restarts.
23
3. The durable updater in `/exe` reads the YAML request before starting the UI.
24
-4. If requested, it creates a zip backup of `/a0/usr`.
25
-5. It fetches the requested branch and update target from the official Agent Zero repository.
26
-6. It updates `/a0` while preserving gitignored paths such as `/a0/usr`.
27
-7. It starts Agent Zero again and waits for `/api/health` to become healthy.
28
-8. If the UI does not become healthy within the allowed time, it restores the previous checkout and starts that version again.
24
+4. It cleans the root `uv` cache when `uv` is available.
25
+5. If requested, it creates a zip backup of `/a0/usr`.
26
+6. It fetches the requested branch and update target from the official Agent Zero repository.
27
+7. It updates `/a0` while preserving gitignored paths such as `/a0/usr`.
28
+8. It starts Agent Zero again and waits for `/api/health` to become healthy.
29
+9. If the UI does not become healthy within the allowed time, it restores the previous checkout and starts that version again.
30
31
## Durable files
32
tests/test_self_update_tag_filter.py
+66
@@ -781,6 +781,72 @@ def test_self_update_manager_usr_backup_skips_broken_symlinks(tmp_path):
781
assert "usr/workdir/reachy-mini-mcp/.venv/bin/python" not in names
782
783
784
+def test_self_update_manager_clean_uv_cache_uses_uv_when_available(monkeypatch):
785
+ manager = load_self_update_manager()
786
+ commands = []
787
+ monkeypatch.setattr(
788
+ manager.shutil,
789
+ "which",
790
+ lambda executable: "/usr/local/bin/uv" if executable == "uv" else None,
791
+ )
792
+
793
+ def fake_run_command(command, *, cwd, logger, error_message=None):
794
+ commands.append((command, cwd, error_message))
795
+
796
+ monkeypatch.setattr(manager, "run_command", fake_run_command)
797
+
798
+ manager.clean_uv_cache(manager.NullLogger())
799
+
800
+ assert commands == [
801
+ (
802
+ ["/usr/local/bin/uv", "cache", "clean"],
803
+ None,
804
+ "Failed to clean uv cache during self-update.",
805
+ )
806
+ ]
807
+
808
+
809
+def test_self_update_manager_clean_uv_cache_skips_when_uv_missing(monkeypatch):
810
+ manager = load_self_update_manager()
811
+ commands = []
812
+ monkeypatch.setattr(manager.shutil, "which", lambda executable: None)
813
+ monkeypatch.setattr(
814
+ manager,
815
+ "run_command",
816
+ lambda command, **kwargs: commands.append(command),
817
+ )
818
+
819
+ manager.clean_uv_cache(manager.NullLogger())
820
+
821
+ assert commands == []
822
+
823
+
824
+def test_self_update_manager_clean_uv_cache_is_best_effort(monkeypatch):
825
+ manager = load_self_update_manager()
826
+ messages = []
827
+ monkeypatch.setattr(
828
+ manager.shutil,
829
+ "which",
830
+ lambda executable: "/usr/local/bin/uv" if executable == "uv" else None,
831
+ )
832
+
833
+ class Logger:
834
+ def log(self, message=""):
835
+ messages.append(message)
836
+
837
+ def log_block(self, title, content):
838
+ return None
839
+
840
+ def fail_run_command(command, **kwargs):
841
+ raise RuntimeError("cache cleanup failed")
842
+
843
+ monkeypatch.setattr(manager, "run_command", fail_run_command)
844
+
845
+ manager.clean_uv_cache(Logger())
846
+
847
+ assert any("uv cache clean skipped after error" in message for message in messages)
848
+
849
+
850
def test_self_update_manager_latest_on_main_uses_current_major_release(monkeypatch):
851
manager = load_self_update_manager()
852
monkeypatch.setattr(