Skip to content

Ask the router about a long message's opening, so it still reaches the coworker it is for - #575

Open
kevin9327 wants to merge 1 commit into
CopilotKit:mainfrom
kevin9327:route-long-message
Open

kevin9327 wants to merge 1 commit into
CopilotKit:mainfrom
kevin9327:route-long-message

Conversation

@kevin9327

Copy link
Copy Markdown
Contributor

What this changes

Since #548, POST /api/route refuses a message over 10,000 characters so the prompt it builds stays bounded. The composer has no such limit, so a pasted email thread, contract or log is one message. Both callers of routeMessage carry on past a routing that failed, so the 400 never showed on screen:

Where the message was written Before After
Home composer, no @ catch in _app/index.tsx sends it to the default coworker, under the line "Sent to the coworker it is for". No channel.routed row. Routed on its opening to the coworker it is for, and recorded.
Home composer with @, or the To: field on /channel/new startWithChosen swallows the 400. The channel starts with no channel.routed row, although the named branch never reads the text. Recorded as the person's choice (viaMention: true).

routeMessage now sends the message's opening: trimmed, as the route trims, cut to 10,000 characters, and one unit short when the cut would split a character, so the router is not handed half of an emoji. Who a message is for is plain from its opening. The whole message still goes to the coworker, because both callers pass draft.text to start unchanged. A message that fits is sent as before.

The route's cap and its test from #548 are untouched. The fix is in the client that was sending more than the route accepts. The channel.routed payload never carried the text, so the audit row is the same either way.

Where it runs

  • New state that outlives a request? None.
  • What happens on the second replica? The same. This is a pure change to what the browser sends.
  • 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. Routing is not an acting call, and the roster check is unchanged.
  • New refusals and new failures each write a row. None are added. This restores the channel.routed row that long messages had stopped writing.
  • Nothing new is trusted from the client that the server can resolve itself. The server still enforces its cap.

Changelog

  • A line in CHANGELOG.md under Unreleased.

Proof

New app/tests/route-long-message.test.ts. It calls the real routeMessage with fetch answered by the real createRoutingRoutes handler (the pattern agent-api-path.test.ts uses), so the cap it holds against is the server's own. The router and audit store are fakes that record what they were asked and what was written.

On main (only the test file added):

error: A message of at most 10000 characters is required.
      at client (app\src\lib\client.ts:96:15)
      at async routeMessage (app\src\lib\channels\route.ts:28:26)
(fail) a message longer than the route reads is routed to the coworker it is for, and recorded
(fail) a long message to a coworker the person chose is recorded as their choice
(fail) the opening is cut between characters, not through an emoji

 1 pass
 3 fail

With the fix:

  • bun test app/tests/route-long-message.test.ts app/tests/start-chosen.test.ts: 8 pass, 0 fail. The four new cases cover the routed branch, the chosen branch, a cut that would split an emoji, and a message that fits.
  • bun test app/: 854 pass, 29 fail. On main it is 850 pass with the same 29 failures, all from this Windows machine: serve.test.ts port and websocket cases, Windows path separators, and a relative-time assertion reading a Korean locale.
  • cd server && bun test tests/routing-routes.test.ts tests/routing-limits-validation.test.ts: 17 pass, 0 fail.
  • bun run typecheck (app, server, worker): exit 0.
  • bunx biome check on the changed files: clean.

Every changed line runs in the new tests: both body shapes, the fits path, and the cut with and without the surrogate step.

This PR adds its line at the top of ## Unreleased, the same place other open PRs add theirs, so the changelog may conflict. I'm happy to rebase.

🤖 Generated with Claude Code

…e coworker it is for

`POST /api/route` refuses a message over 10,000 characters since CopilotKit#548,
so the prompt it builds stays bounded. The composer has no such limit,
and both callers of `routeMessage` carry on past a routing that failed.
The home composer caught the 400 and sent the message to the default
coworker, under a line that says it was "Sent to the coworker it is
for". `startWithChosen` swallowed it, so a coworker chosen with `@` or
from the To: field started with no `channel.routed` row, even though
that branch never reads the text.

`routeMessage` now sends the trimmed opening, cut to the route's limit
and one unit short when the cut would split a character. Who a message
is for is plain from its opening, and the whole message still goes to
the coworker. A message that fits is sent as before.

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