Add relay_json module, update dialogue and file_ops, update local settings

This commit is contained in:
2026-07-31 01:27:20 -06:00
parent 96afc351ac
commit c4e805defd
5 changed files with 257 additions and 22 deletions
+7 -1
View File
@@ -83,7 +83,13 @@
"Bash(timeout 900 /tmp/claude-1000/-home-maji-Documents-Bolt-Pet/58f4a7c3-6a92-47ed-9e43-bb2216d8b135/scratchpad/tmnvenv/bin/python -m pytest tests/test_emotional_memory.py tests/test_inner_monologue.py tests/test_temporal_core.py tests/test_temporal_episodes.py tests/test_temporal_stream.py tests/test_temporal_facade.py tests/test_proactive.py tests/test_desk_api.py tests/test_main_helpers.py -q -p no:cacheprovider)", "Bash(timeout 900 /tmp/claude-1000/-home-maji-Documents-Bolt-Pet/58f4a7c3-6a92-47ed-9e43-bb2216d8b135/scratchpad/tmnvenv/bin/python -m pytest tests/test_emotional_memory.py tests/test_inner_monologue.py tests/test_temporal_core.py tests/test_temporal_episodes.py tests/test_temporal_stream.py tests/test_temporal_facade.py tests/test_proactive.py tests/test_desk_api.py tests/test_main_helpers.py -q -p no:cacheprovider)",
"Bash(timeout 1800 /tmp/claude-1000/-home-maji-Documents-Bolt-Pet/58f4a7c3-6a92-47ed-9e43-bb2216d8b135/scratchpad/tmnvenv/bin/python -m pytest tests -q -p no:cacheprovider --ignore=tests/test_tool_markers.py --ignore=tests/test_tts_sanitization.py --ignore=tests/test_speaker_matching.py --ignore=tests/test_recent_speaker_fallback.py --ignore=tests/test_assistant_cli_call_proxy.py --ignore=tests/test_assistant_cli_permissions.py --ignore=tests/test_memory_store.py --ignore=tests/test_default_agent.py --ignore=tests/test_billing_web.py)", "Bash(timeout 1800 /tmp/claude-1000/-home-maji-Documents-Bolt-Pet/58f4a7c3-6a92-47ed-9e43-bb2216d8b135/scratchpad/tmnvenv/bin/python -m pytest tests -q -p no:cacheprovider --ignore=tests/test_tool_markers.py --ignore=tests/test_tts_sanitization.py --ignore=tests/test_speaker_matching.py --ignore=tests/test_recent_speaker_fallback.py --ignore=tests/test_assistant_cli_call_proxy.py --ignore=tests/test_assistant_cli_permissions.py --ignore=tests/test_memory_store.py --ignore=tests/test_default_agent.py --ignore=tests/test_billing_web.py)",
"WebFetch(domain:elevenlabs.io)", "WebFetch(domain:elevenlabs.io)",
"Bash(QT_QPA_PLATFORM=offscreen .venv/bin/pytest tests/test_dialogue.py -q)" "Bash(QT_QPA_PLATFORM=offscreen .venv/bin/pytest tests/test_dialogue.py -q)",
"Bash(timeout 900 /tmp/claude-1000/-home-maji-Documents-Bolt-Pet/58f4a7c3-6a92-47ed-9e43-bb2216d8b135/scratchpad/tmnvenv/bin/python -m pytest tests/test_system_index.py tests/test_initiative.py tests/test_self_experiments.py -q -p no:cacheprovider)",
"Bash($V *)",
"Bash(dig +short themajesticnetwork.com)",
"Bash(dig +short api.themajesticnetwork.com)",
"Bash(timeout 900 /tmp/claude-1000/-home-maji-Documents-Bolt-Pet/58f4a7c3-6a92-47ed-9e43-bb2216d8b135/scratchpad/tmnvenv/bin/python -m pytest tests/test_site.py -q -p no:cacheprovider)",
"Bash(curl -s -o /dev/null -w 'HTTP %{http_code} bytes=%{size_download}\\\\n' -m 15 -H 'X-Forwarded-For: 1.2.3.4' -H 'X-Real-IP: 1.2.3.4' -A 'Mozilla/5.0 \\(X11; Linux x86_64\\) Firefox/152.0' https://themajesticnetwork.com/?claude-probe-__TRACKED_VAR__)"
] ]
} }
} }
+13 -5
View File
@@ -42,6 +42,8 @@ import json
import re import re
from typing import Iterable, Optional from typing import Iterable, Optional
from . import relay_json
_PREFIXES = ("dialoguectl", "dialogue", "scene") _PREFIXES = ("dialoguectl", "dialogue", "scene")
# ElevenLabs Text to Dialogue limits (docs, 2026-07): at most 10 distinct # ElevenLabs Text to Dialogue limits (docs, 2026-07): at most 10 distinct
@@ -82,11 +84,14 @@ def parse(command: str) -> Optional[dict]:
'dialoguectl needs a JSON argument, e.g. dialoguectl {"lines": ' 'dialoguectl needs a JSON argument, e.g. dialoguectl {"lines": '
'[{"voice": "self", "text": "[cheerfully] hello"}]}' '[{"voice": "self", "text": "[cheerfully] hello"}]}'
) )
# Same lenient parse as filectl (see relay_json): machine-written JSON
# fails in a handful of repeatable ways, and a stray quote shouldn't cost
# a turn — but the repair is reported back rather than hidden.
try: try:
data = json.loads(payload) data, repairs = relay_json.loads(payload)
except json.JSONDecodeError as exc: except relay_json.RelayJsonError as exc:
raise DialogueError( raise DialogueError(
f"couldn't parse the JSON ({exc}). It must be one line of compact " f"couldn't parse the JSON {exc}\nIt must be one line of compact "
"JSON — put line breaks inside text as \\n, never as real newlines." "JSON — put line breaks inside text as \\n, never as real newlines."
) from exc ) from exc
if not isinstance(data, dict): if not isinstance(data, dict):
@@ -109,6 +114,8 @@ def parse(command: str) -> Optional[dict]:
lines.append({"voice": voice, "text": text}) lines.append({"voice": voice, "text": text})
action = {"action": "dialogue", "lines": lines} action = {"action": "dialogue", "lines": lines}
if repairs:
action["_repairs"] = repairs
model = str(data.get("model") or data.get("model_id") or "").strip() model = str(data.get("model") or data.get("model_id") or "").strip()
if model: if model:
action["model"] = model action["model"] = model
@@ -211,9 +218,10 @@ def describe(action: dict, *, played: bool = True) -> str:
"""The tool-result string handed back to the server.""" """The tool-result string handed back to the server."""
lines = action.get("lines") or [] lines = action.get("lines") or []
voices = sorted({str(line.get("voice") or "self") for line in lines}) voices = sorted({str(line.get("voice") or "self") for line in lines})
note = relay_json.repair_note(action.get("_repairs") or [])
if not played: if not played:
return f"[dialogue] not played ({len(lines)} lines)" return f"[dialogue] not played ({len(lines)} lines){note}"
return ( return (
f"[dialogue] played {len(lines)} line{'s' if len(lines) != 1 else ''} " f"[dialogue] played {len(lines)} line{'s' if len(lines) != 1 else ''} "
f"in {len(voices)} voice{'s' if len(voices) != 1 else ''}: {', '.join(voices)}" f"in {len(voices)} voice{'s' if len(voices) != 1 else ''}: {', '.join(voices)}{note}"
) )
+26 -16
View File
@@ -52,6 +52,8 @@ import json
from pathlib import Path from pathlib import Path
from typing import Optional from typing import Optional
from . import relay_json
_PREFIXES = ("filectl", "file") _PREFIXES = ("filectl", "file")
# Keeps a runaway read/write/list from blowing up the tool_result relay (and, # Keeps a runaway read/write/list from blowing up the tool_result relay (and,
@@ -95,17 +97,23 @@ def parse(command: str) -> Optional[dict]:
if not rest or rest.lower() in ("help", "-h", "--help"): if not rest or rest.lower() in ("help", "-h", "--help"):
return {"action": "help"} return {"action": "help"}
# Lenient on purpose — see relay_json. A stray quote in machine-written
# JSON should not cost a turn, but the repair is reported back so the model
# is told it sent something broken while it can still learn from it.
try: try:
payload = json.loads(rest) payload, repairs = relay_json.loads(rest)
except json.JSONDecodeError as exc: except relay_json.RelayJsonError as exc:
raise FileOpError(f"couldn't parse filectl JSON ({exc}); usage:\n{HELP}") from exc raise FileOpError(f"couldn't parse filectl JSON {exc}\nusage:\n{HELP}") from exc
if not isinstance(payload, dict): if not isinstance(payload, dict):
raise FileOpError(f"filectl payload must be a JSON object; usage:\n{HELP}") raise FileOpError(f"filectl payload must be a JSON object; usage:\n{HELP}")
op = str(payload.get("op") or "help").lower() op = str(payload.get("op") or "help").lower()
# Carried on the action so execute() can tell the model what it got wrong;
# a silent repair would fix today's call and guarantee tomorrow's.
tag = {"_repairs": repairs} if repairs else {}
if op == "help": if op == "help":
return {"action": "help"} return {"action": "help", **tag}
if op == "list": if op == "list":
path = _required_str(payload, "path") path = _required_str(payload, "path")
@@ -114,7 +122,7 @@ def parse(command: str) -> Optional[dict]:
raise FileOpError('"pattern" must be a non-empty string') raise FileOpError('"pattern" must be a non-empty string')
return { return {
"action": "list", "path": path, "action": "list", "path": path,
"pattern": pattern, "recursive": bool(payload.get("recursive")), "pattern": pattern, "recursive": bool(payload.get("recursive")), **tag,
} }
if op == "read": if op == "read":
@@ -122,14 +130,14 @@ def parse(command: str) -> Optional[dict]:
return { return {
"action": "read", "path": path, "action": "read", "path": path,
"start": _line_number(payload.get("start"), "start"), "start": _line_number(payload.get("start"), "start"),
"end": _line_number(payload.get("end"), "end"), "end": _line_number(payload.get("end"), "end"), **tag,
} }
if op == "write": if op == "write":
path = _required_str(payload, "path") path = _required_str(payload, "path")
if payload.get("content") is None: if payload.get("content") is None:
raise FileOpError('write needs "content"') raise FileOpError('write needs "content"')
return {"action": "write", "path": path, "content": str(payload["content"])} return {"action": "write", "path": path, "content": str(payload["content"]), **tag}
if op == "edit": if op == "edit":
path = _required_str(payload, "path") path = _required_str(payload, "path")
@@ -138,7 +146,7 @@ def parse(command: str) -> Optional[dict]:
old, new = str(payload["old"]), str(payload["new"]) old, new = str(payload["old"]), str(payload["new"])
if old == new: if old == new:
raise FileOpError("old and new text are identical — nothing to edit") raise FileOpError("old and new text are identical — nothing to edit")
return {"action": "edit", "path": path, "old": old, "new": new} return {"action": "edit", "path": path, "old": old, "new": new, **tag}
raise FileOpError(f"unknown filectl op {op!r}; usage:\n{HELP}") raise FileOpError(f"unknown filectl op {op!r}; usage:\n{HELP}")
@@ -175,14 +183,16 @@ def execute(action: dict) -> str:
if kind == "help": if kind == "help":
return HELP return HELP
if kind == "list": if kind == "list":
return _do_list(action) output = _do_list(action)
if kind == "read": elif kind == "read":
return _do_read(action) output = _do_read(action)
if kind == "write": elif kind == "write":
return _do_write(action) output = _do_write(action)
if kind == "edit": elif kind == "edit":
return _do_edit(action) output = _do_edit(action)
return "[filectl] ok" else:
output = "[filectl] ok"
return output + relay_json.repair_note(action.get("_repairs") or [])
def _resolve(path_str: str) -> Path: def _resolve(path_str: str) -> Path:
+148
View File
@@ -0,0 +1,148 @@
"""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.)"
)
+63
View File
@@ -264,3 +264,66 @@ def test_write_creates_parent_directories(tmp_path):
action = file_ops.parse(_cmd({"op": "write", "path": str(target), "content": "hi"})) action = file_ops.parse(_cmd({"op": "write", "path": str(target), "content": "hi"}))
file_ops.execute(action) file_ops.execute(action)
assert target.read_text() == "hi" assert target.read_text() == "hi"
# ── lenient JSON (see relay_json) ───────────────────────────────────────────
# Machine-written JSON fails in a small, repeatable set of ways. Observed live
# 2026-07-30: a stray quote after `false` cost a whole desk turn — rejected,
# re-sent identically, rejected again, then abandoned with a promise to the
# user that nothing fulfilled.
def test_the_stray_quote_that_cost_a_live_turn_now_parses():
action = file_ops.parse(
'filectl {"op":"list","path":"/home/maji/Documents","pattern":"*","recursive":false"}'
)
assert action["action"] == "list"
assert action["path"] == "/home/maji/Documents"
assert action["recursive"] is False
assert action["_repairs"] == ["removed a stray quote after a bare value"]
@pytest.mark.parametrize("payload,expected", [
('{"op": "read", "path": "/tmp/a.txt",}', "removed a trailing comma"),
("{'op': 'read', 'path': '/tmp/a.txt'}", "converted single-quoted strings to double-quoted"),
('{“op”: “read”, “path”: “/tmp/a.txt”}', "replaced smart quotes with straight ones"),
('```json {"op": "read", "path": "/tmp/a.txt"} ```', "stripped a markdown code fence"),
])
def test_common_model_json_mistakes_are_repaired(payload, expected):
action = file_ops.parse("filectl " + payload)
assert action["action"] == "read"
assert action["path"] == "/tmp/a.txt"
assert expected in action["_repairs"]
def test_python_literals_are_converted():
action = file_ops.parse('filectl {"op": "list", "path": "/tmp", "recursive": True}')
assert action["recursive"] is True
def test_a_repair_is_reported_in_the_output_never_hidden(tmp_path):
"""Silently fixing it would work today and guarantee the same broken call
tomorrow — the model has to be told while it still has the turn."""
(tmp_path / "a.txt").write_text("hello", encoding="utf-8")
action = file_ops.parse(
'filectl {"op":"list","path":"%s","recursive":false"}' % tmp_path
)
output = file_ops.execute(action)
assert "a.txt" in output
assert "your JSON was malformed" in output
assert "stray quote" in output
def test_valid_json_gets_no_repair_note(tmp_path):
(tmp_path / "a.txt").write_text("hello", encoding="utf-8")
action = file_ops.parse('filectl {"op": "list", "path": "%s"}' % tmp_path)
assert "_repairs" not in action
assert "malformed" not in file_ops.execute(action)
def test_genuinely_unparseable_json_points_at_the_character():
""""Expecting ',' delimiter: char 74" is not something a model can act on."""
with pytest.raises(file_ops.FileOpError) as excinfo:
file_ops.parse('filectl {"op": "read", "path": "/tmp/a.txt" "extra": 1}')
message = str(excinfo.value)
assert "^" in message # caret under the offending character
assert '"extra"' in message # ...and the fragment around it