Libra CodeHub

CedarCopilot/cedar-mail

Branch: staging

fix(crm): non-active sibling deals can no longer overwrite conversation fields

merged#2791CedarCopilot

CedarCopilot wants to merge 1 commit into staging from fix/crm-field-sync-multi-deal-guard

Live on prod, no production signal yetTimeline and evidence
  1. Opened
    Sep 15, 2026, 7:49 AM
  2. Merged
    Sep 15, 2026, 8:02 AM
  3. Live on prod
    Sep 15, 2026, 8:02 AM
  4. Observed 2 days
    Sep 15, 2026, 8:02 AM
  5. Pipelines steady after this deploy
    Sep 15, 2026, 8:02 AM
  6. Sep 15, 2026, 8:12 AM
  7. Unobserved

    Live on prod, no production signal yet

    Sep 17, 2026, 6:21 PM

Behaviors Libra is checking

Fields from the conversation's active deal must continue to update conversation fields and working memory, including custom fields.Inconclusivelow confidence

Verification reached its 130s outer budget before the verifier returned. Libra is keeping this intent verifying until the next check.

prod, checked Sep 17, 2026, 5:01 PM
Custom and working-memory-only fields such as `sql`, `sql_date`, and `dq_reason` must also be protected from non-active sibling deal overwrites by loading conversation integration metadata even when no built-in conversation column is mapped.Inconclusivelow confidence

Verification reached its 120s wall-clock budget before a tied verdict. Libra is keeping this intent verifying until the next check.

prod, checked Sep 17, 2026, 5:03 PM
When a non-active sibling's fields are dropped, their CRM change keys must still be reported as handled so downstream agent reconciliation does not reapply those values.Inconclusivelow confidence

Verification reached its 130s outer budget before the verifier returned. Libra is keeping this intent verifying until the next check.

prod, checked Sep 17, 2026, 6:01 PM
A CRM sync event from a non-active sibling deal must not persist any mapped conversation field, rather than only skipping status.Inconclusivelow confidence

Strict CloudWatch fallback saw 518,846 success-shaped log lines matching applycrmfieldsync, crm-field-sync-full.test, crm-field-sync, field, sync, but no tied operation was present, so Libra is not calling this working.

prod, checked Sep 17, 2026, 6:03 PM
Conversations with only one linked CRM deal must continue syncing mapped fields normally even when that deal is closed.Inconclusivelow confidence

Verification reached its budget before a verdict.

prod, checked Sep 17, 2026, 6:21 PM
Full programmatic CRM mappings must discard every field from a non-active sibling deal before updating conversation fields or working memory.Not checked
prod, checked Sep 17, 2026, 6:18 PM

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

Summary

From Pirros's 2026-09-10 check-in, three bugs were reported: HubSpot closed-won field overwrite, "accept SQL" not filling stage/date, and duplicate email drafts. This PR is the code fix for the second , the other two were config-only and already applied live (no PR needed), included below for full traceability.

Bug 1 , HubSpot closed-won fields overwritten (config fix, already live, no code change)

Pirros's crm-updater instructions (org template + all 3 reps) explicitly told the agent to populate TCV/ARR/contract dates/etc. on Closed Won , fields Pirros's own separate line-item workflow owns. Also found a second, non-obvious blocker while implementing: those same fields were marked structurally required for the Closed Won stage in stageRequiredFields (HubSpot's raw stage id for Pirros's "Closed Won" label is decoupled from the display label , a common HubSpot clone-artifact gotcha). Removed the instruction block from all 4 docs and trimmed the required-fields list for Closed Won on all 3 Pirros HubSpot connections. Verified via verify-playbook-resolution.ts and verify-document-yjs-sync.ts, all green.

Bug 2 , "SQL accept" not filling stage/date (this PR , code fix)

Initial theory (no date-type coercion in hubspot.ts) didn't hold up: another HubSpot-connected customer's date pushes on the identical code path persisted fine. Direct investigation via HubSpot's own property-history API showed both real deals stable since mid-2025 , there is no HubSpot workflow involved at all.

Real root cause: crm-field-sync.ts's active-deal guard (dropStatusWriteFromNonActiveDeal) only protected the status field from being overwritten by a sync event from a linked-but-not-active deal, on the stated theory "only stage is contended this way." That theory was wrong. A Pirros conversation linked to two HubSpot deals , one frozen at sql=false/sql_date=2025-07-02 (human-set 2025-07-02, untouched since), one frozen at sql=true/sql_date=2025-09-25 (human-set 2025-09-26, untouched since) , alternated between those two deals' full value sets on every sync for six weeks, because sql/sql_date/dq_reason had zero protection. The same pattern shows up on a dozen+ other Pirros conversations (216, 195, 194, 184 flips since 2026-08-03).

Fix: generalized the guard (renamed dropNonActiveDealWrites) to drop ALL fields from a non-active-deal sync, not just status, in both call sites (applyCrmFieldSync , the diff-based path, and applyFullProgrammaticCrmMappings , the on-demand full-refresh path). The latter had a second, compounding bug: its integrationMetadata fetch was gated on a built-in CONVERSATION_METADATA_FIELDS column being present in the sync , a sync carrying only custom/working-memory fields (exactly sql/sql_date/dq_reason's shape) never even fetched the metadata needed to run the guard. Now fetched unconditionally whenever there are fields to check.

Bug 3 , Duplicate email drafts (config fix, already live, no code change)

Keenan and Xander each had two subagents independently allowed to draft a reply to the same inbound email (drafter and inbound-email-notifier, the latter's own instructions saying its job was "two-fold: draft a reply AND send a Slack DM"), racing on every inbound email. This was already diagnosed and fixed once , for Peter only (2026-08-24) , and never applied to Keenan's/Xander's copies, which is why it kept recurring for them specifically. Rewrote both to notify-only, mirroring Peter's fixed copy exactly. Verified via cedar-cli subagent verify and verify-document-yjs-sync.ts, all green.

Blast radius

crm-field-sync.ts is shared code , any customer with multiple deals linked to one conversation is exposed to this, not just Pirros (Pirros's DQ'd-deal + live-re-engagement pattern just makes them hit it often). Bugs 1 and 3 are Pirros-only config.

Test plan

  • pnpm --filter @zero/server run types , clean
  • `pnpm --filter @zero/
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/services/crm/__tests__/crm-field-sync-full.test.tsno production surface mapped
  • apps/server/src/services/crm/crm-field-sync.tsno production surface mapped