From d97fdbc4077fb3cb087c8349209d2b1cf8539023 Mon Sep 17 00:00:00 2001 From: Kayos Date: Thu, 30 Apr 2026 12:41:58 -0700 Subject: [PATCH] =?UTF-8?q?sterilize=20hot-fixes:=20300s=20timeout=20+=20d?= =?UTF-8?q?efensive=20string=E2=86=92dict=20parsing?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two failures surfaced by job 6 with the bigger prompt (full Mealie food catalog ~50KB + recipe context with steps + spell-cleanup rules): 1. quinoa-chili-with-sweet-potatoes: 180s timeout. The bigger prompt means Sonnet has more to chew through per call. Bumped _parse_batch timeout 180s → 300s. Recipes with many ingredients now get more slack before clawdforge gives up. 2. salmon-sushi-bake: "unexpected response shape" — Sonnet returned the JSON as a STRING rather than a parsed dict (depends on size + how clawdforge unwraps the response). _parse_batch was strictly requiring isinstance(result, dict) and rejecting strings outright, leaving the recipe in error state with valid JSON visible in the error message. Added defensive string→dict parsing (with optional code-fence stripping) mirroring the pattern already used by forge._extract_food_info. Both errored recipes from job 6 can now be re-run cleanly. Apply path unchanged — defensive food.id preservation from 6bcf79e still in effect. --- cauldron/sterilizer.py | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/cauldron/sterilizer.py b/cauldron/sterilizer.py index f1554eb..1a9cc0e 100644 --- a/cauldron/sterilizer.py +++ b/cauldron/sterilizer.py @@ -533,12 +533,28 @@ class Sterilizer: prompt=prompt, model=self.model, system=self._system_prompt(), - timeout_secs=180, + timeout_secs=300, ) except ForgeError as e: raise RuntimeError(f"clawdforge failed: {e}") from e result = resp.get("result") + # Sonnet sometimes returns the JSON as a string rather than a parsed + # dict (depends on output size + clawdforge's parsing). Defensively + # parse if so, mirroring forge._extract_food_info's pattern. + if isinstance(result, str): + try: + stripped = result.strip() + # Strip optional code fences + if stripped.startswith("```"): + import re as _re + stripped = _re.sub(r"^```(?:json)?\s*", "", stripped) + stripped = _re.sub(r"\s*```$", "", stripped) + result = json.loads(stripped) + except Exception: + # Fall through to the shape-check below; will raise with + # the (truncated) original string for debugging + pass if not isinstance(result, dict) or "parses" not in result: raise RuntimeError(f"unexpected response shape: {str(result)[:200]}")