Sonnet sometimes emits JSON with unescaped double quotes inside string values:
{"finding":"The comment says OTP_VERSION is the major version (e.g. "28")"}
This breaks json.Unmarshal with invalid character '2' after object key:value pair.
Seen in gargoyle CI (sonnet-review on PR #551, job 3173).
Fix
Add a repairJSON fallback in ParseResponse. When the initial parse fails, the repair function walks the JSON character-by-character, tracking string state, and escapes any interior quotes that aren't followed by structural characters (,, }, ], :).
Tests
TestParseResponse_UnescapedQuotesInStrings — reproduces the exact CI failure
TestRepairJSON_NoOpOnValid — confirms no modification of valid JSON
## Problem
Sonnet sometimes emits JSON with unescaped double quotes inside string values:
```json
{"finding": "The comment says OTP_VERSION is the major version (e.g. "28")"}
```
This breaks `json.Unmarshal` with `invalid character '2' after object key:value pair`.
Seen in gargoyle CI (sonnet-review on PR #551, job 3173).
## Fix
Add a `repairJSON` fallback in `ParseResponse`. When the initial parse fails, the repair function walks the JSON character-by-character, tracking string state, and escapes any interior quotes that aren't followed by structural characters (`,`, `}`, `]`, `:`).
## Tests
- `TestParseResponse_UnescapedQuotesInStrings` — reproduces the exact CI failure
- `TestRepairJSON_NoOpOnValid` — confirms no modification of valid JSON
- `TestRepairJSON_FixesUnescapedQuotes` — verifies repair produces parseable output
LLMs (especially Sonnet) sometimes emit JSON with unescaped double
quotes inside string values, e.g. (e.g. "28") instead of properly
escaping them. This caused parse failures in CI.
Add a repairJSON fallback that uses a character-by-character scanner
to identify interior quotes (those not followed by structural JSON
characters) and escape them before retrying the parse.
Fixes sonnet-review failures on gargoyle PR #551.
Review: fix: repair unescaped quotes in LLM JSON responses
The fix addresses a real, observed failure mode (Sonnet emitting unescaped quotes inside JSON string values), and the structural approach — character-by-character walking with string-state tracking — is the right level of tool for this problem. The core algorithm is sound for the common case the PR targets. However, there are two correctness gaps that will silently produce broken output in edge cases, and the test coverage only covers the happy path for the tricky scenario.
🔴 Bug: Interior quoted word immediately before a comma
File:review/parser.go, repairJSON
The lookahead heuristic treats a " followed by , (after optional whitespace) as a structural closing quote. This misclassifies an interior quote that happens to precede a comma within the string value.
Reproducer (verified — this test currently fails):
input:=`{"msg": "say "yes", and go"}`repaired:=repairJSON(input)// repaired: {"msg": "say \"yes", and go"}// json.Unmarshal → error: invalid character 'a' looking for beginning of object key string
The repair function correctly escapes the opening quote before yes, but then sees the closing " after yes — followed by , — and treats it as the structural end-of-string. The remainder , and go"} is then outside any string context, producing malformed JSON.
The same failure class applies when an interior quoted word ends just before ], since ] is also in the structural lookahead set.
Root cause:,, }, ], and : are all valid string content (code snippets, key-value descriptions, prose). They cannot reliably distinguish a structural closing quote from an interior one.
🔴 Bug: JSON-shaped syntax in string values
When an LLM describes a struct or map literal in a finding (e.g. "input map {"key": "val"} caused error"), the " after val has } as lookahead and will be treated as a structural close, ending the outer string prematurely.
Given that this tool reviews code, and the LLMs it calls regularly describe map/struct literals, JSON blobs, or key-value pairs in finding text, this is a realistic failure scenario.
🟡 Test coverage misses the known-failing edge case
TestRepairJSON_FixesUnescapedQuotes uses {"msg": "use "foo" here"} — the interior quotes aren't adjacent to any structural character, so this passes cleanly. TestParseResponse_UnescapedQuotesInStrings also passes because the interior "28" is followed by ), not , or }.
Neither test covers the comma-adjacency failure. The following test currently fails with code as written:
funcTestRepairJSON_InteriorQuoteBeforeComma(t*testing.T){input:=`{"msg": "say "yes", and go"}`result:=repairJSON(input)varmmap[string]interface{}iferr:=json.Unmarshal([]byte(result),&m);err!=nil{t.Fatalf("repaired JSON should parse, got: %v\nrepaired: %s",err,result)}}
✅ What works well
Escape sequence passthrough (\ handling) is correct — pre-existing escapes are preserved without double-escaping.
Rune-level walking avoids byte-level bugs with multibyte content in string values.
strings.Builder with Grow — idiomatic and allocation-efficient.
Original error preserved in the fallback return — diagnostics remain actionable when repair also fails.
No-op on valid JSON — confirmed by TestRepairJSON_NoOpOnValid.
The "try repair as fallback" pattern is architecturally correct: if repair itself fails, the original first-parse error still propagates.
Summary
The algorithm correctly handles the exact CI failure that motivated this PR ((e.g. "28") — interior quotes not adjacent to structural characters). For the most common LLM unescaped-quote pattern it works. But the lookahead heuristic has a fundamental ambiguity: ,, }, ], and : are valid string content, so a quoted word ending just before one of them causes the algorithm to close the string prematurely — producing output more broken than the input.
Requesting changes on the two correctness bugs. If the intent is "handles the observed pattern, not a general-purpose JSON repair," the test suite should explicitly cover the known edge cases (including currently failing ones) so future maintainers understand the boundaries.
— sonnet-review-bot
## Review: `fix: repair unescaped quotes in LLM JSON responses`
The fix addresses a real, observed failure mode (Sonnet emitting unescaped quotes inside JSON string values), and the structural approach — character-by-character walking with string-state tracking — is the right level of tool for this problem. The core algorithm is sound for the **common case** the PR targets. However, there are two correctness gaps that will silently produce broken output in edge cases, and the test coverage only covers the happy path for the tricky scenario.
---
### 🔴 Bug: Interior quoted word immediately before a comma
**File:** `review/parser.go`, `repairJSON`
The lookahead heuristic treats a `"` followed by `,` (after optional whitespace) as a structural closing quote. This misclassifies an interior quote that happens to precede a comma within the string value.
**Reproducer (verified — this test currently fails):**
```go
input := `{"msg": "say "yes", and go"}`
repaired := repairJSON(input)
// repaired: {"msg": "say \"yes", and go"}
// json.Unmarshal → error: invalid character 'a' looking for beginning of object key string
```
The repair function correctly escapes the opening quote before `yes`, but then sees the closing `"` after `yes` — followed by `,` — and treats it as the structural end-of-string. The remainder `, and go"}` is then outside any string context, producing malformed JSON.
The same failure class applies when an interior quoted word ends just before `]`, since `]` is also in the structural lookahead set.
**Root cause:** `,`, `}`, `]`, and `:` are all valid string content (code snippets, key-value descriptions, prose). They cannot reliably distinguish a structural closing quote from an interior one.
---
### 🔴 Bug: JSON-shaped syntax in string values
When an LLM describes a struct or map literal in a finding (e.g. `"input map {"key": "val"} caused error"`), the `"` after `val` has `}` as lookahead and will be treated as a structural close, ending the outer string prematurely.
Given that this tool reviews code, and the LLMs it calls regularly describe map/struct literals, JSON blobs, or key-value pairs in finding text, this is a realistic failure scenario.
---
### 🟡 Test coverage misses the known-failing edge case
`TestRepairJSON_FixesUnescapedQuotes` uses `{"msg": "use "foo" here"}` — the interior quotes aren't adjacent to any structural character, so this passes cleanly. `TestParseResponse_UnescapedQuotesInStrings` also passes because the interior `"28"` is followed by `)`, not `,` or `}`.
Neither test covers the comma-adjacency failure. The following test currently **fails** with code as written:
```go
func TestRepairJSON_InteriorQuoteBeforeComma(t *testing.T) {
input := `{"msg": "say "yes", and go"}`
result := repairJSON(input)
var m map[string]interface{}
if err := json.Unmarshal([]byte(result), &m); err != nil {
t.Fatalf("repaired JSON should parse, got: %v\nrepaired: %s", err, result)
}
}
```
---
### ✅ What works well
- **Escape sequence passthrough** (`\` handling) is correct — pre-existing escapes are preserved without double-escaping.
- **Rune-level walking** avoids byte-level bugs with multibyte content in string values.
- **`strings.Builder` with `Grow`** — idiomatic and allocation-efficient.
- **Original error preserved** in the fallback `return` — diagnostics remain actionable when repair also fails.
- **No-op on valid JSON** — confirmed by `TestRepairJSON_NoOpOnValid`.
- The "try repair as fallback" pattern is architecturally correct: if repair itself fails, the original first-parse error still propagates.
---
### Summary
The algorithm correctly handles the exact CI failure that motivated this PR (`(e.g. "28")` — interior quotes not adjacent to structural characters). For the most common LLM unescaped-quote pattern it works. But the lookahead heuristic has a fundamental ambiguity: `,`, `}`, `]`, and `:` are valid string content, so a quoted word ending just before one of them causes the algorithm to close the string prematurely — producing output *more* broken than the input.
Requesting changes on the two correctness bugs. If the intent is "handles the observed pattern, not a general-purpose JSON repair," the test suite should explicitly cover the known edge cases (including currently failing ones) so future maintainers understand the boundaries.
— sonnet-review-bot
Rewrites the JSON repair algorithm to address two correctness bugs
identified in code review:
1. Interior quoted word before comma: "say "yes", and go" was
misidentified as structural close because "," followed the quote.
2. JSON-shaped content in strings: {"key": "val"} inside a string
value was being parsed as actual JSON structure.
The new approach:
- Distinguishes keys from values (only values need repair)
- Uses first-valid-candidate scan with deep lookahead
- Verifies that after a candidate close, the continuation is not just
a structural char but a complete valid JSON pattern
- Validates keyword tokens (true/false/null) fully, not just first char
- Checks container closes recursively for valid continuation
Adds comprehensive tests for all reported edge cases plus a complex
combined scenario with nested JSON-like content, quoted words before
commas, and multiple failure modes in one string.
The review was submitted against an earlier version of the algorithm. The current code already resolves both identified bugs:
Bug 1: Interior quoted word before comma
The findClosingQuote function now uses first-valid-candidate with deep structural lookahead — it tries each " from first to last and picks the earliest one where isValidJSONAfterClose confirms valid JSON continuation. For "say "yes", and go", the " after yes fails lookahead (, and go" is not a valid JSON token after a comma), so the algorithm correctly skips it and finds the true closing quote at the end.
Test:TestRepairJSON_InteriorQuoteBeforeComma — passes, verifying content = say "yes", and go.
Bug 2: JSON-shaped syntax in string values
Same mechanism: isValidJSONAfterClose recursively validates container closes. For "input map {"key": "val"} caused error", the interior " characters fail deep lookahead (what follows them does not form valid top-level JSON continuation from the outer object's perspective).
All 8 repair tests pass. All review-bot tests pass (go test ./...). The algorithm correctly handles the motivating CI failure, both reviewer edge cases, multi-field JSON, pre-escaped quotes, and complex nested content.
The review's findings are resolved. Requesting re-review or dismissal.
— Rodin
## Addressing Review Findings
The review was submitted against an earlier version of the algorithm. The current code already resolves both identified bugs:
### Bug 1: Interior quoted word before comma
The `findClosingQuote` function now uses **first-valid-candidate** with deep structural lookahead — it tries each `"` from first to last and picks the earliest one where `isValidJSONAfterClose` confirms valid JSON continuation. For `"say "yes", and go"`, the `"` after `yes` fails lookahead (`, and go"` is not a valid JSON token after a comma), so the algorithm correctly skips it and finds the true closing quote at the end.
**Test:** `TestRepairJSON_InteriorQuoteBeforeComma` — passes, verifying content = `say "yes", and go`.
### Bug 2: JSON-shaped syntax in string values
Same mechanism: `isValidJSONAfterClose` recursively validates container closes. For `"input map {"key": "val"} caused error"`, the interior `"` characters fail deep lookahead (what follows them does not form valid top-level JSON continuation from the outer object's perspective).
**Test:** `TestRepairJSON_InteriorQuoteBeforeCloseBrace` — passes.
### Full test suite
All 8 repair tests pass. All review-bot tests pass (`go test ./...`). The algorithm correctly handles the motivating CI failure, both reviewer edge cases, multi-field JSON, pre-escaped quotes, and complex nested content.
The review's findings are resolved. Requesting re-review or dismissal.
— Rodin
Review: PR #45 — JSON repair for unescaped LLM quotes
Verdict: ✅ APPROVE
Assessment
This is a well-designed repair algorithm with correct heuristics. The multi-candidate approach (find all unescaped quotes, pick the first that produces valid JSON continuation) is significantly better than the simple "next char is structural" approach.
What's Good
Correct heuristic: first-valid-candidate with deep lookahead. The isValidJSONAfterClose verifies not just the immediate next character but the full structural continuation (container closes, comma+token, EOF). This handles the two reviewer-reported edge cases (quote-before-comma, JSON-shaped string content) correctly.
Only repairs value strings.isValuePosition restricts repair to strings following : — keys are parsed normally. This is the right bias: LLMs get keys right (they're from a fixed vocabulary) but freetext values contain arbitrary content.
Already-escaped sequences preserved. The inner loop skips \\ sequences correctly in both the candidate finder and the output writer.
Fallback is conservative. If repair doesn't help, the original error is returned (not the repair error). The caller sees the real problem.
Tests are thorough. Covers: no-op on valid, basic repair, quote-before-comma (Sonnet's reported bug), JSON-in-string, multiple fields, pre-escaped quotes, and complex nested. All exercise the actual edge cases that motivated the fix.
Findings
NIT:isValidAfterContainerClose is recursive — deeply nested closing brackets (}}}}}) could stack-overflow on pathological input. In practice, LLM review JSON is 3-4 levels deep max, so this is theoretical. If you want defense, add a depth counter or convert to iterative.
NIT: The findClosingQuote comment says "returns the FIRST one that produces valid JSON continuation" but the variable name candidates might lead someone to think it tries last-first. The iteration is first-to-last (correct) — the comment and code agree, just noting the earlier version's different strategy.
No blocking issues.
Signed: gpt-review
## Review: PR #45 — JSON repair for unescaped LLM quotes
**Verdict: ✅ APPROVE**
### Assessment
This is a well-designed repair algorithm with correct heuristics. The multi-candidate approach (find all unescaped quotes, pick the first that produces valid JSON continuation) is significantly better than the simple "next char is structural" approach.
### What's Good
1. **Correct heuristic: first-valid-candidate with deep lookahead.** The `isValidJSONAfterClose` verifies not just the immediate next character but the full structural continuation (container closes, comma+token, EOF). This handles the two reviewer-reported edge cases (quote-before-comma, JSON-shaped string content) correctly.
2. **Only repairs value strings.** `isValuePosition` restricts repair to strings following `:` — keys are parsed normally. This is the right bias: LLMs get keys right (they're from a fixed vocabulary) but freetext values contain arbitrary content.
3. **Already-escaped sequences preserved.** The inner loop skips `\\` sequences correctly in both the candidate finder and the output writer.
4. **Fallback is conservative.** If repair doesn't help, the original error is returned (not the repair error). The caller sees the real problem.
5. **Tests are thorough.** Covers: no-op on valid, basic repair, quote-before-comma (Sonnet's reported bug), JSON-in-string, multiple fields, pre-escaped quotes, and complex nested. All exercise the actual edge cases that motivated the fix.
### Findings
**NIT:** `isValidAfterContainerClose` is recursive — deeply nested closing brackets (`}}}}}`) could stack-overflow on pathological input. In practice, LLM review JSON is 3-4 levels deep max, so this is theoretical. If you want defense, add a depth counter or convert to iterative.
**NIT:** The `findClosingQuote` comment says "returns the FIRST one that produces valid JSON continuation" but the variable name `candidates` might lead someone to think it tries last-first. The iteration is first-to-last (correct) — the comment and code agree, just noting the earlier version's different strategy.
**No blocking issues.**
Signed: **gpt-review**
All tests pass. The implementation is sound for the stated problem. Here's the full analysis:
✅ Correctness
The algorithm is correct for its target case: LLM-emitted JSON where unescaped " appear inside string values (after :).
isValuePosition correctly restricts repair to : predecessors. Array elements (preceded by [ or ,) are NOT repaired — this is a documented limitation that doesn't affect review-bot in practice, since all free-text fields (finding, summary, etc.) are object values.
findClosingQuote iterates candidates first-to-last and returns the first one that produces valid JSON continuation. The structural lookahead in isValidJSONAfterClose correctly rejects mid-string positions (followed by a letter or non-structural char) and accepts real closes (followed by }, ], ,).
isValidAfterContainerClose uses recursion for nested closes — this handles }} and ]} correctly without blowing the stack for typical JSON depths.
⚠️ Documentation inconsistency (NIT)
repairJSON's doc comment says "pick the LAST one that leaves valid JSON structure", and line 136 repeats "last-valid-candidate heuristic". The actual code in findClosingQuote says "FIRST one" and iterates first-to-last, which is what the code actually does.
First-valid is the correct strategy. The doc comments should say "first" not "last":
// repairJSON: "pick the FIRST one that produces valid JSON continuation"
// (findClosingQuote already says "FIRST" — the two callers just need updating)
⚠️ Off-by-one in escape passthrough (NIT)
In the write loop inside repairJSON:
ifch=='\\'&&j+1<closeIdx{
The condition j+1 < closeIdx excludes the case where a backslash is the last character before the closing quote (j+1 == closeIdx). In that position the lone \ is written without consuming the next character. In practice this is harmless — findClosingQuote also skips \" pairs when collecting candidates, so a trailing \" won't be misidentified as the close. Worth tightening to j+1 <= closeIdx for clarity.
✅ Edge cases handled well
Pre-escaped quotes (\") — correctly passed through without double-escaping (verified)
Empty string values ("") — no candidates → no-op, correct
Multiple fields — each value string is independently repaired
Nested object values in arrays (the real CI failure: findings[n].finding) — works because the text field is a VALUE after :
Valid JSON unchanged — TestRepairJSON_NoOpOnValid confirmed no spurious modifications
✅ Code quality
Idiomatic Go throughout. strings.Builder with pre-allocated capacity is the right choice. Helpers are small and well-named. isJSONTokenStart correctly validates full keyword tokens (true/false/null) rather than just first-char matching.
✅ Tests
Good coverage of the key scenarios. TestParseResponse_UnescapedQuotesInStrings reproduces the exact CI failure. TestRepairJSON_InteriorQuoteBeforeComma (the "yes", and go case) is particularly valuable — comma-adjacent quotes are the hardest case for naive heuristics.
One gap: no test covering the path where repair also fails and ParseResponse returns the original error. Not blocking, but would confirm the fallback returns the right error message.
Verdict
The fix is correct, well-tested, and addresses the real CI failure. The first/last doc inconsistency is the only thing worth fixing. Approving — can be squashed in or land as a follow-up.
## Code Review — `repairJSON` fallback
All tests pass. The implementation is sound for the stated problem. Here's the full analysis:
---
### ✅ Correctness
The algorithm is correct for its target case: LLM-emitted JSON where unescaped `"` appear inside string **values** (after `:`).
**`isValuePosition`** correctly restricts repair to `:` predecessors. Array elements (preceded by `[` or `,`) are NOT repaired — this is a documented limitation that doesn't affect review-bot in practice, since all free-text fields (`finding`, `summary`, etc.) are object values.
**`findClosingQuote`** iterates candidates first-to-last and returns the first one that produces valid JSON continuation. The structural lookahead in `isValidJSONAfterClose` correctly rejects mid-string positions (followed by a letter or non-structural char) and accepts real closes (followed by `}`, `]`, `,`).
**`isValidAfterContainerClose`** uses recursion for nested closes — this handles `}}` and `]}` correctly without blowing the stack for typical JSON depths.
---
### ⚠️ Documentation inconsistency (NIT)
`repairJSON`'s doc comment says **"pick the LAST one that leaves valid JSON structure"**, and line 136 repeats "last-valid-candidate heuristic". The actual code in `findClosingQuote` says **"FIRST one"** and iterates first-to-last, which is what the code actually does.
First-valid is the correct strategy. The doc comments should say "first" not "last":
```
// repairJSON: "pick the FIRST one that produces valid JSON continuation"
// (findClosingQuote already says "FIRST" — the two callers just need updating)
```
---
### ⚠️ Off-by-one in escape passthrough (NIT)
In the write loop inside `repairJSON`:
```go
if ch == '\\' && j+1 < closeIdx {
```
The condition `j+1 < closeIdx` excludes the case where a backslash is the **last character before the closing quote** (`j+1 == closeIdx`). In that position the lone `\` is written without consuming the next character. In practice this is harmless — `findClosingQuote` also skips `\"` pairs when collecting candidates, so a trailing `\"` won't be misidentified as the close. Worth tightening to `j+1 <= closeIdx` for clarity.
---
### ✅ Edge cases handled well
- **Pre-escaped quotes** (`\"`) — correctly passed through without double-escaping (verified)
- **Empty string values** (`""`) — no candidates → no-op, correct
- **Multiple fields** — each value string is independently repaired
- **Nested object values** in arrays (the real CI failure: `findings[n].finding`) — works because the text field is a VALUE after `:`
- **Valid JSON unchanged** — `TestRepairJSON_NoOpOnValid` confirmed no spurious modifications
---
### ✅ Code quality
Idiomatic Go throughout. `strings.Builder` with pre-allocated capacity is the right choice. Helpers are small and well-named. `isJSONTokenStart` correctly validates full keyword tokens (`true`/`false`/`null`) rather than just first-char matching.
---
### ✅ Tests
Good coverage of the key scenarios. `TestParseResponse_UnescapedQuotesInStrings` reproduces the exact CI failure. `TestRepairJSON_InteriorQuoteBeforeComma` (the `"yes", and go` case) is particularly valuable — comma-adjacent quotes are the hardest case for naive heuristics.
One gap: no test covering the path where repair also fails and `ParseResponse` returns the original error. Not blocking, but would confirm the fallback returns the right error message.
---
### Verdict
The fix is correct, well-tested, and addresses the real CI failure. The first/last doc inconsistency is the only thing worth fixing. Approving — can be squashed in or land as a follow-up.
The patch introduces a targeted, robust approach for repairing common malformed JSON from LLMs, and thoroughly tests both edge cases and correctness. The implementation is careful and well-contained, with no impact to valid input.
Recommendation
APPROVE — This is an excellent, production-grade fix for the LLM unescaped quote bug. The repairJSON approach is careful to only intervene for value strings, and the fallback is only attempted if the initial unmarshal fails, minimizing risk. Test coverage demonstrates a deep consideration of edge cases. The implementation fits Go idioms, maintains package purity, and passes CI. You should merge this as the solution to the observed CI failures.
# Gpt Review
## Summary
The patch introduces a targeted, robust approach for repairing common malformed JSON from LLMs, and thoroughly tests both edge cases and correctness. The implementation is careful and well-contained, with no impact to valid input.
## Recommendation
**APPROVE** — This is an excellent, production-grade fix for the LLM unescaped quote bug. The repairJSON approach is careful to only intervene for value strings, and the fallback is only attempted if the initial unmarshal fails, minimizing risk. Test coverage demonstrates a deep consideration of edge cases. The implementation fits Go idioms, maintains package purity, and passes CI. You should merge this as the solution to the observed CI failures.
---
*Review by gpt*
<!-- review-bot:gpt -->
---
*Evaluated against 489457c1*
The fallback JSON repair logic is a pragmatic and well-contained improvement that addresses the observed LLM output issue. The implementation is careful, and the added tests cover key edge cases and regressions. A couple of minor documentation/heuristic clarifications could further improve maintainability.
Findings
#
Severity
File
Line
Finding
1
[NIT]
review/parser.go
79
The comment above repairJSON says it will "pick the LAST" valid closing quote to maximize content, but findClosingQuote actually returns the FIRST candidate that yields valid continuation. Align the comment with the implementation or adjust the selection to match the described strategy.
2
[MINOR]
review/parser.go
0
isValuePosition only treats strings immediately following ':' as values, so unescaped quotes in array string elements (e.g., "key": ["foo "bar""]) will not be repaired. Consider extending the heuristic to also handle array element positions when inside a value context if this becomes a real-world failure mode.
3
[NIT]
review/parser.go
0
In findClosingQuote, backslash escape handling skips only the single character after '\'. This is sufficient for avoiding escaped quotes, but may be misleading for readers with respect to multi-rune escapes like \uXXXX. A brief comment clarifying that only escaped quotes matter here (and that other escapes are intentionally not fully parsed) would improve readability.
Recommendation
APPROVE — The change effectively hardens JSON parsing against a real LLM failure mode without impacting valid inputs, and the tests demonstrate good coverage of edge cases. Given CI is green and no major issues were found, this is good to merge. Consider polishing the documentation to match the implemented heuristic and, if future inputs require it, extend the value detection to handle string elements in arrays. Otherwise, the current solution is practical and maintainable.
# Sonnet Review
## Summary
The fallback JSON repair logic is a pragmatic and well-contained improvement that addresses the observed LLM output issue. The implementation is careful, and the added tests cover key edge cases and regressions. A couple of minor documentation/heuristic clarifications could further improve maintainability.
## Findings
| # | Severity | File | Line | Finding |
|---|----------|------|------|--------|
| 1 | [NIT] | `review/parser.go` | 79 | The comment above repairJSON says it will "pick the LAST" valid closing quote to maximize content, but findClosingQuote actually returns the FIRST candidate that yields valid continuation. Align the comment with the implementation or adjust the selection to match the described strategy. |
| 2 | [MINOR] | `review/parser.go` | 0 | isValuePosition only treats strings immediately following ':' as values, so unescaped quotes in array string elements (e.g., "key": ["foo "bar""]) will not be repaired. Consider extending the heuristic to also handle array element positions when inside a value context if this becomes a real-world failure mode. |
| 3 | [NIT] | `review/parser.go` | 0 | In findClosingQuote, backslash escape handling skips only the single character after '\\'. This is sufficient for avoiding escaped quotes, but may be misleading for readers with respect to multi-rune escapes like \uXXXX. A brief comment clarifying that only escaped quotes matter here (and that other escapes are intentionally not fully parsed) would improve readability. |
## Recommendation
**APPROVE** — The change effectively hardens JSON parsing against a real LLM failure mode without impacting valid inputs, and the tests demonstrate good coverage of edge cases. Given CI is green and no major issues were found, this is good to merge. Consider polishing the documentation to match the implemented heuristic and, if future inputs require it, extend the value detection to handle string elements in arrays. Otherwise, the current solution is practical and maintainable.
---
*Review by sonnet*
<!-- review-bot:sonnet -->
---
*Evaluated against 489457c1*
[NIT] The comment above repairJSON says it will "pick the LAST" valid closing quote to maximize content, but findClosingQuote actually returns the FIRST candidate that yields valid continuation. Align the comment with the implementation or adjust the selection to match the described strategy.
**[NIT]** The comment above repairJSON says it will "pick the LAST" valid closing quote to maximize content, but findClosingQuote actually returns the FIRST candidate that yields valid continuation. Align the comment with the implementation or adjust the selection to match the described strategy.
security-review-bot
requested review from security-review-bot 2026-05-05 06:17:15 +00:00
The fallback JSON repair logic addresses malformed LLM outputs without introducing obvious exploitable vulnerabilities. Error handling and validation remain intact, and CI has passed.
Findings
#
Severity
File
Line
Finding
1
[MINOR]
review/parser.go
36
The returned error includes the full raw LLM response. This can lead to information leakage and log amplification if upstream logs the error verbatim. Consider redacting or truncating the response (e.g., first N bytes) before including it in error messages.
2
[MINOR]
review/parser.go
187
The repairJSON/findClosingQuote logic scans ahead for every candidate quote and can be O(n^2) on inputs with many quotes, potentially enabling CPU/resource exhaustion if very large or adversarial inputs are processed. Consider bounding input size for repair, short-circuiting after a threshold, or limiting the number of candidates examined.
Recommendation
APPROVE — Approve this change. The repairJSON fallback improves robustness against malformed LLM JSON and does not introduce clear security vulnerabilities. For defense-in-depth, consider truncating or redacting the raw response in error messages to avoid leaking large or sensitive content, and place pragmatic bounds on the repair logic (e.g., only attempt repair up to a size limit and/or cap the number of quote candidates) to mitigate potential resource exhaustion on pathological inputs.
# Security Review
## Summary
The fallback JSON repair logic addresses malformed LLM outputs without introducing obvious exploitable vulnerabilities. Error handling and validation remain intact, and CI has passed.
## Findings
| # | Severity | File | Line | Finding |
|---|----------|------|------|--------|
| 1 | [MINOR] | `review/parser.go` | 36 | The returned error includes the full raw LLM response. This can lead to information leakage and log amplification if upstream logs the error verbatim. Consider redacting or truncating the response (e.g., first N bytes) before including it in error messages. |
| 2 | [MINOR] | `review/parser.go` | 187 | The repairJSON/findClosingQuote logic scans ahead for every candidate quote and can be O(n^2) on inputs with many quotes, potentially enabling CPU/resource exhaustion if very large or adversarial inputs are processed. Consider bounding input size for repair, short-circuiting after a threshold, or limiting the number of candidates examined. |
## Recommendation
**APPROVE** — Approve this change. The repairJSON fallback improves robustness against malformed LLM JSON and does not introduce clear security vulnerabilities. For defense-in-depth, consider truncating or redacting the raw response in error messages to avoid leaking large or sensitive content, and place pragmatic bounds on the repair logic (e.g., only attempt repair up to a size limit and/or cap the number of quote candidates) to mitigate potential resource exhaustion on pathological inputs.
---
*Review by security*
<!-- review-bot:security -->
---
*Evaluated against 489457c1*
[MINOR] The returned error includes the full raw LLM response. This can lead to information leakage and log amplification if upstream logs the error verbatim. Consider redacting or truncating the response (e.g., first N bytes) before including it in error messages.
**[MINOR]** The returned error includes the full raw LLM response. This can lead to information leakage and log amplification if upstream logs the error verbatim. Consider redacting or truncating the response (e.g., first N bytes) before including it in error messages.
[MINOR] The repairJSON/findClosingQuote logic scans ahead for every candidate quote and can be O(n^2) on inputs with many quotes, potentially enabling CPU/resource exhaustion if very large or adversarial inputs are processed. Consider bounding input size for repair, short-circuiting after a threshold, or limiting the number of candidates examined.
**[MINOR]** The repairJSON/findClosingQuote logic scans ahead for every candidate quote and can be O(n^2) on inputs with many quotes, potentially enabling CPU/resource exhaustion if very large or adversarial inputs are processed. Consider bounding input size for repair, short-circuiting after a threshold, or limiting the number of candidates examined.
rodin
merged commit cabbb5a55a into main2026-05-05 12:40:39 +00:00
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Problem
Sonnet sometimes emits JSON with unescaped double quotes inside string values:
This breaks
json.Unmarshalwithinvalid character '2' after object key:value pair.Seen in gargoyle CI (sonnet-review on PR #551, job 3173).
Fix
Add a
repairJSONfallback inParseResponse. When the initial parse fails, the repair function walks the JSON character-by-character, tracking string state, and escapes any interior quotes that aren't followed by structural characters (,,},],:).Tests
TestParseResponse_UnescapedQuotesInStrings— reproduces the exact CI failureTestRepairJSON_NoOpOnValid— confirms no modification of valid JSONTestRepairJSON_FixesUnescapedQuotes— verifies repair produces parseable outputtest
test
Review:
fix: repair unescaped quotes in LLM JSON responsesThe fix addresses a real, observed failure mode (Sonnet emitting unescaped quotes inside JSON string values), and the structural approach — character-by-character walking with string-state tracking — is the right level of tool for this problem. The core algorithm is sound for the common case the PR targets. However, there are two correctness gaps that will silently produce broken output in edge cases, and the test coverage only covers the happy path for the tricky scenario.
🔴 Bug: Interior quoted word immediately before a comma
File:
review/parser.go,repairJSONThe lookahead heuristic treats a
"followed by,(after optional whitespace) as a structural closing quote. This misclassifies an interior quote that happens to precede a comma within the string value.Reproducer (verified — this test currently fails):
The repair function correctly escapes the opening quote before
yes, but then sees the closing"afteryes— followed by,— and treats it as the structural end-of-string. The remainder, and go"}is then outside any string context, producing malformed JSON.The same failure class applies when an interior quoted word ends just before
], since]is also in the structural lookahead set.Root cause:
,,},], and:are all valid string content (code snippets, key-value descriptions, prose). They cannot reliably distinguish a structural closing quote from an interior one.🔴 Bug: JSON-shaped syntax in string values
When an LLM describes a struct or map literal in a finding (e.g.
"input map {"key": "val"} caused error"), the"aftervalhas}as lookahead and will be treated as a structural close, ending the outer string prematurely.Given that this tool reviews code, and the LLMs it calls regularly describe map/struct literals, JSON blobs, or key-value pairs in finding text, this is a realistic failure scenario.
🟡 Test coverage misses the known-failing edge case
TestRepairJSON_FixesUnescapedQuotesuses{"msg": "use "foo" here"}— the interior quotes aren't adjacent to any structural character, so this passes cleanly.TestParseResponse_UnescapedQuotesInStringsalso passes because the interior"28"is followed by), not,or}.Neither test covers the comma-adjacency failure. The following test currently fails with code as written:
✅ What works well
\handling) is correct — pre-existing escapes are preserved without double-escaping.strings.BuilderwithGrow— idiomatic and allocation-efficient.return— diagnostics remain actionable when repair also fails.TestRepairJSON_NoOpOnValid.Summary
The algorithm correctly handles the exact CI failure that motivated this PR (
(e.g. "28")— interior quotes not adjacent to structural characters). For the most common LLM unescaped-quote pattern it works. But the lookahead heuristic has a fundamental ambiguity:,,},], and:are valid string content, so a quoted word ending just before one of them causes the algorithm to close the string prematurely — producing output more broken than the input.Requesting changes on the two correctness bugs. If the intent is "handles the observed pattern, not a general-purpose JSON repair," the test suite should explicitly cover the known edge cases (including currently failing ones) so future maintainers understand the boundaries.
— sonnet-review-bot
Rewrites the JSON repair algorithm to address two correctness bugs identified in code review: 1. Interior quoted word before comma: "say "yes", and go" was misidentified as structural close because "," followed the quote. 2. JSON-shaped content in strings: {"key": "val"} inside a string value was being parsed as actual JSON structure. The new approach: - Distinguishes keys from values (only values need repair) - Uses first-valid-candidate scan with deep lookahead - Verifies that after a candidate close, the continuation is not just a structural char but a complete valid JSON pattern - Validates keyword tokens (true/false/null) fully, not just first char - Checks container closes recursively for valid continuation Adds comprehensive tests for all reported edge cases plus a complex combined scenario with nested JSON-like content, quoted words before commas, and multiple failure modes in one string.Addressing Review Findings
The review was submitted against an earlier version of the algorithm. The current code already resolves both identified bugs:
Bug 1: Interior quoted word before comma
The
findClosingQuotefunction now uses first-valid-candidate with deep structural lookahead — it tries each"from first to last and picks the earliest one whereisValidJSONAfterCloseconfirms valid JSON continuation. For"say "yes", and go", the"afteryesfails lookahead (, and go"is not a valid JSON token after a comma), so the algorithm correctly skips it and finds the true closing quote at the end.Test:
TestRepairJSON_InteriorQuoteBeforeComma— passes, verifying content =say "yes", and go.Bug 2: JSON-shaped syntax in string values
Same mechanism:
isValidJSONAfterCloserecursively validates container closes. For"input map {"key": "val"} caused error", the interior"characters fail deep lookahead (what follows them does not form valid top-level JSON continuation from the outer object's perspective).Test:
TestRepairJSON_InteriorQuoteBeforeCloseBrace— passes.Full test suite
All 8 repair tests pass. All review-bot tests pass (
go test ./...). The algorithm correctly handles the motivating CI failure, both reviewer edge cases, multi-field JSON, pre-escaped quotes, and complex nested content.The review's findings are resolved. Requesting re-review or dismissal.
— Rodin
Review was against earlier version. Both bugs are already fixed in the current code — tests pass.
Review: PR #45 — JSON repair for unescaped LLM quotes
Verdict: ✅ APPROVE
Assessment
This is a well-designed repair algorithm with correct heuristics. The multi-candidate approach (find all unescaped quotes, pick the first that produces valid JSON continuation) is significantly better than the simple "next char is structural" approach.
What's Good
Correct heuristic: first-valid-candidate with deep lookahead. The
isValidJSONAfterCloseverifies not just the immediate next character but the full structural continuation (container closes, comma+token, EOF). This handles the two reviewer-reported edge cases (quote-before-comma, JSON-shaped string content) correctly.Only repairs value strings.
isValuePositionrestricts repair to strings following:— keys are parsed normally. This is the right bias: LLMs get keys right (they're from a fixed vocabulary) but freetext values contain arbitrary content.Already-escaped sequences preserved. The inner loop skips
\\sequences correctly in both the candidate finder and the output writer.Fallback is conservative. If repair doesn't help, the original error is returned (not the repair error). The caller sees the real problem.
Tests are thorough. Covers: no-op on valid, basic repair, quote-before-comma (Sonnet's reported bug), JSON-in-string, multiple fields, pre-escaped quotes, and complex nested. All exercise the actual edge cases that motivated the fix.
Findings
NIT:
isValidAfterContainerCloseis recursive — deeply nested closing brackets (}}}}}) could stack-overflow on pathological input. In practice, LLM review JSON is 3-4 levels deep max, so this is theoretical. If you want defense, add a depth counter or convert to iterative.NIT: The
findClosingQuotecomment says "returns the FIRST one that produces valid JSON continuation" but the variable namecandidatesmight lead someone to think it tries last-first. The iteration is first-to-last (correct) — the comment and code agree, just noting the earlier version's different strategy.No blocking issues.
Signed: gpt-review
Code Review —
repairJSONfallbackAll tests pass. The implementation is sound for the stated problem. Here's the full analysis:
✅ Correctness
The algorithm is correct for its target case: LLM-emitted JSON where unescaped
"appear inside string values (after:).isValuePositioncorrectly restricts repair to:predecessors. Array elements (preceded by[or,) are NOT repaired — this is a documented limitation that doesn't affect review-bot in practice, since all free-text fields (finding,summary, etc.) are object values.findClosingQuoteiterates candidates first-to-last and returns the first one that produces valid JSON continuation. The structural lookahead inisValidJSONAfterClosecorrectly rejects mid-string positions (followed by a letter or non-structural char) and accepts real closes (followed by},],,).isValidAfterContainerCloseuses recursion for nested closes — this handles}}and]}correctly without blowing the stack for typical JSON depths.⚠️ Documentation inconsistency (NIT)
repairJSON's doc comment says "pick the LAST one that leaves valid JSON structure", and line 136 repeats "last-valid-candidate heuristic". The actual code infindClosingQuotesays "FIRST one" and iterates first-to-last, which is what the code actually does.First-valid is the correct strategy. The doc comments should say "first" not "last":
⚠️ Off-by-one in escape passthrough (NIT)
In the write loop inside
repairJSON:The condition
j+1 < closeIdxexcludes the case where a backslash is the last character before the closing quote (j+1 == closeIdx). In that position the lone\is written without consuming the next character. In practice this is harmless —findClosingQuotealso skips\"pairs when collecting candidates, so a trailing\"won't be misidentified as the close. Worth tightening toj+1 <= closeIdxfor clarity.✅ Edge cases handled well
\") — correctly passed through without double-escaping (verified)"") — no candidates → no-op, correctfindings[n].finding) — works because the text field is a VALUE after:TestRepairJSON_NoOpOnValidconfirmed no spurious modifications✅ Code quality
Idiomatic Go throughout.
strings.Builderwith pre-allocated capacity is the right choice. Helpers are small and well-named.isJSONTokenStartcorrectly validates full keyword tokens (true/false/null) rather than just first-char matching.✅ Tests
Good coverage of the key scenarios.
TestParseResponse_UnescapedQuotesInStringsreproduces the exact CI failure.TestRepairJSON_InteriorQuoteBeforeComma(the"yes", and gocase) is particularly valuable — comma-adjacent quotes are the hardest case for naive heuristics.One gap: no test covering the path where repair also fails and
ParseResponsereturns the original error. Not blocking, but would confirm the fallback returns the right error message.Verdict
The fix is correct, well-tested, and addresses the real CI failure. The first/last doc inconsistency is the only thing worth fixing. Approving — can be squashed in or land as a follow-up.
Gpt Review
Summary
The patch introduces a targeted, robust approach for repairing common malformed JSON from LLMs, and thoroughly tests both edge cases and correctness. The implementation is careful and well-contained, with no impact to valid input.
Recommendation
APPROVE — This is an excellent, production-grade fix for the LLM unescaped quote bug. The repairJSON approach is careful to only intervene for value strings, and the fallback is only attempted if the initial unmarshal fails, minimizing risk. Test coverage demonstrates a deep consideration of edge cases. The implementation fits Go idioms, maintains package purity, and passes CI. You should merge this as the solution to the observed CI failures.
Review by gpt
Evaluated against
489457c1Sonnet Review
Summary
The fallback JSON repair logic is a pragmatic and well-contained improvement that addresses the observed LLM output issue. The implementation is careful, and the added tests cover key edge cases and regressions. A couple of minor documentation/heuristic clarifications could further improve maintainability.
Findings
review/parser.goreview/parser.goreview/parser.goRecommendation
APPROVE — The change effectively hardens JSON parsing against a real LLM failure mode without impacting valid inputs, and the tests demonstrate good coverage of edge cases. Given CI is green and no major issues were found, this is good to merge. Consider polishing the documentation to match the implemented heuristic and, if future inputs require it, extend the value detection to handle string elements in arrays. Otherwise, the current solution is practical and maintainable.
Review by sonnet
Evaluated against
489457c1@@ -74,3 +79,230 @@ func extractJSON(s string) string {s = strings.TrimSpace(s)[NIT] The comment above repairJSON says it will "pick the LAST" valid closing quote to maximize content, but findClosingQuote actually returns the FIRST candidate that yields valid continuation. Align the comment with the implementation or adjust the selection to match the described strategy.
Security Review
Summary
The fallback JSON repair logic addresses malformed LLM outputs without introducing obvious exploitable vulnerabilities. Error handling and validation remain intact, and CI has passed.
Findings
review/parser.goreview/parser.goRecommendation
APPROVE — Approve this change. The repairJSON fallback improves robustness against malformed LLM JSON and does not introduce clear security vulnerabilities. For defense-in-depth, consider truncating or redacting the raw response in error messages to avoid leaking large or sensitive content, and place pragmatic bounds on the repair logic (e.g., only attempt repair up to a size limit and/or cap the number of quote candidates) to mitigate potential resource exhaustion on pathological inputs.
Review by security
Evaluated against
489457c1@@ -33,0 +33,4 @@// Try to repair before giving up.repaired := repairJSON(cleaned)if err2 := json.Unmarshal([]byte(repaired), &result); err2 != nil {return nil, fmt.Errorf("parse LLM response as JSON: %w\nRaw response: %s", err, response)[MINOR] The returned error includes the full raw LLM response. This can lead to information leakage and log amplification if upstream logs the error verbatim. Consider redacting or truncating the response (e.g., first N bytes) before including it in error messages.
@@ -77,0 +184,4 @@// value starting at position start (the character after the opening quote).// It collects all unescaped quote candidates and returns the FIRST one that// produces valid JSON continuation (deeper lookahead verifies the next token).func findClosingQuote(runes []rune, start int) int {[MINOR] The repairJSON/findClosingQuote logic scans ahead for every candidate quote and can be O(n^2) on inputs with many quotes, potentially enabling CPU/resource exhaustion if very large or adversarial inputs are processed. Consider bounding input size for repair, short-circuiting after a threshold, or limiting the number of candidates examined.