Skip to repository content94 lines · 3.3 KB · python
tenant.openagents/omega
No repository description is available.
OpenAgents Git authority 2026-07-28T04:31:08.204Z Public web read
NIP-34 coordinate
30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omegaMaintainersHidden in public view
References2 branches · 1 tag
Read-only clone
git clone https://openagents.com/git/tenant.openagents/omega.gitBrowse files
cleanup.py
1"""Prune stale build artifacts and build cache from the shared volume.
2
3Eval results are sacrosanct: this only ever touches the build-related top-level
4directories listed in `PRUNABLE_DIRS`. `runs/` (the eval outputs) is never in
5that set and is never walked, so a bug here cannot delete results.
6
7The logic is pure-filesystem (no Modal dependency) so it can be unit-tested
8against a temporary directory; `modal_app` wraps it with volume reload/commit and
9a daily schedule.
10"""
11
12from __future__ import annotations
13
14import datetime
15import shutil
16import time
17from pathlib import Path
18from typing import Any
19
20# The ONLY top-level volume directories cleanup may modify. Eval results live in
21# runs/, which is deliberately absent here.
22PRUNABLE_DIRS = frozenset({"builds", "build-locks", "tmp"})
23
24DEFAULT_BUILD_RETENTION_DAYS = 14.0
25DEFAULT_LOCK_TTL_HOURS = 6.0
26DEFAULT_TMP_MAX_AGE_HOURS = 24.0
27
28
29def _age_days(path: Path) -> float:
30 try:
31 return (time.time() - path.stat().st_mtime) / 86400.0
32 except OSError:
33 return 0.0
34
35
36def prune_artifacts(
37 data_root: Path | str,
38 *,
39 dry_run: bool = False,
40 build_retention_days: float = DEFAULT_BUILD_RETENTION_DAYS,
41 lock_ttl_hours: float = DEFAULT_LOCK_TTL_HOURS,
42 tmp_max_age_hours: float = DEFAULT_TMP_MAX_AGE_HOURS,
43) -> dict[str, Any]:
44 data_root = Path(data_root)
45 removed: dict[str, list[Any]] = {
46 "builds": [],
47 "locks": [],
48 "tmp": [],
49 }
50
51 # 1. Stale content-addressed builds, aged by their READY marker (or the dir
52 # itself for never-finished builds). Cheap to rebuild on demand.
53 builds_dir = data_root / "builds"
54 if builds_dir.is_dir():
55 for build_dir in sorted(builds_dir.iterdir()):
56 if not build_dir.is_dir():
57 continue
58 ready = build_dir / "READY"
59 age = _age_days(ready if ready.exists() else build_dir)
60 if age > build_retention_days:
61 removed["builds"].append(
62 {"build_id": build_dir.name, "age_days": round(age, 1)}
63 )
64 if not dry_run:
65 shutil.rmtree(build_dir, ignore_errors=True)
66
67 # 2. Orphaned single-flight build leases left by crashed builds.
68 locks_dir = data_root / "build-locks"
69 if locks_dir.is_dir():
70 for lock in sorted(locks_dir.glob("*.json")):
71 if _age_days(lock) * 24.0 > lock_ttl_hours:
72 removed["locks"].append(lock.name)
73 if not dry_run:
74 lock.unlink(missing_ok=True)
75
76 # 3. Orphaned temp build dirs from failed/raced builds.
77 tmp_builds = data_root / "tmp" / "builds"
78 if tmp_builds.is_dir():
79 for entry in sorted(tmp_builds.iterdir()):
80 if _age_days(entry) * 24.0 > tmp_max_age_hours:
81 removed["tmp"].append(entry.name)
82 if not dry_run:
83 if entry.is_dir():
84 shutil.rmtree(entry, ignore_errors=True)
85 else:
86 entry.unlink(missing_ok=True)
87
88 return {
89 "dry_run": dry_run,
90 "at": datetime.datetime.now(datetime.timezone.utc).isoformat(),
91 "removed": removed,
92 "counts": {key: len(value) for key, value in removed.items()},
93 }
94