Skip to content

Cut a long Composio result or failure between characters, not through an emoji - #568

Merged
davidmckayv merged 1 commit into
CopilotKit:mainfrom
kevin9327:cap-composio-result-between-characters
Sep 15, 2026
Merged

davidmckayv merged 1 commit into
CopilotKit:mainfrom
kevin9327:cap-composio-result-between-characters

Conversation

@kevin9327

Copy link
Copy Markdown
Contributor

What this changes

cap in server/src/plugins/composio.ts is the cap every string the Composio transport puts in front of a model goes through: an action's answer (resultOf) and a failure sentence (failure). It cut at MAX_RESULT_CHARS with slice, which counts UTF-16 code units. When the cut landed between the two halves of an emoji, or any other character outside the Basic Multilingual Plane, the text ended on a lone high surrogate. JSON carries that as a bare \ud83d and UTF-8 as U+FFFD, so the model read a broken character that Composio never sent. Mail bodies, chat messages and documents are exactly the answers long enough to reach the cap and likely to hold emoji.

#525 fixed this cut in the MCP transport and both built-in transports with cutAtCodeUnits in channels/text.ts. The Composio transport landed afterwards (#481) with its own slice, so this is the fourth transport's copy of the same cut. It now calls the same helper: one import and one call.

Text that fits, a cut that lands between characters, the [truncated] note and truncated: true are all unchanged. The result is at most one code unit shorter, never longer than the cap.

Where it runs

  • New state that outlives a request? None. A pure string function on the call path.
  • What happens on the second replica? Same as the first; nothing is held.
  • 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. The call path is untouched; only the text of an answer that was already being cut changes.
  • New refusals and new failures each write a row. None are added.
  • Nothing new is trusted from the client that the server can resolve itself.

Changelog

  • A line in CHANGELOG.md under Unreleased.

Unreleased was empty, so this entry sits at its top. If another PR lands there first, I'm happy to rebase.

Proof

New test in server/tests/composio-transport.test.ts: "a result or a failure over the cap is cut between characters, not through an emoji". It drives both answers through callTool with an emoji whose high surrogate lands on the cap's last unit. The result goes through JSON.stringify(data, null, 2), and the test accounts for its 12-unit opening.

On main without the fix, both come back cut through the emoji:

error: expect(received).toEqual(expected)

  [
-   "result: truncated true, marked true, kept 19999, ends on a lone surrogate false",
-   "failure: truncated true, marked true, kept 19999, ends on a lone surrogate false",
+   "result: truncated true, marked true, kept 20000, ends on a lone surrogate true",
+   "failure: truncated true, marked true, kept 20000, ends on a lone surrogate true",
  ]
(fail) calling one action > a result or a failure over the cap is cut between characters, not through an emoji

With the fix:

  • bun test tests/composio-transport.test.ts tests/mcp-result.test.ts: 122 pass, 0 fail. This includes the existing boundary test ("a result the exact size of the cap is not cut, and one character more is") and "a result is capped visibly rather than silently", both unchanged.
  • bun run typecheck (app, server, worker): exit 0.
  • bunx biome check on the changed files: clean.

🤖 Generated with Claude Code

… an emoji

The Composio transport caps every string it puts in front of a model at
MAX_RESULT_CHARS in `cap`, for an action's answer and for a failure
sentence alike, and it cut with `slice`, which counts UTF-16 code units.
When the cap landed between the halves of a surrogate pair, the text the
model read ended on a lone high surrogate: JSON carries it as a bare
`\ud83d` and UTF-8 encodes it as U+FFFD, so the model was handed a broken
character that was never in what Composio sent.

The MCP and built-in transports have stopped one code unit short in this
case since CopilotKit#525, through `cutAtCodeUnits`. The Composio transport arrived
after that with its own cut and now uses the same helper. Text that fits,
and a cut that lands between characters, are unchanged, and so is the
note saying the answer 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.

@davidmckayv davidmckayv left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code-verified clean; CI green on this sha.

@davidmckayv
davidmckayv merged commit 775a9e5 into CopilotKit:main Sep 15, 2026
15 checks passed
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.

2 participants