Skip to content

Cut a long tool result or relayed answer between characters, not through an emoji - #525

Open
kevin9327 wants to merge 1 commit into
CopilotKit:mainfrom
kevin9327:fix/tool-result-cut-between-characters
Open

Cut a long tool result or relayed answer between characters, not through an emoji#525
kevin9327 wants to merge 1 commit into
CopilotKit:mainfrom
kevin9327:fix/tool-result-cut-between-characters

Conversation

@kevin9327

Copy link
Copy Markdown
Contributor

What this changes

Four places cut text for a model with slice, which counts UTF-16 code units:

  • resultText in server/src/plugins/mcp.ts, for every MCP tool result over MAX_RESULT_CHARS (20,000)
  • asResult in server/src/plugins/google-drive-rest.ts and server/src/plugins/builtin-routines.ts, the same cap for the two builtin transports
  • clip in server/src/agents/handoff-runner.ts, for an answer relayed back through a handoff over RELAY_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 resultText on main:

truncated: true  last unit: d83d  lone high surrogate: true
JSON tail:       aaaaa\ud83d"
UTF-8 tail bytes: ef bf bd

So the model is handed a broken character that was never in what the tool, or the other Bot, said. extractDocumentText in server/src/channels/attachment-parts.ts already 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 to oneLine in server/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

  • New state that outlives a request? None. A pure function over a string already in hand.
  • What happens on the second replica? Nothing changes; no state.
  • Anything serialised? No.
  • Anything fanned out to a browser? No.
  • New listener, port, or schedule? No.

Boundary and audit

  • Every acting call still goes through the gateway: resolve, decide, audit, then act. Untouched; only the text of a result the gateway already allowed changes.
  • New refusals and new failures each write a row. None added.
  • Nothing new is trusted from the client that the server can resolve itself.

Changelog

  • A line in CHANGELOG.md under Unreleased.

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 through read_file_content), builtin-routines.test.ts (list_routines, with the emoji's offset measured from a probe listing rather than assumed) and agent-handoff-runner.test.ts (next to the existing "clipped, and says so" test). mcp-result.test.ts also gets the case where the emoji ends just inside the limit, so the guard cannot cost a character.

Before the fix:

bun test tests/mcp-result.test.ts tests/google-drive-rest.test.ts tests/builtin-routines.test.ts tests/agent-handoff-runner.test.ts
(fail) relaying the answer home > an answer cut inside a character loses the whole character, not half of it
(fail) a list too long for one result > is cut between characters, never through one
(fail) reading a file asks Drive what it is first > a file too long for one result is cut between characters, never through one
(fail) a result too large to hand a model > a cut that would land inside a character stops one code unit short
- aaaa…aaaa"
+ aaaa…aaaa�"
77 pass, 4 fail

After:

bun test tests/mcp-result.test.ts tests/google-drive-rest.test.ts tests/builtin-routines.test.ts tests/agent-handoff-runner.test.ts tests/channel-one-line.test.ts tests/mcp-protocol.test.ts tests/plugin-catalogue.test.ts
142 pass, 0 fail

bun run typecheck          # app, server, worker: exit 0
bunx biome check <changed files>   # no fixes

🤖 Generated with Claude Code

…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>
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

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.

1 participant