From e6e2688b5698d86e2355b1ac0965065d78fcc2fc Mon Sep 17 00:00:00 2001 From: Benjamin Admin Date: Mon, 27 Apr 2026 18:59:03 +0200 Subject: [PATCH] fix(pipeline): add idempotency guard to submit-pass0b endpoint Prevents duplicate batch submissions that caused ~$170 in extra costs. Refuses new submit if a batch was submitted in the last 10 minutes. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../api/control_generator_routes.py | 26 +++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/control-pipeline/api/control_generator_routes.py b/control-pipeline/api/control_generator_routes.py index a5f1c49..5a46979 100644 --- a/control-pipeline/api/control_generator_routes.py +++ b/control-pipeline/api/control_generator_routes.py @@ -2298,13 +2298,29 @@ class SubmitPass0bRequest(BaseModel): batch_size: int = 5 +_last_submit_batch_id: str = "" +_last_submit_time: float = 0 + + @router.post("/generate/submit-pass0b") async def submit_pass0b(req: SubmitPass0bRequest): """Submit Pass 0b batch to Anthropic Batch API. - Loads unprocessed obligations, applies pre-LLM filter, submits batch. - Returns batch_id for status polling and later result processing. + SAFETY: Refuses to submit if a batch was submitted in the last 10 minutes. + This prevents duplicate batches from curl retries or timeouts. """ + import time + global _last_submit_batch_id, _last_submit_time + + # Idempotency guard: refuse if last submit was <10 min ago + elapsed = time.time() - _last_submit_time + if elapsed < 600 and _last_submit_batch_id: + return { + "status": "blocked", + "reason": f"Batch {_last_submit_batch_id} was submitted {int(elapsed)}s ago. Wait {int(600 - elapsed)}s or use force=true.", + "last_batch_id": _last_submit_batch_id, + } + from services.decomposition_pass import DecompositionPass db = SessionLocal() try: @@ -2313,6 +2329,12 @@ async def submit_pass0b(req: SubmitPass0bRequest): limit=req.limit, batch_size=req.batch_size, ) + # Record successful submit + batch_id = result.get("batch_id", "") + if batch_id: + _last_submit_batch_id = batch_id + _last_submit_time = time.time() + logger.info("Submit guard: recorded batch %s", batch_id) return result except Exception as e: logger.error("Submit Pass 0b failed: %s", e)