From d33ddd9f3d35005f3aead0c5ee5b7b784377b688 Mon Sep 17 00:00:00 2001 From: m3taversal Date: Tue, 24 Mar 2026 14:37:50 +0000 Subject: [PATCH] fix: fixer GC now closes PRs on Forgejo + deletes branches, not just DB MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Root cause of 5-day pipeline stall: fixer GC marked PRs as closed in DB but never synced to Forgejo. Branches stayed alive on remote, blocking Gate 2 in batch-extract (branch exists → skip forever). Now: GC fetches PR numbers, posts audit comment, closes on Forgejo, deletes remote branch, THEN updates DB. Same pattern as _terminate_pr in evaluate.py. Pentagon-Agent: Epimetheus <3D35839A-7722-4740-B93D-51157F7D5E70> --- lib/fixer.py | 30 ++++++++++++++++++++++++------ 1 file changed, 24 insertions(+), 6 deletions(-) diff --git a/lib/fixer.py b/lib/fixer.py index d5eda70..c08f186 100644 --- a/lib/fixer.py +++ b/lib/fixer.py @@ -225,15 +225,33 @@ async def fix_cycle(conn, max_workers=None) -> tuple[int, int]: # Garbage collection: close PRs with exhausted fix budget that are stuck in open. # These were evaluated, rejected, fixer couldn't help, nobody closes them. # (Epimetheus session 2 — prevents zombie PR accumulation) - _gc = conn.execute( - """UPDATE prs SET status = 'closed', last_error = 'fix budget exhausted — auto-closed' + # Bug fix: must also close on Forgejo + delete branch, not just DB update. + # DB-only close caused Forgejo/DB state divergence — branches stayed alive, + # blocking Gate 2 in batch-extract for 5 days. (Epimetheus session 4) + gc_rows = conn.execute( + """SELECT number, branch FROM prs WHERE status = 'open' AND fix_attempts >= ? AND (domain_verdict = 'request_changes' OR leo_verdict = 'request_changes')""", - (config.MAX_FIX_ATTEMPTS + 2,), # GC threshold = mechanical + substantive budget - ) - if _gc.rowcount > 0: - logger.info("GC: closed %d exhausted PRs", _gc.rowcount) + (config.MAX_FIX_ATTEMPTS + 2,), + ).fetchall() + if gc_rows: + from .forgejo import api as _gc_forgejo, repo_path as _gc_repo_path + for row in gc_rows: + pr_num, branch = row["number"], row["branch"] + try: + await _gc_forgejo("POST", _gc_repo_path(f"issues/{pr_num}/comments"), + {"body": "Auto-closed: fix budget exhausted. Source will be re-extracted."}) + await _gc_forgejo("PATCH", _gc_repo_path(f"pulls/{pr_num}"), {"state": "closed"}) + if branch: + await _gc_forgejo("DELETE", _gc_repo_path(f"branches/{branch}")) + except Exception as e: + logger.warning("GC: failed to close PR #%d on Forgejo: %s", pr_num, e) + conn.execute( + "UPDATE prs SET status = 'closed', last_error = 'fix budget exhausted — auto-closed' WHERE number = ?", + (pr_num,), + ) + logger.info("GC: closed %d exhausted PRs (DB + Forgejo + branch cleanup)", len(gc_rows)) batch_limit = min(max_workers or config.MAX_FIX_PER_CYCLE, config.MAX_FIX_PER_CYCLE)