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(