Skip to content

Match a coworker's connector by name, not as a substring of another word - #293

Open
kevin9327 wants to merge 1 commit into
CopilotKit:mainfrom
kevin9327:fix/routing-reach-word-boundary
Open

Match a coworker's connector by name, not as a substring of another word#293
kevin9327 wants to merge 1 commit into
CopilotKit:mainfrom
kevin9327:fix/routing-reach-word-boundary

Conversation

@kevin9327

Copy link
Copy Markdown
Contributor

What this changes

When the intent router falls back — it is unreachable, its answer does not parse, it names nobody on the roster, or no specialist is a confident match — and exactly one coworker can reach a system the message names, the message is routed to that coworker instead of the default (onlyCoworkerReaching, server/src/routing/classify.ts).

The system id was matched against the message with haystack.includes(...), a bare substring test. So a system id that appears inside a longer word was read as the message naming that system:

  • "how do I deal with a slacker on my team" → matched the slack connector
  • "escribe un cuento sobre una jirafa" (a giraffe) → matched the jira connector

A message that named neither system was routed to that specialist — and because a fallback pins the channel to one coworker for the life of the thread, it misrouted the whole conversation to a Bot that could not answer it. Every untagged message takes the fallback path when the router endpoint is down, which is the state that surfaced it.

The fix matches the id on word boundaries: it has to sit bounded by a non-alphanumeric character or an edge of the message, with the id's own characters taken literally (so a custom server id with regex metacharacters is safe). Separator loosening is unchanged — google-drive still answers to "google drive" — and a system named on its own still routes to its holder.

Reproduced before the fix (all now pass): "slacker" and "jirafa" both fell to the connector holders; after, both stay on the default, while "post this to slack" still routes to the Slack coworker.

A name that genuinely appears as its own word for another reason — a linear connector and "linear regression", a notion connector and "no notion" — is a limit of using lexical presence as a reach hint, not this substring defect, and is left as is.

Where it runs

onlyCoworkerReaching / messageNames / bounded are pure string functions with no state. They run inside the API's routing decision (server/src/app.ts wires createIntentRouter), per message.

  • New state that outlives a request? None.
  • What happens on the second replica? Identical: pure functions of their inputs.
  • Anything serialised? N/A.
  • Anything fanned out to a browser? No.
  • New listener, port, or schedule? None.

Boundary and audit

  • Every acting call still goes through the gateway: routing decides which coworker answers; it does not act. Unchanged.
  • New refusals and new failures each write a row: N/A — the undecided reason is still recorded exactly as before; this only changes which coworker a reach fallback names.
  • Nothing new is trusted from the client that the server can resolve itself: the reach set is still computed server-side; this only tightens how it is matched.

Changelog

  • Added under Unreleased: "A message no longer routes to a specialist because a longer word contained a connector's name".

Proof

Verified locally (bun 1.3.14):

  • bun test server/tests/routing-classify.test.ts27 pass / 0 fail, including the new cases: a system id inside an unrelated word ("slacker", "jirafa") is not a match and stays on the default; the same system named on its own still routes to its holder. Every pre-existing routing test (positive reach match, hyphen-as-space, two-holder default, confident-match-wins) still passes.
  • cd server && bun run typecheck — clean.
  • bunx biome format / bunx biome lint on the changed source and test — clean.

(Server integration tests that need Postgres fail locally with Connection closed; that is the absent local DB, not this change. The routing suite is pure and needs none.)

When the intent router falls back and exactly one coworker can reach a system
the message names, the message is routed to that coworker. onlyCoworkerReaching
matched the system id with `haystack.includes(...)`, a bare substring test.

So "how do I deal with a slacker" matched the `slack` connector, and "escribe
un cuento sobre una jirafa" — a giraffe — matched `jira`. A message that named
neither system was read as naming one, and because a fallback pins the channel
to one coworker for the life of the thread, it misrouted the whole conversation
to a specialist that could not answer it. Every untagged message takes this
path when the router endpoint is down, which is the case that surfaced it.

The id is now matched on word boundaries: bounded by a non-alphanumeric
character or an edge of the message, with the id's own characters taken
literally. A system named on its own still routes to its holder, and one buried
inside a longer word does not. Separator loosening is unchanged, so google-drive
still answers to "google drive".

Word boundaries do not settle a name that genuinely appears as its own word for
another reason (a `linear` connector and "linear regression"); that is a limit
of a lexical reach hint, not this substring defect, and is left as is.

Co-Authored-By: Claude Opus 4.8 <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