sterilize hot-fixes: 300s timeout + defensive string→dict parsing

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.
This commit is contained in:
Kayos 2026-04-30 12:41:58 -07:00
parent 6bcf79e5dc
commit d97fdbc407

View file

@ -533,12 +533,28 @@ class Sterilizer:
prompt=prompt, prompt=prompt,
model=self.model, model=self.model,
system=self._system_prompt(), system=self._system_prompt(),
timeout_secs=180, timeout_secs=300,
) )
except ForgeError as e: except ForgeError as e:
raise RuntimeError(f"clawdforge failed: {e}") from e raise RuntimeError(f"clawdforge failed: {e}") from e
result = resp.get("result") 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: if not isinstance(result, dict) or "parses" not in result:
raise RuntimeError(f"unexpected response shape: {str(result)[:200]}") raise RuntimeError(f"unexpected response shape: {str(result)[:200]}")