Libra CodeHub

CedarCopilot/cedar-mail

Branch: staging

fix(crm): convert Cedar's comma-joined multi-select values to Salesforce's semicolon multipicklist format

merged#2900CedarCopilot

CedarCopilot wants to merge 2 commits into staging from worktree-send-it-sf-multipicklist-fix

Live on prod, 0 of 1 surfaces working, 2 days leftTimeline and evidence
  1. Opened
    Sep 23, 2026, 1:13 AM
  2. Sep 23, 2026, 1:59 AM
  3. Live on staging
    Sep 23, 2026, 2:13 AM
  4. Pipelines steady after this deploy
    Sep 23, 2026, 2:13 AM
  5. Merged
    Sep 23, 2026, 2:13 AM
  6. Live on prod
    Sep 23, 2026, 2:13 AM
  7. Observed 1 hour, 1 surface, 0 requests
    Sep 23, 2026, 2:13 AM
  8. Watching

    Live on prod, 0 of 1 surfaces working, 2 days left

    Sep 23, 2026, 2:13 AM
  9. Pipelines steady after this deploy
    Sep 23, 2026, 2:13 AM

Behaviors Libra is checking

The external CRM workflow reports only fields actually written, propagates failedFields for partial local drops, and excludes both failed and intentionally omitted fields from updatedFields.Not checked
prod
Operators receive an explicit warning when an otherwise-successful CRM write drops or omits fields, including counts and field-level reasons.Not checked
prod
Cedar comma-joined values for Salesforce multipicklist mappings are tokenized against known options and emitted as semicolon-joined Salesforce labels, while non-multipicklist mappings are not comma-split.Not checked
prod
A Salesforce multipicklist option whose value contains commas remains a single option when selected alone, and is combined correctly with other options when selected together.Not checked
prod
Empty or uncoercible boolean fields are omitted from Salesforce writes and reported through omittedFields without being treated as validation failures, including successful no-op batches containing only omissions.Not checked
prod
Salesforce local allowed-value validation drops only invalid fields from an update batch and still sends the remaining valid fields instead of aborting the entire write.Not checked
prod

Failures attributed to this change

No prod customers are affected while this is only in staging. If promoted, prod impact is unknown because Libra still needs a concrete exception, route, and failed user action before assigning severity. 0 hits · no retained affected-user count · no retained trace sample.Introducedmedium confidence

internal_only

staging, first seen Sep 23, 2026, 6:47 AM
No prod customers are affected while this is only in staging. If promoted, prod impact is unknown because Libra still needs a concrete exception, route, and failed user action before assigning severity. 1 hit · no retained affected-user count · no retained trace sample.Introducedmedium confidence

internal_only

staging, first seen Sep 23, 2026, 9:47 AM
No prod customers are affected while this is only in staging. If promoted, prod impact is unknown because Libra still needs a concrete exception, route, and failed user action before assigning severity. 1 hit · no retained affected-user count · no retained trace sample.Introducedmedium confidence

internal_only

staging, first seen Sep 23, 2026, 10:32 AM
No prod customers are affected while this is only in staging. If promoted, prod impact is unknown because Libra still needs a concrete exception, route, and failed user action before assigning severity. 1 hit · no retained affected-user count · no retained trace sample.Introducedmedium confidence

internal_only

staging, first seen Sep 23, 2026, 11:17 AM
No prod customers are affected while this is only in staging. If promoted, prod impact is unknown because Libra still needs a concrete exception, route, and failed user action before assigning severity. 2 hits · no retained affected-user count · no retained trace sample.Introducedmedium confidence

internal_only

staging, first seen Sep 23, 2026, 12:33 PM

Libra found 1 production surface on prod but could not judge any of them yet. 1 surface had no requests at all. Libra checks hourly for 3 days after each deploy.

