Libra CodeHub

CedarCopilot/cedar-mail

Branch: staging

fix(crm): stop silently dropping unsupported eventFilter conditions in buildConversationFilterConditions

merged#2888CedarCopilot

CedarCopilot wants to merge 4 commits into staging from worktree-agent-aad1abeaa9c583f99

Live on prod, 0 of 14 surfaces working, 2 days leftTimeline and evidence
  1. Opened
    Sep 22, 2026, 5:39 PM
  2. Sep 22, 2026, 8:08 PM
  3. Merged
    Sep 22, 2026, 8:14 PM
  4. Live on prod
    Sep 22, 2026, 8:14 PM
  5. Observed 0 hours, 14 surfaces, 12 requests
    Sep 22, 2026, 8:14 PM
  6. Watching

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

    Sep 22, 2026, 8:14 PM
  7. Pipelines steady after this deploy
    Sep 22, 2026, 8:14 PM

Behaviors Libra is checking

An unsupported eventFilter.count.eventType causes the count filter operation to fail explicitly rather than silently dropping the entire count constraint.Not checked
prod
An unsupported eventFilter.types.exclude value causes conversation filtering to fail explicitly rather than being silently removed and weakening the exclusion condition.Not checked
prod
An unsupported eventFilter.types.include value causes conversation filtering to fail explicitly rather than being silently removed while valid sibling values continue to apply.Not checked
prod
Conversation filter configurations reject unsupported event types in eventFilter.types.include, eventFilter.types.exclude, and latestEventFilter.types during validation instead of allowing invalid rules to be saved.Not checked
prod
An unsupported latestEventFilter.types value causes latest-event conversation filtering to fail explicitly rather than being removed and turning the filter into a weaker query.Not checked
prod

Libra found 14 production surfaces on prod but could not judge any of them yet. 6 surfaces had under 20 requests, so Libra has not judged them; 4 surfaces had no requests at all. Libra checks hourly for 3 days after each deploy.

Summary

Third instance of the silent-no-op bug family fixed by #2882 (filters array) and #2886 (eventFilter), both in conversation-lookup.ts. This one is in a different file/call path: buildConversationFilterConditions (apps/server/src/services/crm/conversations.ts) , the column_filters mode / frontend CRM view + inbox-rule call path, not flexible_query.

Symptom, same shape as the other two: eventFilter.types.include/.exclude, latestEventFilter.types, and eventFilter.count.eventType all silently dropped an explicit-but-invalid event type instead of throwing , .filter((c): c is SQL => c !== null) quietly excluded a bad entry from an array while valid siblings still applied, and if (typeCondition) { ...push... } silently dropped the whole count filter. The tool/query still reported success against an unfiltered-or-partially-filtered result.

The fix: every one of those four sites now throws for an explicit-but-invalid value (never silently drops), while the genuinely-empty-criteria case (no types at all, no date) remains a legitimate no-op , verified by dedicated tests for both.

A second, bigger finding surfaced during review, and fixed here too (not deferred): the two blocks each declared their own byte-for-byte-identical local VALID_EVENT_TYPES Set. More importantly, tracing every caller of buildConversationFilterConditions found one with a materially worse blast radius than a one-off agent query failing , a saved inbox CRM rule (InboxConfig.conversationFilter) flows through this same builder on every inbox load (list-threads-from-db.ts, count-threads-from-db.ts), with no try/catch anywhere in between. Before this diff, a bad saved rule silently mis-filtered; after a naive version of this fix, it would throw on every subsequent load of that inbox , a materially worse outcome than a chat-agent tool call getting a clear retry-able error.

