From 30928b482f1d2dd1caac446173d0c6905db8ecd5 Mon Sep 17 00:00:00 2001 From: Kayos Date: Thu, 30 Apr 2026 17:55:13 -0700 Subject: [PATCH] =?UTF-8?q?sterilize:=20fix=20finalize=20WHERE=20=E2=80=94?= =?UTF-8?q?=20allow=20review=E2=86=92applying=E2=86=92done=20transitions?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bug: my anti-zombie guard from 4707e6a was too strict — WHERE clause required state IN ('running','applying') to update. But the normal flow goes running→review→applying→done. Once a job entered review, NO state transition could fire — including the legitimate apply sequence triggered by user clicking "apply selected". Symptom Cobb hit: clicked apply on job 6, the daemon thread did the work (11 of 13 proposals applied cleanly to Mealie), but the row stayed at state='review' so the UI never moved off the review screen. The 11 successful applies are real — Mealie has the updated recipeIngredient food links. The bookkeeping just didn't follow. Fix: change WHERE clause from a positive whitelist (running/applying) to a negative blocklist (NOT IN done/failed/cancelled). This still prevents the original failure mode (daemon overwriting a user-cancelled job) — terminal states still can't be overwritten — but lets review transition to applying when the user approves. Same fix applied to finalize_consolidate_job since it copy-pasted the same too-strict guard. --- cauldron/db.py | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/cauldron/db.py b/cauldron/db.py index f95e70f..71c85f5 100644 --- a/cauldron/db.py +++ b/cauldron/db.py @@ -972,13 +972,14 @@ class DB: ) def finalize_sterilize_job(self, job_id: int, *, state: str) -> None: - """Move job to a terminal state (review/done/failed/cancelled). + """Move job to a new state. Will NOT overwrite a terminal state + (done / failed / cancelled) — that's the anti-zombie guard that + keeps user cancels from being silently replaced when the daemon + thread limps to the finish line. - Will NOT overwrite a job that's already terminal — if a runner is - about to call finalize('done') but the row was set to 'cancelled' - externally, we leave the cancellation in place. This is the - anti-zombie guard that keeps user cancels from being silently - replaced when the daemon thread limps to the finish line.""" + Allowed source states: running, applying, review. The review state + is part of the normal flow (walk done → review → user approves + → applying), so transitions out of review must work.""" with self.conn() as c, c.cursor() as cur: cur.execute( """ @@ -989,7 +990,7 @@ class DB: last_progress_at = NOW(), current_slug = NULL WHERE id=%s - AND state IN ('running','applying') + AND state NOT IN ('done','failed','cancelled') """, (state, state, job_id), ) @@ -1169,6 +1170,9 @@ class DB: ) def finalize_consolidate_job(self, job_id: int, *, state: str) -> None: + """Same anti-zombie guard as finalize_sterilize_job — won't overwrite + a terminal state (done/failed/cancelled), but allows the normal + running→review→applying→done flow.""" with self.conn() as c, c.cursor() as cur: cur.execute( """UPDATE cauldron_consolidate_jobs @@ -1178,7 +1182,7 @@ class DB: last_progress_at = NOW(), current_cluster = NULL WHERE id=%s - AND state IN ('running','applying')""", + AND state NOT IN ('done','failed','cancelled')""", (state, state, job_id), )