Libra CodeHub

CedarCopilot/cedar-mail

Branch: staging

feat(crm): json custom field type (deep-merged partial writes)

merged#2550CedarCopilot

CedarCopilot wants to merge 3 commits into staging from feat/custom-field-types-json-timestamp

Live on prod, no production signal yetTimeline and evidence
  1. Opened
    Aug 23, 2026, 7:48 AM
  2. Aug 23, 2026, 2:46 PM
  3. Merged
    Aug 23, 2026, 9:40 PM
  4. Live on staging
    Aug 23, 2026, 9:40 PM
  5. Live on prod
    Aug 23, 2026, 9:40 PM
  6. Observed 2 days
    Aug 23, 2026, 9:40 PM
  7. Pipelines steady after this deploy
    Aug 23, 2026, 9:40 PM
  8. Pipelines steady after this deploy
    Aug 23, 2026, 9:40 PM
  9. Unobserved

    Live on prod, no production signal yet

    Aug 26, 2026, 9:06 PM

Behaviors Libra is checking

CRM integration behavior from components/add-column-popover.tsx, components/crm-cell.tsx, components/editable-column-header.tsx keeps working in prod.Degradedlow confidence

Strict CloudWatch fallback found 91 prod failure log lines for [UpdateExternalCrmWorkflow] Driver update failed (Step updateOpportunityField failed: HTTP 400: [{\), but this domain-wide failure family is not tied to this intent.

prod, checked Aug 26, 2026, 9:06 PM
AOP context behavior from db/aop-schema.ts keeps working in prod.Inconclusivelow confidence

Strict CloudWatch fallback saw 677 success-shaped log lines matching pipeline, analytics, quota, aop, but no tied operation was present, so Libra is not calling this working.

prod, checked Aug 25, 2026, 10:55 AM
Chat workflow behavior from lib/conversation-filter-schema.ts, conversation/updateConversationFieldsTool.ts, context-formatting/conversation-context.ts keeps working in prod.Inconclusivelow confidence

Strict CloudWatch fallback found 16 recent prod failure log lines for mirror.upsertEmailThread failed (Failed query: insert into \), but the same failure had 8 log lines in the comparable pre-deploy baseline. Libra is not blaming this PR intent without a post-deploy increase or.

prod, checked Aug 25, 2026, 10:55 AM
AOP context behavior from db/aop-schema.ts keeps working in staging.Inconclusivelow confidence

Strict CloudWatch fallback saw 381 success-shaped log lines matching pipeline, analytics, quota, aop, but no tied operation was present, so Libra is not calling this working.

staging, checked Aug 26, 2026, 6:13 PM
Chat workflow behavior from lib/conversation-filter-schema.ts, conversation/updateConversationFieldsTool.ts, context-formatting/conversation-context.ts keeps working in staging.Inconclusivelow confidence

Strict CloudWatch fallback found 63 prod failure log lines for mirror.upsertEmailThread failed (Failed query: insert into \), but this domain-wide failure family is not tied to this intent.

staging, checked Aug 26, 2026, 6:13 PM
CRM integration behavior from components/add-column-popover.tsx, components/crm-cell.tsx, components/editable-column-header.tsx keeps working in staging.Inconclusivelow confidence

Strict CloudWatch fallback saw 476 success-shaped log lines matching crm, hubspot, salesforce, deal, deals, but no tied operation was present, so Libra is not calling this working.

staging, checked Aug 26, 2026, 6:21 PM

Libra has verdicts on 0 of 3 tracked behaviors on prod; 3 are still being checked. Libra has 1 low-confidence degraded verdict it is still confirming. Libra checks hourly for 3 days after each deploy.

What

Adds a new json custom field type , a Cedar conversation/CRM custom field whose value is a JSON object, written by agents as partial patches that deep-merge onto the stored value. Motivating use case: Pirros's HubSpot deal brief is a single structured-JSON property (ae_data_brief , reasonToBuy, mainObjection, coiValue, incentive, per-contact reads, engagements). The agent fills the case keys while a rep owns the people keys; a partial write must never clobber the rep's sub-keys.

Design doc: docs/design/custom-field-types-json-timestamp.md.

A timestamp type was scoped but dropped , the existing date type already stores full time (toISOString(), datetime display), so it's redundant.

How it works (the key invariant)

A json field's incoming value is a partial patch. It is deep-merged onto the stored object once, up front, before anything downstream sees it (updateConversationFieldsTool §4.5), so both the external-CRM push and the Cedar persist receive the whole merged object , never the partial. A rejected merge (corrupt stored JSON, non-object root, over-size, invalid patch) is dropped: not pushed, not persisted, not returned , and surfaced to the agent as a failed field.

  • services/crm/json-field-merge.ts , pure deepMergeJson (immutable; object-over-object recurses so sibling keys like reads are preserved; undefined ignored, null clears, arrays replace, scalars win) + resolveJsonFieldPatch (read+merge+validate) + applyJsonFieldPatch (resolve+persist) + resolveJsonFieldPatchesInList (batch resolve for the write path). Typed rejection reasons; no throw escapes; io-error distinguishes infra failure from corrupt-existing.
  • Values live in the existing content text column , no DB migration.
  • CRM push: passthrough (HubSpot ae_data_brief is a text property holding stringified JSON).

Invariant to preserve: partial-patch json writes must go through the tool's §4.5 resolver. Any other caller writing a json field must send the whole merged object (or call resolveJsonFieldPatch). Today the only partial-patch writer is the agent tool; the frontend editor writes whole objects.

Tests

services/crm/__tests__/json-field-merge.test.ts , 29 tests: the deep-merge matrix (incl. sibling-preservation / the deal-brief never-clobber case), the Cedar-side guards (corrupt/non-object/too-large reject without overwriting), io-error vs corrupt-existing for read+persist failures, and the batch resolver (a json field resolves to the merged object , the exact value pushed to the CRM , and a rejected field is dropped from the resolved set).

Review

Went through thermo-review. The first pass caught a blocking data-loss bug , the merge originally protected only the Cedar copy while the CRM push sent the pre-merge partial (clobbering reads in HubSpot on every agent write). Fixed by moving the merge ahead of the push (§4.5 above). Gates: @zero/server types ✓, @zero/mail types ✓, deps:check ✓ (no violations), json tests 29/29 ✓.

🤖 Generated with Claude Code

Greptile Summary

The PR adds JSON custom fields whose agent-written partial objects are resolved into complete deep-merged values before proposal, streaming, external CRM synchronization, and Cedar persistence.

  • Adds JSON field definitions, validation, formatting, UI editing, and persistence support.
  • Centralizes patch resolution and rejection handling in the CRM field-write path.
  • Marks whole-object UI and inbound-CRM writes so they bypass partial-patch merging.
  • Adds focused coverage for merge semantics, persistence, synchronization, and tool behavior.

Confidence Score: 5/5

The PR appears safe to merge.

No blocking failure remains.

Important Files Changed

FilenameOverview
apps/server/src/services/crm/json-field-merge.t
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/crm/components/add-column-popover.tsxno production surface mapped
  • apps/mail/modules/crm/components/crm-cell.tsxno production surface mapped
  • apps/mail/modules/crm/components/editable-column-header.tsxno production surface mapped
  • apps/mail/modules/crm/types/index.tsno production surface mapped
  • apps/mail/modules/crm/utils/compute-canvas-filters.tsno production surface mapped
  • apps/server/src/db/aop-schema.tsno production surface mapped
  • apps/server/src/lib/conversation-filter-schema.tsno production surface mapped
  • apps/server/src/mastra/tools/conversation/__tests__/updateConversationFieldsTool.test.tsno production surface mapped
  • apps/server/src/mastra/tools/conversation/updateConversationFieldsTool.tsno production surface mapped
  • apps/server/src/mastra/utils/context-formatting/conversation-context.tsno production surface mapped
  • apps/server/src/services/crm/__tests__/conversations.json-persist.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__/field-type-utils.json.test.tsno production surface mapped
  • apps/server/src/services/crm/__tests__/json-field-merge.test.tsno production surface mapped
  • apps/server/src/services/crm/conversation-field-values.tsno production surface mapped
  • apps/server/src/services/crm/conversations.tsno production surface mapped
  • apps/server/src/services/crm/crm-field-sync.tsno production surface mapped
  • apps/server/src/services/crm/field-type-utils.tsno production surface mapped
  • apps/server/src/services/crm/json-field-merge.tsno production surface mapped
  • apps/server/src/services/integrations/crm/external-crm-field-mapping.tsno production surface mapped
  • apps/server/src/trpc/routes/crm.tsno production surface mapped
  • apps/server/src/trpc/routes/user-tasks.tsno production surface mapped
  • docs/design/custom-field-types-json-timestamp.mdno production surface mapped