From c3f0693b3bcdc455cfa030d766080b609cca4ee1 Mon Sep 17 00:00:00 2001 From: Kevin Mills Date: Thu, 30 Apr 2026 10:19:25 -0700 Subject: [PATCH] Tolerate unescaped control characters in AI JSON output json.loads(strict=False) allows literal newlines and tabs inside string values, a common LLM failure mode when emitting multi-line `issue` or `fix` fields without escaping. Previously a stray newline would discard the entire (paid-for) review with no way to recover the AI output. On final parse failure, dump the full AI output to /tmp/reviewd-failed-.txt so the review can be salvaged manually rather than losing all but the first 1000 chars to the error log. --- src/reviewd/reviewer.py | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/src/reviewd/reviewer.py b/src/reviewd/reviewer.py index 48ea082..2c70f28 100644 --- a/src/reviewd/reviewer.py +++ b/src/reviewd/reviewer.py @@ -475,16 +475,26 @@ def extract_json(output: str) -> dict: logger.error('No JSON block found in AI output. Last 500 chars:\n%s', tail) raise ValueError('No JSON block found in AI output') raw = matches[-1] + # strict=False permits literal control characters (newlines, tabs, CR) inside + # string values — a common LLM failure mode when emitting multi-line `issue` + # or `fix` fields without escaping. try: - return json.loads(raw) + return json.loads(raw, strict=False) except json.JSONDecodeError: # Strip trailing commas before } or ] (common LLM JSON error) and retry fixed = re.sub(r',\s*([}\]])', r'\1', raw) try: logger.warning('Fixed trailing commas in AI JSON output') - return json.loads(fixed) + return json.loads(fixed, strict=False) except json.JSONDecodeError as e: - logger.error('Malformed JSON in AI output: %s\nRaw JSON:\n%s', e, raw[:1000]) + # Dump the full AI output so the review can be salvaged manually + # rather than throwing away the entire (paid-for) call. + dump_path = Path(tempfile.gettempdir()) / f'reviewd-failed-{int(time.time())}.txt' + try: + dump_path.write_text(output) + logger.error('Malformed JSON in AI output: %s. Full output saved to %s', e, dump_path) + except OSError: + logger.error('Malformed JSON in AI output: %s\nRaw JSON:\n%s', e, raw[:1000]) raise ValueError(f'Malformed JSON in AI output: {e}') from e