Live-checked production (user_settings.settings ILIKE '%eventFilter%'/%latestEventFilter%') , zero rows currently use either field, so this isn't an active incident. But nothing in the schema prevented one from being saved. Fixed at the write boundary instead of leaving it as a read-time landmine:

  • New, deliberately dependency-free module apps/server/src/services/crm/event-types.ts , the canonical VALID_EVENT_TYPES + isValidEventType/assertValidEventType. Zero imports, so importing it doesn't reintroduce the heavy conversations.ts import-graph cost conversation-filter-subquery.ts's lazy import() exists specifically to avoid (verified: conversation-filter-schema.ts's existing conversations.ts import stays import type-only, unchanged).
  • conversations.ts imports it for the SQL condition builders (throws , defense in depth at read time).
  • conversation-filter-schema.ts imports it for a zod .refine() on eventFilter.types.include/.exclude/latestEventFilter.types, so a bad value is now rejected with a clean validation error at save time, before it can ever reach the read-time throw.

Deliberately NOT reconciled: conversation-lookup.ts's own separate VALID_EVENT_TYPES (task/linkedin there vs. slack/custom here) , different call paths, independently-evolved lists. Figuring out which is actually correct requires checking real crm_events.event_type values in production, a data question not a code fix , out of scope here, same as it was for #2886.

Test plan

  • pnpm --filter @zero/server run types , clean
  • pnpm deps:check , 0 violations (2010 modules)
  • New regression tests, conversations.event-filter-conditions.test.ts (15 tests): invalid type in types.include/.exclude/count/latestEventFilter.types all throw; valid values across the real VALID_EVENT_TYPES set (iterated, not hardcoded) all succeed; the genuinely-empty-criteria case stays a no-op; unparseable dates already threw pre-existing (Date#toISOString on an Invalid Date) , verified,
Show production surfaces and changed-file mapping

Production surfaces

SurfaceRequestsErrorsp95UsersVerdict
/api/trpc/crm.searchConversationsMinimal0 → 30 → 0 (0%)not measured → 135 ms0Insufficient traffic
3 requests, under the 20 Libra needs
/api/trpc/crm.listConversations0 → 30 → 0 (0%)not measured → 1016 ms0Insufficient traffic
3 requests, under the 20 Libra needs
POST /api/trpc/crm.searchConversationsMinimal0 → 20 → 0 (0%)not measured → 135 ms0Insufficient traffic
2 requests, under the 20 Libra needs
POST /api/trpc/crm.listConversations0 → 20 → 0 (0%)not measured → 1016 ms0Insufficient traffic
2 requests, under the 20 Libra needs
trpc.crm.searchConversationsMinimal0 → 10 → 0 (0%)not measured → 123 ms1Insufficient traffic
1 request, under the 20 Libra needs
crm.searchConversationsMinimal0 → 10 → 0 (0%)not measured → 122 ms1Insufficient traffic
1 request, under the 20 Libra needs
trpc.crm.loadCalendarConversationsBatch0 → 00 → 0 (0%)not measured0No traffic
No requests recorded since this deploy.
/api/trpc/crm.loadCalendarConversationsBatch0 → 00 → 0 (0%)not measured0No traffic
No requests recorded since this deploy.
POST /api/trpc/crm.loadCalendarConversationsBatch0 → 00 → 0 (0%)not measured0No traffic
No requests recorded since this deploy.
/api/trpc/crm.getConversations0 → 00 → 0 (0%)not measured0No traffic
No requests recorded since this deploy.
buildConversationFilterConditions.eventFilter.countno baseline → 0no baseline → 0 (0%)not measurednot countedUnavailable
ClickHouse reads are unavailable, so Libra could not measure this surface.
buildConversationFilterConditions.eventFilter.types.excludeno baseline → 0no baseline → 0 (0%)not measurednot countedUnavailable
ClickHouse reads are unavailable, so Libra could not measure this surface.
buildConversationFilterConditions.eventFilter.types.includeno baseline → 0no baseline → 0 (0%)not measurednot countedUnavailable
ClickHouse reads are unavailable, so Libra could not measure this surface.
buildConversationFilterConditions.latestEventFilter.typesno baseline → 0no baseline → 0 (0%)not measurednot countedUnavailable
ClickHouse reads are unavailable, so Libra could not measure this surface.

Changed files → surfaces

  • apps/server/src/services/crm/conversations.ts/api/trpc/crm.searchConversationsMinimal/api/trpc/crm.listConversationsPOST /api/trpc/crm.searchConversationsMinimalPOST /api/trpc/crm.listConversationstrpc.crm.searchConversationsMinimalcrm.searchConversationsMinimal+4
  • apps/server/src/lib/__tests__/conversation-filter-schema.test.tsno production surface mapped
  • apps/server/src/lib/conversation-filter-schema.tsno production surface mapped
  • apps/server/src/services/crm/__tests__/conversations.event-filter-conditions.test.tsno production surface mapped
  • apps/server/src/services/crm/event-types.tsno production surface mapped