Libra CodeHub

CedarCopilot/cedar-mail

Branch: staging

feat(aop): reclassify no-op AOP conversations on real meeting evidence

merged#2774CedarCopilot

CedarCopilot wants to merge 1 commit into staging from fix/no-op-aop-meeting-reclassification

Live on prod, 3 of 9 behaviors degradedTimeline and evidence
  1. Opened
    Sep 13, 2026, 2:32 PM
  2. Sep 13, 2026, 7:23 PM
  3. Merged
    Sep 13, 2026, 8:27 PM
  4. Live on prod
    Sep 13, 2026, 8:27 PM
  5. Observed 2 days
    Sep 13, 2026, 8:27 PM
  6. Pipelines steady after this deploy
    Sep 13, 2026, 8:27 PM
  7. Mixed signals

    Live on prod, 3 of 9 behaviors degraded

    Sep 16, 2026, 7:57 PM

Behaviors Libra is checking

A MEDIUM-confidence no-op reclassification uses the existing ambiguity-ping decision, feature-flag, claim, send, deduplication, and release-on-failure pipeline, keyed to the surviving conversation.Degradedlow confidence

Strict CloudWatch fallback found 18 prod failure log lines for Failed to create execution for task (Task already claimed or no longer eligible) in the last 24h while checking decideaopambiguityping, claimaopambiguityping, sendaopambiguityping, medium, medium-.

prod, checked Sep 16, 2026, 7:03 PM
When meeting evidence successfully moves a conversation out of a no-op AOP, execution state is updated to the new AOP and normal agent processing continues instead of returning through the no-op short circuit.Degradedmedium confidence

Evidence snapshot saw 50 error synthetic spans out of 139 prod CloudWatch-derived synthetic spans matching runPreExecutionSetup since deploy (36%). Duration and lifecycle are inferred from CloudWatch log timestamps.

prod, checked Sep 16, 2026, 7:57 PM
A meeting that causes the first AOP selection for a new conversation does not invoke the no-op reclassification classifier a second time in the same execution.Degradedmedium confidence

Evidence snapshot saw 49 error synthetic spans out of 137 prod CloudWatch-derived synthetic spans matching runPreExecutionSetup since deploy (35.8%). Duration and lifecycle are inferred from CloudWatch log timestamps.

prod, checked Sep 16, 2026, 7:03 PM
Every no-op meeting reclassification decision records structured gate outcomes, classifier reasoning, confidence bands, applied AOPs, merge IDs, and ambiguity-ping results under one queryable log tag.Degradedmedium confidence

Strict CloudWatch fallback found 448 prod failure log lines for sync.provider_read_failed_served_snapshot (Requested entity was not found.) in the last 24h while checking reclassified, [no-op-meeting-reclassification], skipped, classifier result, no action — low confidence, up.

prod, checked Sep 16, 2026, 7:57 PM
A no-op conversation whose most recent AOP change was made by a human is not automatically reclassified by later meeting evidence.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 15, 2026, 7:32 PM
Any conversation currently assigned to a no-op AOP, including Vendor notification and other no-op categories, is reconsidered only when the triggering event is a meeting with at least one attendee outside the user's internal domains.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 15, 2026, 8:21 PM

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

Summary

Adds one narrow admission path back into selectAopForConversation for a conversation stuck on a no-op AOP (Spam, Vendor notification, Recruiting, Inbound cold pitch, "Non-deal professional relationships"), triggered only when a meeting event has a real external attendee and the current AOP wasn't set by a human.

⚠️ Depends on #2772 (fix/ambiguous-aop-meeting-prep-floor) , this branch is built on top of it to reuse internal-domains.ts. Do not merge this before #2772 merges; rebase onto staging after that lands.

Why

docs/design/ambiguous-aop-classification-escalation.md built confidence-band classification but deliberately never re-runs it once any AOP (including a no-op one) is set , re-running on every event measurably causes flip-flopping (306 conversations landed on 2+ distinct AOPs in 90 days per that doc's own data).

Real case: Vooma/Jason Conlow's "Run Logistics" conversation was classified into "Non-deal professional relationships" from one thin early email (0.85 confidence, correctly HIGH-banded at the time), then got zero deal treatment , no CRM updates, no tasks, no prep , for a real discovery call 17 days later with clear buying intent ($50k/year, "sign you up tomorrow").

Scoped to all no-op AOPs, not just one , unlike #2772's prep-floor guarantee (a blanket "always run" fix, narrowly scoped because Vendor notification had real measured noise). This mechanism only acts on real evidence in meeting content, so it's safe to scope broadly: an eval harness (.claude/skills/playbook-instruction-eval/) validated zero false positives distinguishing a vendor call revealing a real deal from a routine vendor renewal call, and the escalation doc's own historical data already shows non-zero Vendor notification → Deals (2) and Inbound cold pitch → Deals (4) corrections in 90 days.

The three gates (all required)

  1. Triggering event is a meeting (not email/Slack).
  2. Meeting has a real (external) attendee , computed off the meeting's own reported participants list, so ad-hoc meetings with no calendar invite still qualify. Open issue, not yet fixed , see below.
  3. Current AOP was not set by a human , checked against the most recent crm_conversation_updates row with action_type = 'aop_change'. This is the guard against silently re-overriding someone who already reviewed and corrected the classification once.

A fourth gate lives in pre-execution-setup.ts: skip entirely when needsAopSelection is true (this execution just ran the classifier for the first time on a brand-new conversation), so a meeting that's the very first event on a conversation doesn't invoke the classifier twice in one run.

What's reused, unchanged

No new prompt, no new confidence-band policy, no new Slack surface , this calls the exact same selectAopForConversation, setupConversationForAop, and decideAopAmbiguityPing/claimAopAmbiguityPing/sendAopAmbiguityPing/releaseAopAmbiguityPing pipeline already shipped for the new-conversation classification path. Includes the same org-scope-merge handling that pipeline requires: an org-scoped target AOP (e.g. Deals) can merge this conversation into another org member's, deleting the input , effectiveConversationId is threaded through and both this module's ping calls and pre-execution-setup.ts's downstream state switch to the surviving id, mirroring the identical handling already in place for the original classification path a few lines above.

Observability

Every decision point logs through a single consistent tag, [no-op-meeting-reclassification], including the full verbatim classifier output (confidence, band, all considered alternatives, reasoning) , see the design doc's Observability section for the exact log shape and Axiom query pattern.

Test plan

  • Unit: each of the three gates independently prevents the classifier from being invoked at all.
  • Unit: HIGH band reclassifies silently; MEDIUM band reclassifies AND go
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/mastra/workflows/event-execution/pre-execution-setup.tsno production surface mapped
  • apps/server/src/services/aop/__tests__/no-op-meeting-reclassification.test.tsno production surface mapped
  • apps/server/src/services/aop/no-op-meeting-reclassification.tsno production surface mapped
  • apps/server/src/services/crm/integrity-heal.tsno production surface mapped
  • docs/design/no-op-aop-meeting-reclassification.mdno production surface mapped