CedarCopilot wants to merge 3 commits into staging from fix/company-name-stored-as-domain
Not deployed
Libra has no production signal for this change yet because it has not deployed. Libra checks hourly for 3 days after each deploy.
selectPrimaryCompany takes whatever the domain-identification agent puts in
primaryDomain and runs it through normalizePrimaryDomain:
apps/server/src/mastra/utils/company-domain-assignment.ts:542 (pre-change) , normalizePrimaryDomain(result.primaryDomain ?? ''), returned at line 550 as finalPrimaryCompany.apps/server/src/services/crm/utils.ts:277 , normalizePrimaryDomain ends return registered || hostOnly;, where registered = getDomain(hostOnly, GET_DOMAIN_OPTIONS).When the agent answers with a company NAME rather than a domain , infoports,
ws strategy, eluve-inc, skyline , getDomain returns null, the raw string
falls through verbatim, and findOrCreateCompanyRelationship stores it as a
company domain. The zod schema for primaryDomain
(company-domain-assignment.ts:60-65)
is z.string().nullable() with prose only, so nothing rejects it.
232 such rows exist across 48 users. 39 were created in Aug 2026 and 9 in Sep 2026 , it is ongoing, not historical.
This is the strongest argument for a guard, and it is why the population is not static.
findOrCreateCompanyRelationship
inserts the value into companies_global as both domain and name. On a later run
lookup-company-fuzzy's name lookup
(lookupCompanyFuzzyTool.ts:394-404)
matches that row and returns {found: true, domain: "preet patel"} to the agent , which is
then handed its own earlier mistake as evidence that the name is a domain.
A parallel eval established that the prompt/schema route does not fix this. Schema-only moved one case by one trial in eight; schema plus instruction fixed one case and failed its near-identical sibling. The guard is deterministic; the instruction is not.
The 232 split three ways, and only one of them is the defect this PR closes. The other two are the reason the change is narrow:
| population | rows | what it is |
|---|---|---|
| no external sender on the conversation | 175 | The domain was genuinely unknowable. <email> is an insurance broker whose prospects are small LLCs with no counterparty address; his conversations carry only his own <email> and are named pristine cleaning professionals llc, quote trade market llc, seafarer consulting llc. The business name is what gives each prospect a distinct identity , forcing these to null or "Unknown" would collapse a real customer's whole book into one company, strictly worse than today. Preserved exactly. |
| the only external sender is a vendor/notifier | 43 | zoom.us ×15, otter.ai ×8, google ×5, fathom ×3, gong ×2, calendly ×2, circleback ×2, plus spotdraft/swapcard/linkedin/stripe/commonpaper. The agent invented a company from a display name or subject. These land on the vendor domain under this change, which is the correct outcome: the company row records zoom.us instead of a company minted from a display name. Note this fixes the COMPANY DOMAIN only. Whether the conversation itself reaches the vendor bucket is decided in assignEventToConversationTool, which this PR does not touch. They belong to Step 3 of the workstream, and vendor domains are deliberately not special-cased out of this path. |
| the external sender is a real counterparty | 14 | The genuine defect. |
That constraint is what determines where the fix goes. normalizePrimaryDomain is
unchanged: its || hostOnly fallback is precisely what the 175 depend on, and it is
shared with external-CRM paths that have no participants to consult. The
Libra has not measured any production surfaces for this change yet.