Skip to content

Fix DirtyJson decoding \u surrogate pairs as unpaired surrogates#1774

Open
King0James0 wants to merge 1 commit into
agent0ai:mainfrom
King0James0:fix/dirty-json-surrogate-pairs
Open

Fix DirtyJson decoding \u surrogate pairs as unpaired surrogates#1774
King0James0 wants to merge 1 commit into
agent0ai:mainfrom
King0James0:fix/dirty-json-surrogate-pairs

Conversation

@King0James0

Copy link
Copy Markdown

Fixes #1773.

Fixes the UnicodeEncodeError: surrogates not allowed crash class described in #1773 (also the actual root cause of #1384).

Problem

DirtyJson._parse_string decodes each \uXXXX escape independently, so a surrogate-pair escape like \ud83c\udf78 (U+1F378, the legal JSON encoding any conforming serializer produces for non-BMP characters) parses into two lone UTF-16 surrogates. The string then crashes at the first .encode("utf-8") downstream — observed live in code_execution_tool (tty_session.send) as "Critical error occurred, retrying...", and the same poison hits print_style log writes (#1384). Every tool envelope goes through this parser via extract_tools.json_parse_dirty, so any model that escape-encodes an emoji in a code/text arg trips it.

Change

  • After decoding a \uXXXX escape to a high surrogate (D800-DBFF), look ahead for an immediately following \uXXXX low-surrogate escape and combine the pair into the real code point — the same behavior as json.loads.
  • Any remaining unpaired surrogate (lone high, lone low) becomes U+FFFD, so the parser can never emit a string that is not UTF-8-encodable. (json.loads passes lone surrogates through; given every consumer here eventually encodes to UTF-8, emitting the replacement character instead of a guaranteed downstream crash seemed the right call for this parser — happy to match json.loads exactly instead if preferred.)
  • BMP escapes (\u00e9, \u2713, ...) and the existing invalid-hex literal fallback are unchanged.

Tests

Four new cases in tests/test_dirty_json.py: pair combining (with an encode assertion on the exact crash site), lone high/low replacement, BMP escapes unchanged, and a high surrogate followed by plain text. python -m pytest tests/test_dirty_json.py passes 11/11.

DirtyJson._parse_string decodes each \uXXXX escape independently, so an
emoji written in its legal JSON escape form (e.g. \ud83c\udf78 for
U+1F378) parses into two lone UTF-16 surrogate code points instead of
one character. Python strings tolerate lone surrogates, but UTF-8 does
not: the parsed string raises UnicodeEncodeError (surrogates not
allowed) at the first .encode("utf-8") downstream - observed live as
code_execution_tool crashing in tty_session.send ("Critical error
occurred, retrying..."), and the same poison crashes print_style HTML
log writes (agent0ai#1384).

Combine high+low surrogate escape pairs into the real character,
matching json.loads. Any remaining unpaired surrogate becomes U+FFFD so
the parsed string is always UTF-8-encodable. BMP escapes and invalid
hex handling are unchanged.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

DirtyJson decodes \uXXXX surrogate pairs as lone surrogates, crashing tool execution with UnicodeEncodeError

1 participant