"""Lenient JSON for the relayed command channel — and honest about it. `filectl` and `dialoguectl` both take a single line of compact JSON, hand-typed by a language model into a tool marker. Models get that *nearly* right and then get it wrong in a small, boringly repeatable set of ways: {"op":"list","path":"/home/x","recursive":false"} ← stray quote after a literal {"op": "read", "path": "/tmp/a.txt",} ← trailing comma {'op': 'read', 'path': '/tmp/a.txt'} ← single quotes {“op”: “read”, “path”: “/tmp/a.txt”} ← smart quotes {"op": "list", "recursive": False} ← Python literals ```json {"op": "list"} ``` ← fenced Observed live (2026-07-30): a stray quote after `false` cost an entire desk turn — the call was rejected, the model re-sent the *identical* line, was rejected again, and then gave up and told the user "I'll check now" without ever calling anything. The user got a promise instead of an answer because of one character. Strict parsing is the wrong trade here. Nothing about a misplaced quote is ambiguous, the payload is machine-written and machine-read, and the cost of refusing is a wasted round trip that the model has already demonstrated it won't recover from. So: try strict first, then apply narrow repairs, and accept a repair **only if the result parses**. Two rules keep this from becoming "guess what they meant": 1. **Repairs are conservative and named.** Each one fixes a known malformation, is applied in isolation, and is reported by name. 2. **Repairs are never silent.** The caller appends the repair note to the tool result, so the model is told it sent broken JSON *while it still has the turn* — the fix works today and teaches within the conversation. Hiding it would trade a visible failure for an invisible one. When nothing parses, the error points at the exact character with a caret, because "Expecting ',' delimiter: char 74" is not something a model can act on and a pointed-at fragment is. """ from __future__ import annotations import json import re from typing import Any, Callable # Ordered, cheapest and safest first. Each entry is (name, transform); after # each one the payload is re-parsed, so the first repair that works wins and # nothing more aggressive gets applied than the input actually needed. _REPAIRS: tuple[tuple[str, Callable[[str], str]], ...] = ( ( "stripped a markdown code fence", lambda text: re.sub(r"^\s*```(?:json)?\s*|\s*```\s*$", "", text), ), ( "replaced smart quotes with straight ones", lambda text: text.translate(str.maketrans({"“": '"', "”": '"', "‘": "'", "’": "'"})), ), ( "removed a stray quote after a bare value", # {"recursive":false"} -> {"recursive":false} lambda text: re.sub( r"(:\s*(?:true|false|null|-?\d+(?:\.\d+)?))\s*\"(\s*[,}\]])", r"\1\2", text), ), ( "removed a trailing comma", lambda text: re.sub(r",(\s*[}\]])", r"\1", text), ), ( "converted Python literals (True/False/None) to JSON", lambda text: re.sub(r"(:\s*)(True|False|None)\b", lambda m: m.group(1) + {"True": "true", "False": "false", "None": "null"}[m.group(2)], text), ), ( "converted single-quoted strings to double-quoted", lambda text: re.sub(r"'([^'\"]*)'", r'"\1"', text), ), ) class RelayJsonError(ValueError): """Unparseable even after repairs — carries a pointed-at fragment.""" def loads(payload: str) -> tuple[Any, list[str]]: """Parse *payload*, repairing common model mistakes. Returns (data, repairs) where *repairs* names what had to be fixed — empty when the input was already valid. Raises RelayJsonError with a caret at the offending character when nothing works.""" text = str(payload or "").strip() if not text: raise RelayJsonError("empty payload") try: return json.loads(text), [] except json.JSONDecodeError as exc: # Bound to a plain name: Python deletes the `as` target at the end of # the except block, so referring to it further down would raise # UnboundLocalError instead of reporting the parse failure. first_error = exc applied: list[str] = [] candidate = text for name, repair in _REPAIRS: repaired = repair(candidate) if repaired == candidate: continue candidate = repaired applied.append(name) try: return json.loads(candidate), applied except json.JSONDecodeError: continue # keep going: a payload can be broken in more than one way raise RelayJsonError(point_at(text, first_error)) def point_at(text: str, error: json.JSONDecodeError, width: int = 28) -> str: """Show the failure where it happened. A model can act on "you wrote `false\"}` here"; it cannot act on "Expecting ',' delimiter: line 1 column 75".""" position = max(0, min(len(text), getattr(error, "pos", 0))) start = max(0, position - width) end = min(len(text), position + width) fragment = text[start:end] caret = " " * (position - start) + "^" lead = "…" if start > 0 else "" tail = "…" if end < len(text) else "" return ( f"{error.msg} at character {position}:\n" f" {lead}{fragment}{tail}\n" f" {' ' * len(lead)}{caret}" ) def repair_note(repairs: list[str]) -> str: """The line appended to a tool result when repairs were needed. Phrased as feedback rather than an apology: the model is the author of the broken JSON and is the one who can stop sending it.""" if not repairs: return "" return ( " (note: your JSON was malformed — I " + "; ".join(repairs) + " and ran it anyway. Send valid single-line JSON next time.)" )