From d018c769bb3d69d1af0f6d87e0aa2a6e45a6a874 Mon Sep 17 00:00:00 2001 From: Alexander Whitestone Date: Wed, 15 Apr 2026 03:21:06 +0000 Subject: [PATCH] fix: add gate file rotation to prevent unbounded directory growth Closes #674, closes #628 - Age-based cleanup: deletes files older than 7 days - Count-based cap: keeps at most 50 historical files - Always preserves eval_gate_latest.json - Runs automatically after each evaluate_candidate() call --- bin/soul_eval_gate.py | 50 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/bin/soul_eval_gate.py b/bin/soul_eval_gate.py index dbf545cd..10fc158b 100644 --- a/bin/soul_eval_gate.py +++ b/bin/soul_eval_gate.py @@ -33,6 +33,11 @@ from pathlib import Path from typing import Optional +# ── Gate File Rotation ────────────────────────────────────────────── +GATE_FILE_MAX_AGE_DAYS = 7 +GATE_FILE_MAX_COUNT = 50 + + # ── SOUL.md Constraints ────────────────────────────────────────────── # # These are the non-negotiable categories from SOUL.md and the @@ -240,6 +245,9 @@ def evaluate_candidate( latest_file = gate_dir / "eval_gate_latest.json" latest_file.write_text(json.dumps(result, indent=2)) + # Rotate old gate files to prevent unbounded growth + _rotate_gate_files(gate_dir) + return result @@ -249,6 +257,48 @@ def _load_json(path: str | Path) -> dict: return json.loads(Path(path).read_text()) +def _rotate_gate_files(gate_dir: Path) -> None: + """Clean up old gate files to prevent unbounded directory growth. + + - Deletes files older than GATE_FILE_MAX_AGE_DAYS + - Caps total count at GATE_FILE_MAX_COUNT (oldest first) + - Always preserves eval_gate_latest.json + """ + if not gate_dir.exists(): + return + + latest_name = "eval_gate_latest.json" + cutoff = datetime.now(timezone.utc).timestamp() - (GATE_FILE_MAX_AGE_DAYS * 86400) + + gate_files = [] + for f in gate_dir.iterdir(): + if f.name == latest_name or not f.name.startswith("eval_gate_") or f.suffix != ".json": + continue + try: + mtime = f.stat().st_mtime + except OSError: + continue + gate_files.append((mtime, f)) + + # Sort oldest first + gate_files.sort(key=lambda x: x[0]) + + deleted = 0 + for mtime, f in gate_files: + should_delete = False + if mtime < cutoff: + should_delete = True + elif len(gate_files) - deleted > GATE_FILE_MAX_COUNT: + should_delete = True + + if should_delete: + try: + f.unlink() + deleted += 1 + except OSError: + pass + + def _find_category_score( sessions: dict[str, dict], category: str,