runtime: make valid UTF-8 the JSON emitter's contract #148
Reference in New Issue
Block a user
Delete Branch "fix/utf8-truncation"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Three nodes in the live graph carry labels truncated to exactly 80 bytes ending in a lone
0xE2— the first byte of an em-dash, cut mid-sequence.jb_emit_escapedcopied every byte ≥ 0x20 through verbatim, so those three nodes made the entire/api/nodes/listresponse undecodable and no strict parser could read the graph at all.The damage was not written by this runtime
No 80-byte truncation exists here (the only label truncation is
engram_first_n_charsat 60), and the content of those nodes is 2572 and 2746 bytes. Some other producer wrote them.That is exactly why fixing a writer could not have fixed this: the store already holds the damage, and it accepts data from importers, other producers and older binaries. So the fix goes where the promise is made. A serializer that emits JSON owes valid UTF-8 whatever it is handed.
jb_emit_escapednow validates each multi-byte sequence before emitting any of it, substituting U+FFFD for a bad lead byte, a missing or malformed continuation, an overlong encoding, a UTF-16 surrogate, or a codepoint above U+10FFFF. Invalid bytes are replaced, not dropped, so damage stays visible rather than silently papered over. Well-formed input is byte-identical to before.Second change — preventive, and explicitly not the cause of the above
engram_first_n_charstruncated by bytes despite its name, so content with a multi-byte character crossing byte 60 would produce a half codepoint. It now usesel_utf8_safe_len, which returns the largest byte length ≤ max that does not split a codepoint. Bounded by bytes, not codepoints, so existing labels never grow — they only stop splitting.el_utf8_safe_lenlives besidestr_count_charsrather than in the engram, because the rest of el’s string layer is already codepoint-aware (str_count_charscounts codepoints,str_reversewalks codepoint lengths). Byte truncation was the outlier, and the concern is a string concern.Note on the investigation
I first “fixed” the truncator and wrote a test that passed on the unpatched build too — because
route_create_nodepasseslabel = contentwhen no label is supplied, soengram_first_n_charsis never reached over HTTP. The test proved nothing. The real cause was found only by decoding the actual failing bytes out of the live response.will.anderson referenced this pull request2026-08-16 17:20:41 +00:00