Libra CodeHub

CedarCopilot/cedar-mail

Branch: staging

fix(crm): don't abort agent runs on duplicate participant contact

merged#2520CedarCopilot

CedarCopilot wants to merge 0 commits into staging from fix/sherlock-preexec-contact-dup-abort-20260818-112553

Live on prod, no production signal yetTimeline and evidence
  1. Opened
    Aug 18, 2026, 11:29 AM
  2. Merged
    Aug 18, 2026, 4:07 PM
  3. Live on staging
    Aug 18, 2026, 4:07 PM
  4. Live on prod
    Aug 18, 2026, 4:07 PM
  5. Observed 2 days
    Aug 18, 2026, 4:07 PM
  6. Pipelines steady after this deploy
    Aug 18, 2026, 4:07 PM
  7. Pipelines steady after this deploy
    Aug 18, 2026, 4:07 PM
  8. Unobserved

    Live on prod, no production signal yet

    Aug 21, 2026, 2:50 AM

Behaviors Libra is checking

CRM contact creation recovers existing participant contacts when a PostgreSQL duplicate-person violation omits or leaves blank its constraint field but names the constraint in the error message.Inconclusivelow confidence

OTEL fallback found no prod spans matching findOrCreateContact since deploy.

prod, checked Aug 21, 2026, 2:49 AM
Meeting events remain created and downstream agent runs continue when linking any organizer or participant contact/company fails, while other participants are still processed.Inconclusivelow confidence

OTEL fallback found no prod spans matching findOrCreateMeetingEvent since deploy.

prod, checked Aug 21, 2026, 2:50 AM
Pull-request CI uses oxlint 1.78.0 so lint checks remain stable instead of failing on newly introduced upstream default rules.Inconclusivelow confidence

OTEL fallback found no prod spans matching GitHub Actions CI / Lint JS since deploy.

staging, checked Aug 21, 2026, 2:52 AM
CRM contact creation recovers from duplicate user-person contact violations even when a database pooler reports the constraint as empty or only embeds its name in the error message.Inconclusivelow confidence

OTEL fallback found no prod spans matching findOrCreateContact since deploy.

staging, checked Aug 21, 2026, 2:52 AM
Meeting events remain created and downstream agent runs continue when linking any organizer or participant contact/company relationship fails.Inconclusivelow confidence

OTEL fallback found no prod spans matching findOrCreateMeetingEvent since deploy.

staging, checked Aug 21, 2026, 3:13 PM

Libra has verdicts on 0 of 2 tracked behaviors on prod; 2 are still being checked. Libra checks hourly for 3 days after each deploy.

What was wrong

Event-driven agent runs (drafting, wiki maintainer, crm-updater) were silently aborting at pre-execution-setup whenever linking a meeting/email participant as a contact hit the partial UNIQUE(user_id, person_id) index (uq_crm_contacts_user_person) , i.e. any time a participant is a person the user already has a contact for under a different email (very common: varun@/varun.lahoti@, @company/@other-domain aliases).

findOrCreateContact has a recovery path for exactly this violation, but it only inspected err.constraint / err.constraint_name. Under the Supabase pgbouncer transaction pooler, the violation surfaces with an empty/absent constraint field, so:

  • namedPersonUq → false ('' !== 'uq_crm_contacts_user_person')
  • unnamed23505 → false ('' == null is false)
  • → the 23505 re-threw, and pre-execution-setup treats any create/link error as fatal, killing the whole run before a single trigger executed.

Evidence

  • Execution 154bc0dd-11b9-4258-9daf-468a9dede7c1 (a real user's meeting): pre-execution-setup returned success:false, "Failed to create/link events: duplicate key value violates unique constraint \"uq_crm_contacts_user_person\""; output empty; no wiki proposal generated.
  • 30 executions for that one user aborted identically over 2 days, across meetings and emails, across multiple deals , so the wiki maintainer, drafts, and CRM updates for those events never ran. The meeting/email rows were saved; only the agent work was lost.
  • Live index confirmed partial (WHERE person_id IS NOT NULL) and all of the user's contacts carry a non-null person_id, so every re-contact of a known person trips it.

What changed

  • services/crm/contacts.ts , normalize an empty-string constraint to null and, when the driver/pooler doesn't expose a usable constraint name, fall back to matching uq_crm_contacts_user_person inside err.message. The person-UQ recovery now fires regardless of how the error is surfaced. A named constraint that isn't the person UQ still re-throws (unchanged safety); a truly nameless 23505 still recovers by SQLSTATE elimination (unchanged).
  • services/crm/meeting-events.ts , link participant/organizer contacts best-effort: each link is independent and its failure is logged and skipped. The meeting event is already persisted before this; a contact write can no longer abort event creation. This mirrors the existing best-effort external_crm linking + Turbopuffer upload in the same path.

Blast radius

Shared CRM services → affects all users/orgs. This is intentional: the fatal-abort behavior was hitting any account with multi-email participants, silently. The recovery change only widens when we recover (guarded by personId + the existing "UPDATE must match a row, else re-throw" check); the meeting-events change only makes a previously-fatal side-effect non-fatal.

How to test

  • Re-run an affected execution for the reported user (deal ode.ai, conversation ecbeac31-f9fe-4413-b6a4-95912911fcaf, meeting event c09265f1-…) after deploy and confirm pre-execution-setup succeeds and the wiki-maintainer proposal generates.
  • In agent_tool_calls, pre-execution-setup should return success:true for events whose participants collide on (user_id, person_id).
  • Watch for crm.contacts.personUqRecovery.unnamed warns (should remain rare) and the new non-fatal Failed to link contact … (non-fatal) errors (should be near-zero once recovery works).

🤖 Fixed by Sherlock

Greptile Summary

The PR prevents duplicate participant contacts from aborting event-driven agent runs and makes meeting participant relationship writes best-effort.

  • Normalizes missing or empty database constraint metadata and recognizes the person uniqueness constraint from error messages.
  • Recovers the existing person contact after a duplicate-person conflict.
  • Isolates organizer and participa
Show production surfaces and changed-file mapping

Production surfaces

Libra has not measured any production surfaces for this change yet.

Changed files → surfaces

  • .github/workflows/ci.ymlno production surface mapped
  • apps/server/src/services/crm/contacts.tsno production surface mapped
  • apps/server/src/services/crm/meeting-events.tsno production surface mapped