Summary

  • Cedar's list-type custom fields (e.g. won_reason, pain_points_list, competitors_mentioned) are comma-joined internally, but Salesforce multipicklist fields require one semicolon-delimited string on the wire. getLabelFromAopEnumOptions() only ever recognized an already-semicolon-joined value, so a genuine Cedar multi-select value never converted correctly and failed Salesforce's own picklist validation , with the whole-string-match-before-split guard added so a compound option label (one that legitimately contains a comma) is never corrupted, even when its own comma-fragments happen to also be separately-defined option values.
  • Because updateOpportunityFields() validated every field in a push up front and aborted the whole batch on the first invalid field, one bad multi-select field silently killed unrelated, valid fields in the same push , including closed_won_reason_details/closed_lost_reason_details, the customer-visible symptom this was root-caused from (a Warp rep's deal close-detail fields populating "sometimes" and not others). Per-field validation failures are now isolated: a bad field is dropped and logged, valid fields still reach Salesforce, and the result carries an optional failedFields array , plumbed through driver.ts, the sync workflow, and both tRPC mutations that push CRM field batches (upsertWorkingMemory, updateConversation) so a partial drop is surfaced to the caller instead of silently reporting bare success.

Test plan

  • pnpm --filter @zero/server run types , clean.
  • pnpm --filter @zero/server exec vitest run on all 7 touched/new test files , 69/69 passing:
    • salesforce-multipicklist.test.ts , comma→semicolon conversion, compound-label-not-corrupted, ;-input regression, isMultiPicklist:false no-op.
    • salesforce-partial-field-validation.test.ts (new) , mixed valid/invalid batch still writes the valid fields; all-invalid batch makes no Salesforce call; dropped fields logged and returned in failedFields.
    • update-external-crm-workflow.test.ts (extended) , updatedFields excludes a driver-dropped field instead of reporting the full attempted set.
    • crm-upsert-working-memory-partial-failure.test.ts / crm-update-conversation-partial-failure.test.ts (new) , both tRPC mutations throw naming the dropped field(s) on a partial CRM-sync failure, via a shared throwIfPartialCrmFailure() helper.
    • salesforce-object-binding.test.ts / salesforce-reference-resolution.test.ts , re-run for regression coverage on the shared updateOpportunityFields/validateAllowedValue path.
  • Went through 3 rounds of fresh, impartial /thermo-review (each a context-free subagent, not the implementer) , 0 blocking issues survived to the final round. Fixed along the way: the failedFields plumbing gap on updateConversation, the compound-option-label corruption edge case, three line-width nits, and a stale doc comment.
  • pnpm deps:check , 0 violations across 2011 modules.
  • Full autofix:local was run once; its only failure (apps/mail/tests/modules/chat-store/chatHistoryList.test.ts) is a pre-existing, unrelated real-clock "Today"/"Yesterday" bucketing flake , confirmed via git diff --stat origin/staging -- apps/mail/ showing zero mail-app changes in this branch.

Verify in prod

  • Axiom dataset: cedar-prod , a comma-joined multi-select value that fails the "every token is a known option" check:
    ['cedar-prod'] | where _time > ago(1h) and message contains "Comma-joined multipicklist value did not split into known options"
    | project _time, fieldName, value, parts
    
  • Axiom dataset: cedar-prod , a field dropped from a Salesforce push by local validation:
    ['cedar-prod'] | where _time > ago(1h) and message == "[updateOpportunityFields] Dropped field , failed local validation"
    | project _time, user_id, opportunity_id, field, value, allowed_values
    
  • Axiom dataset: cedar-prod , the workflow-level view of the sam
Show production surfaces and changed-file mapping

Production surfaces

SurfaceRequestsErrorsp95UsersVerdict
salesforce.updateOpportunityFields0 → 00 → 0 (0%)not measured0No traffic
No requests recorded since this deploy.
salesforce.updateOpportunityFields0 → 00 → 0 (0%)not measured0No traffic
No requests recorded since this deploy.

Changed files → surfaces

  • apps/server/src/mastra/workflows/sync/__tests__/update-external-crm-workflow.test.tsno production surface mapped
  • apps/server/src/mastra/workflows/sync/update-external-crm-workflow.tsno production surface mapped
  • apps/server/src/services/integrations/crm/__tests__/salesforce-multipicklist.test.tsno production surface mapped
  • apps/server/src/services/integrations/crm/__tests__/salesforce-partial-field-validation.test.tsno production surface mapped
  • apps/server/src/services/integrations/crm/driver.tsno production surface mapped
  • apps/server/src/services/integrations/crm/external-crm-field-mapping.tsno production surface mapped
  • apps/server/src/services/integrations/crm/salesforce.tsno production surface mapped
  • apps/server/src/trpc/routes/__tests__/crm-update-conversation-partial-failure.test.tsno production surface mapped
  • apps/server/src/trpc/routes/__tests__/crm-upsert-working-memory-partial-failure.test.tsno production surface mapped
  • apps/server/src/trpc/routes/crm.tsno production surface mapped