Libra CodeHub

CedarCopilot/cedar-mail

Branch: staging

fix(crm): address Greptile P1/P2 findings from PR #2838

merged#2839CedarCopilot

CedarCopilot wants to merge 1 commit into staging from fix/staging-main-greptile-findings

Live on prod, watching, 2 days leftTimeline and evidence
  1. Opened
    Sep 20, 2026, 11:08 AM
  2. Sep 20, 2026, 11:14 AM
  3. Merged
    Sep 20, 2026, 11:33 AM
  4. Live on prod
    Sep 20, 2026, 11:33 AM
  5. Observed 1 hour
    Sep 20, 2026, 11:33 AM
  6. Watching

    Live on prod, watching, 2 days left

    Sep 20, 2026, 11:33 AM
  7. Pipelines steady after this deploy
    Sep 20, 2026, 11:33 AM

Behaviors Libra is checking

The aop_conversion metric counts a Lead reassignment as a conversion only when its destination is the confirmed Deals/Opportunity AOP, not Spam, Vendor, another unbound AOP, or an unresolved AOP.Not checked
prod
The CRM backfill no longer reports success when a capped fetch scanned only already-linked records, instead instructing the operator how to widen the fetch window and exiting non-zero.Not checked
prod
The CRM object backfill accepts --fetch-max-pages and forwards it to HubSpot discovery so operators can fetch beyond the default three pages when older records may be hidden behind the batch ceiling.Not checked
prod
The operator-run CRM backfill can enqueue more than the normal 500-record on-demand limit when --per-pass is set above 500, while ordinary callers remain capped.Not checked
prod
discoverNewDealsFromProvider marks a non-empty pass where every scanned record was already linked and none failed as possibly capped instead of treating it as proven exhaustion.Not checked
prod
The external CRM sync job treats non-object connection metadata as absent settings rather than passing it through as a record-shaped configuration.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.

Summary

Fixes the 5 Greptile findings surfaced on #2838 (staging→main), per Isabelle's request to fix them in their own PR while #2838 itself merges.

🔴 P1 , Account unlink removes records

CrmAgentBadge.tsx/crm.ts: unlinking the account on a conversation that also had a real recordId link (a Lead/Partnership, which , like an account-only placeholder , has no dealId) silently deleted that record link too. Fixed on both sides:

  • Frontend: the account row's unlink button now hides whenever hasRecord is true, mirroring the existing hasDeal rule.
  • Server: the filter logic (extracted into filterIntegrationMetadataForUnlink, now directly unit-tested) requires neither dealId nor recordId before removing an entry as a "placeholder."

🔴 P1 , Backfill cannot pass caps

backfill-crm-object-records.ts: the repeat-until-exhausted loop's fetch is a fixed-size, sorted-newest-first, since-bounded query with no upper-bound cursor. Once the true total exceeds one fetch's cap, older records are permanently unreachable , every repeated pass returns the identical top-N and the script declared success.

  • Added possiblyCapped to DiscoverNewDealsResult: true when records were found but all were already linked (not proof of exhaustion).
  • Added two real, safe levers: maxPages (HubSpot's getAllDeals already supports this , just never threaded through) and unclampedMaxDeals (the 500-per-pass ceiling exists to protect the on-demand chat tool from an unbounded burst, not a deliberate backfill run).
  • The script now exits non-zero and tells the operator exactly what to do (raise --fetch-max-pages for HubSpot, or re-run with an earlier --since for Salesforce/Attio, which have no page-count lever) instead of silently claiming victory.

🔴 P1 , Conversions include non-Deals

deal-metrics.ts's aop_conversion classifier collapsed "AOP resolved with no object binding" (Spam, Vendor, any unbound AOP) and "deleted/unresolvable AOP id" into the same value as the true Deals AOP , so a Lead-bound conversation moved to Spam counted as a 100% conversion. classifyAopForConversion now only returns 'deal' for an explicit Opportunity binding or the implicit default (no binding, named literally "Deals"); everything else is 'unknown', never a conversion target.

🟡 P2 , Validate metadata at boundary

process-external-crm-sync.ts cast connectionMetadata: unknown directly. Added a real isRecord type guard.

🟡 P2 , Use shared form controls

crm-integration-card.tsx's new AOP picker manually composed Label + Select instead of the shared form-kit. Rebuilt with SelectField/FieldRows.

Test plan

  • pnpm --filter @zero/server run types clean
  • pnpm --filter @zero/mail run types clean (touched files)
  • pnpm dlx oxlint --deny-warnings clean
  • pnpm deps:check clean
  • 3207 tests passing across crm/cron/trpc/aop/integrations, including 17 new tests directly pinning findings 1-3 (filter logic, possiblyCapped/maxPages/unclampedMaxDeals, and the Spam/Vendor/deleted-AOP conversion regression)

🤖 Generated with Claude Code

RetriggerConfidence Score: 3/5

The PR is not safe to merge until the backfill can distinguish successful exhaustion from a capped response and provides recovery controls that actually advance every supported provider path; the explicit fixture-typing rule must also be satisfied.

Findings

  1. <img alt="P1" src="h
Show production surfaces and changed-file mapping

Production surfaces

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

Changed files → surfaces

  • apps/mail/modules/conversations/components/CrmAgentBadge.tsxno production surface mapped
  • apps/mail/modules/integrations/crm-integration-card.tsxno production surface mapped
  • apps/server/scripts/backfill-crm-object-records.tsno production surface mapped
  • apps/server/src/cron/process-external-crm-sync.tsno production surface mapped
  • apps/server/src/services/aop/__tests__/no-hardcoded-deals-aop-comparison.test.tsno production surface mapped
  • apps/server/src/services/crm/__tests__/crm-discover-new-deals.test.tsno production surface mapped
  • apps/server/src/services/crm/__tests__/deal-metrics.test.tsno production surface mapped
  • apps/server/src/services/crm/crm-discover-new-deals.tsno production surface mapped
  • apps/server/src/services/crm/deal-metrics.tsno production surface mapped
  • apps/server/src/trpc/routes/__tests__/crm-unlink-external-crm-filter.test.tsno production surface mapped
  • apps/server/src/trpc/routes/crm.tsno production surface mapped