Skip to content

Commit f910eea

Browse files
dennys246claude
andcommitted
fix(research): JSON quote repair + campaign-mode first-turn guidance
Two fixes for the hippocampal recall experiment: 1. JSON parser now repairs unescaped double quotes inside string values. Narrative dialogue (She said "hello") embedded in tool params no longer breaks parsing. Retry pipeline: sanitize → quote repair → fail. 2. When a campaign is active, the orchestrator's first instruction says 'send the FIRST campaign turn verbatim' instead of 'send your first probe'. Only triggers when goal contains CAMPAIGN PROTOCOL — normal sims keep full creative freedom. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 209e606 commit f910eea

2 files changed

Lines changed: 278 additions & 146 deletions

File tree

src/maxim/models/language/json_parser.py

Lines changed: 103 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ def _sanitize_json_string(text: str) -> str:
2222
escape_next = False
2323
continue
2424

25-
if char == '\\':
25+
if char == "\\":
2626
result.append(char)
2727
escape_next = True
2828
continue
@@ -34,21 +34,92 @@ def _sanitize_json_string(text: str) -> str:
3434

3535
if in_string:
3636
# Escape control characters inside strings
37-
if char == '\n':
38-
result.append('\\n')
39-
elif char == '\r':
40-
result.append('\\r')
41-
elif char == '\t':
42-
result.append('\\t')
37+
if char == "\n":
38+
result.append("\\n")
39+
elif char == "\r":
40+
result.append("\\r")
41+
elif char == "\t":
42+
result.append("\\t")
4343
elif ord(char) < 32:
4444
# Other control characters - escape as unicode
45-
result.append(f'\\u{ord(char):04x}')
45+
result.append(f"\\u{ord(char):04x}")
4646
else:
4747
result.append(char)
4848
else:
4949
result.append(char)
5050

51-
return ''.join(result)
51+
return "".join(result)
52+
53+
54+
def _repair_unescaped_quotes(text: str) -> str:
55+
"""Attempt to escape unescaped double quotes inside JSON string values.
56+
57+
When an LLM embeds narrative text containing dialogue (e.g., She said
58+
"hello") into a JSON string, the inner quotes break parsing. This
59+
function heuristically detects quotes that appear inside a string value
60+
(not at a structural boundary) and escapes them.
61+
62+
Strategy: walk the text tracking JSON structural context. A quote that
63+
appears inside a string value and is followed by content that doesn't
64+
look like a JSON key or structural token is treated as a literal that
65+
needs escaping.
66+
"""
67+
import re
68+
69+
# Quick check: if it parses, no repair needed
70+
try:
71+
json.loads(text)
72+
return text
73+
except Exception:
74+
pass
75+
76+
# Find all string value positions: after `"key":` patterns, the next
77+
# quote opens a string value. We re-escape any unescaped quotes
78+
# inside that value by looking for the *correct* closing quote
79+
# (one followed by , or } or ] or whitespace then one of those).
80+
# This is a heuristic — it won't handle all edge cases but covers the
81+
# common case of narrative dialogue embedded in tool params.
82+
structural_close = re.compile(r'"\s*[,}\]]')
83+
84+
result = []
85+
i = 0
86+
n = len(text)
87+
while i < n:
88+
ch = text[i]
89+
if ch == "\\":
90+
# Escaped char — pass through both characters
91+
result.append(text[i : i + 2])
92+
i += 2
93+
continue
94+
if ch == '"':
95+
# Opening quote of a string — find the structural close
96+
result.append('"')
97+
i += 1
98+
# Scan for the closing quote (one followed by structural char)
99+
while i < n:
100+
ic = text[i]
101+
if ic == "\\":
102+
result.append(text[i : i + 2])
103+
i += 2
104+
continue
105+
if ic == '"':
106+
# Is this the structural close?
107+
rest = text[i:]
108+
if structural_close.match(rest):
109+
result.append('"')
110+
i += 1
111+
break
112+
# Not structural — escape it
113+
result.append('\\"')
114+
i += 1
115+
continue
116+
result.append(ic)
117+
i += 1
118+
continue
119+
result.append(ch)
120+
i += 1
121+
122+
return "".join(result)
52123

53124

54125
def _find_first_json_object(text: str) -> str | None:
@@ -192,23 +263,32 @@ def _extract_json_object(text: str) -> dict[str, Any] | None:
192263
try:
193264
obj = json.loads(json_candidate)
194265
except json.JSONDecodeError as e:
195-
# Try sanitizing control characters inside strings
196-
if "control character" in str(e).lower() or "invalid" in str(e).lower():
266+
# Try sanitizing (control chars, unescaped quotes) on any parse failure
267+
try:
268+
sanitized = _sanitize_json_string(json_candidate)
269+
obj = json.loads(sanitized)
270+
info("JSON parse succeeded after sanitizing")
271+
except Exception:
272+
# Last resort: try repairing unescaped quotes inside string values
197273
try:
198-
sanitized = _sanitize_json_string(json_candidate)
199-
obj = json.loads(sanitized)
200-
info("JSON parse succeeded after sanitizing control characters")
201-
except Exception as e2:
202-
warn("JSON parse failed after sanitization: %s | len=%d | last_50: %s",
203-
str(e2)[:80], len(json_candidate), json_candidate[-50:] if len(json_candidate) > 50 else json_candidate)
274+
repaired = _repair_unescaped_quotes(json_candidate)
275+
obj = json.loads(repaired)
276+
info("JSON parse succeeded after quote repair")
277+
except Exception:
278+
warn(
279+
"JSON parse failed: %s | len=%d | last_50: %s",
280+
str(e)[:80],
281+
len(json_candidate),
282+
json_candidate[-50:] if len(json_candidate) > 50 else json_candidate,
283+
)
204284
return None
205-
else:
206-
warn("JSON parse failed: %s | len=%d | last_50: %s",
207-
str(e)[:80], len(json_candidate), json_candidate[-50:] if len(json_candidate) > 50 else json_candidate)
208-
return None
209285
except Exception as e:
210-
warn("JSON parse failed: %s | len=%d | last_50: %s",
211-
str(e)[:80], len(json_candidate), json_candidate[-50:] if len(json_candidate) > 50 else json_candidate)
286+
warn(
287+
"JSON parse failed: %s | len=%d | last_50: %s",
288+
str(e)[:80],
289+
len(json_candidate),
290+
json_candidate[-50:] if len(json_candidate) > 50 else json_candidate,
291+
)
212292
return None
213293

214294
return obj if isinstance(obj, dict) else None

0 commit comments

Comments
 (0)