Cut a long tool result or relayed answer between characters, not through an emoji - #525
Open
kevin9327 wants to merge 1 commit into
Open
Conversation
…ugh an emoji A tool result over MAX_RESULT_CHARS (in `resultText` and the two builtin transports' `asResult`) and a handoff answer over RELAY_ANSWER_LIMIT were cut with `slice`, which counts UTF-16 code units. When the limit landed between the halves of a surrogate pair, the text handed to the model ended on a lone high surrogate: JSON carries it as a bare `\ud83d` and UTF-8 encodes it as U+FFFD, so the model read a broken character that was never in what the tool or the Bot said. `extractDocumentText` already stops one code unit short in exactly this case for attachments. The same rule now lives in one helper beside `oneLine` and is used at all four cuts. Text that fits, and a cut that lands between characters, are unchanged, and so are the notes saying a result was cut. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
kevin9327
requested review from
MikeRyanDev,
davidmckayv,
guidovizoso,
mxmzb and
tylerslaton
as code owners
September 13, 2026 22:05
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What this changes
Four places cut text for a model with
slice, which counts UTF-16 code units:resultTextinserver/src/plugins/mcp.ts, for every MCP tool result overMAX_RESULT_CHARS(20,000)asResultinserver/src/plugins/google-drive-rest.tsandserver/src/plugins/builtin-routines.ts, the same cap for the two builtin transportsclipinserver/src/agents/handoff-runner.ts, for an answer relayed back through a handoff overRELAY_ANSWER_LIMIT(12,000)When the limit lands between the two halves of a surrogate pair (any emoji, any astral-plane glyph), the cut text ends on a lone high surrogate. Measured through
resultTextonmain:So the model is handed a broken character that was never in what the tool, or the other Bot, said.
extractDocumentTextinserver/src/channels/attachment-parts.tsalready stops one code unit short in exactly this case for attachments, and its comment spells out the hazard; these four cuts did not.The fix is that same rule in one helper,
cutAtCodeUnits, next tooneLineinserver/src/channels/text.ts, used at all four cuts. It drops the orphaned half rather than completing the pair, so a result never goes over the limit it was given. Text that fits, and a cut that lands between characters, are unchanged, and so are the "truncated" and "cut here for length" notes.Where it runs
Boundary and audit
Changelog
CHANGELOG.mdunderUnreleased.Proof
One test per cut, each placing an emoji so its high half lands on the last unit the limit keeps, in the file that already covers that cut:
mcp-result.test.ts,google-drive-rest.test.ts(a downloaded text file throughread_file_content),builtin-routines.test.ts(list_routines, with the emoji's offset measured from a probe listing rather than assumed) andagent-handoff-runner.test.ts(next to the existing "clipped, and says so" test).mcp-result.test.tsalso gets the case where the emoji ends just inside the limit, so the guard cannot cost a character.Before the fix:
After:
🤖 Generated with Claude Code