Libra CodeHub

CedarCopilot/cedar-mail

Branch: staging

fix(crm): seed mapped Cedar fields when a deal is first linked

merged#2712CedarCopilot

CedarCopilot wants to merge 3 commits into staging from fix/seed-crm-fields-on-first-link

Live on prod, no production signal yetTimeline and evidence
  1. Opened
    Sep 6, 2026, 9:37 PM
  2. Sep 6, 2026, 10:05 PM
  3. Merged
    Sep 6, 2026, 11:10 PM
  4. Live on prod
    Sep 6, 2026, 11:10 PM
  5. Observed 9 hours
    Sep 6, 2026, 11:10 PM
  6. Unobserved

    Live on prod, no production signal yet

    Sep 6, 2026, 11:10 PM
  7. Pipelines steady after this deploy
    Sep 6, 2026, 11:10 PM

Behaviors Libra is checking

When the link-time CRM mirror write fulfills or supersedes a pending field-approval card, the card is marked `done` when its proposed value matches the CRM value or `agent_deleted` when the CRM value differs.Not checked
prod
The first-link field seed runs only after the CRM event has been linked and the deal is present in conversation integration metadata, allowing the active-deal guard to accept the snapshot as belonging to that conversation.Not checked
prod
If the workflow fails after durably linking the deal, the handler discovers the linked conversation and still seeds its CRM fields, while preserving the original workflow failure when no link exists or the recovery lookup fails.Not checked
prod
When a CRM deal is linked to a Cedar conversation for the first time, Cedar populates the full effective CRM-to-Cedar mapping from the deal snapshot, including status, name, value, close date, and custom fields, so deals that arrive already terminal do not retain empty Cedar fields.Not checked
prod
A deal is seeded once for an already-linked conversation, while a workflow that merges the deal into a different conversation also seeds that new target without duplicating writes to the original target.Not checked
prod
The post-link snapshot seed is a one-way CRM-to-Cedar mirror that does not dispatch field-change playbooks or push the already-existing CRM value back to the CRM.Not checked
prod

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

The bug

When a CRM deal is linked to a conversation, Cedar never applies its CRM to Cedar field mappings, so crm_conversations.status stays NULL for any deal that was already at its final stage the first time Cedar saw it.

The mechanism, in order:

  1. crm-discover-new-deals.ts:234 (and the hourly background sync behind it) calls executeExternalCrmAgentExecutionDirect({ ..., conversationId: undefined }) , by definition, a deal with no conversation yet.
  2. handleExecuteExternalCrm.ts:359 gates the programmatic mapping step on if (conversationId). With no conversation it is skipped.
  3. The workflow resolves and links a conversation (assignEventToConversationTool.ts:1246), and the post-execution self-heal link at handleExecuteExternalCrm.ts:617 re-links it , but neither applies field mappings. linkExternalCrmEventToConversation has zero references to them, and its details argument carries only dealStage as a display string, not a deal snapshot the mapper could read.
  4. A closed deal is never modified again, so no later CRM event arrives to correct it. status is NULL permanently.

The pile of approval cards asking a rep to approve a stage the CRM already holds is the agent correctly noticing an empty status. It is downstream of this, not the cause.

Scale (read-only queries against prod, today)

92 conversations across 18 users / 8 orgs have a linked deal whose integration_metadata carries a real dealStage while crm_conversations.status is NULL , Pirros 35, Corgi 25, Getdynasty 13, AspireIQ 12, the rest in ones and twos.

Not all 92 are this bug, and the split matters for the backfill:

  • 23 are seedable by the mappings as configured today (Pirros 21 , peter 11, keenan 5, xander 5 , plus Warp 2): the connection maps dealstage to status and the stage label matches an AOP status option.
  • ~44 have no status field mapping at all on the connection (Corgi's two users, 25 conversations; Dynasty; parts of AspireIQ). Configuration, not code.
  • The remainder have a stage label with no matching AOP option (Plan Review, Appointment Scheduled, Closed Won - Renewal), so status-match resolves to null.

A representative case: Pirros conversation be9d87b5 (ODA - 1), created 2026-08-31, deal 15172683409 linked the same day already isClosed: true at Lost - Company, one completed external_crm execution, no events since, status still NULL.

The fix

syncCedarFieldsFromCrmSnapshot (crm-pull-refresh.ts) is the "resolve the effective mappings, apply the whole snapshot, swallow failures" triple every call site needs, extracted so the error handling exists once. handleExecuteExternalCrm now applies it in three places, all via one local seedFieldsOnFirstLink helper that no-ops on a conversation the pre-step already seeded:

  • the existing pre-step, unchanged in behavior;
  • after the post-execution link, for a conversation the run resolved itself (this also covers a workflow that merges into a different conversation than the one seeded); and
  • after a FAILED run. The link is made inside the workflow, before whatever step failed, and it is durable , but the failure result carries no conversation id, so the branch asks externalCrmEventHasConversation which conversation now holds the deal and seeds that. Without it a newly discovered terminal-stage deal could stay unseeded permanently on any run that linked and then failed. (Greptile P1, fixed in dd3a769b1.)

Two consequences worth stating up front

The seed applies the FULL mapping set, not just the stage. On a real conversation that is 28 fields , status, closeDate, dealValue, name, segment, firm_size, sql_date, dq_reason, type_of_firm, drawing_software, product_modules… , and it renames a domain-named conversation (svdesign.com) to the CRM's deal name (SV Design - 1). This is deliberate: it is the same set every other CRM to Cedar path applies, the CRM is author

Show production surfaces and changed-file mapping

Production surfaces

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

Changed files → surfaces

  • apps/server/src/mastra/routeHandlers/event-execution/__tests__/handleExecuteExternalCrm.link-field-seed.test.tsno production surface mapped
  • apps/server/src/mastra/routeHandlers/event-execution/__tests__/handleExecuteExternalCrm.test.tsno production surface mapped
  • apps/server/src/mastra/routeHandlers/event-execution/handleExecuteExternalCrm.tsno production surface mapped
  • apps/server/src/services/crm/__tests__/conversations.field-approval-reconcile.test.tsno production surface mapped
  • apps/server/src/services/crm/__tests__/crm-field-sync-full.test.tsno production surface mapped
  • apps/server/src/services/crm/__tests__/crm-pull-refresh.field-seed.test.tsno production surface mapped
  • apps/server/src/services/crm/crm-pull-refresh.tsno production surface mapped