CedarCopilot wants to merge 1 commit into staging from fix/caretta-webhook-attendee-routing
Live on prod, no production signal yet
OTEL fallback found no prod spans matching carettaDriver.listMeetings / caretta_list_my_calls since deploy.
Since deployment at 2026-08-25T01:12:27Z, production CloudWatch shows 48 '[caretta] incoming webhook' and '[caretta] event parsed' executions. There are 27 '[caretta] mapped to Cedar meeting' successes across 19 distinct external calls and 3 organizers; all 27 successful.
Since 2026-08-25T01:12:27Z, CloudWatch for production api-service recorded 48 `[caretta] incoming webhook` and 48 `[caretta] event parsed` logs. Of these, 27 were verified and emitted `[caretta] mapped to Cedar meeting`; 21 were explicitly rejected as `[caretta] no Cedar user.
Since 2026-08-25T01:12:27Z, CloudWatch recorded 48 `[caretta] incoming webhook` and 48 parsed events. Of these, 27 attendee-matched deliveries logged both `[caretta] no org signing secret — accepting on attendee match alone (unauthenticated)` and `[caretta] verified` with.
Libra has verdicts on 1 of 6 tracked behaviors on prod; 5 are still being checked. Libra has 1 low-confidence broken verdict it is still confirming. Libra checks hourly for 3 days after each deploy.
Caretta started pushing call events to /webhooks/meeting-notes/caretta on 2026-08-24. All 77 deliveries that day were rejected, so no meeting reached Cedar and no post-meeting recap could be drafted for Rhocar at Corgi.
| Reason | Count |
|---|---|
no_user_found_for_owner_email | 43 |
webhook_secret_not_configured | 34 |
Both return 401, which is why Caretta kept retrying , those 77 are retry ladders, not 77 distinct calls.
1. Routing was owner-only. verifyCarettaRequest resolved data.call.owner.email and gave up if that address had no Cedar account. At Corgi only rhocar@ and <email> have accounts; Kian, Eddy, Moksh, Omar and Simon do not. Every call a teammate organized was dropped even with a Cedar user in the room.
Now matches every attendee (owner and participants) via a Google connection.email then user.email, mirroring findUserIdsFromCirclebackPayload, and returns userIds. The multi-user fan-out plumbing already existed through MeetingVerificationResult → verifyAndParseMeetingWebhookRequest → the webhook handler, so no shared code changed. Batched into two inArray queries rather than per-attendee lookups, since this runs before any signature check.
2. A signing secret was mandatory, and no org has ever had one. Zero rows in organizations.provider_configs carry a caretta key. Signing is now optional.
Security note: secrets are resolved across every matched user's org, not just
userIds[0]. Resolving only the first would let a caller pick which org's secret applies simply by ordering the participants, so adding one attendee from a secret-less org would skip verification for an org that had configured one. If any matched org has awhsec_, a valid signature is mandatory.With no secret configured anywhere, this endpoint is unauthenticated: the payload, and therefore the attendee list it routes by, comes from the caller. Attendee matching is routing, not authentication. That is a deliberate trade to unblock a customer, documented at the call site, and closed by configuring the org secret.
3. The driver's logs reached neither sink. Every diagnostic called createStructuredLog bare, which returns a string and fire-and-forgets to Axiom without writing stdout. Nothing was in CloudWatch, and nothing landed in Axiom either, so owner_email / call_id / event type were unrecoverable , the exact fields needed to diagnose the failure. All sites now emit to both.
meetingTime came from the envelope's occurred_at, which is when Caretta emitted the event, not when the call started. A notes_ready firing after the call pushes the meeting past the ±2 minute window findConversationFromMeetingCalendarLink uses to reach the calendar entry and, through it, the deal. The MCP mapper already did this correctly (toDate(call.start ?? call.createdAt)); only the webhook path didn't.googleCalendarEventId was never set on either path, despite CarettaMcpCall.eventId being modeled and left unread, so crm_meeting_events.calendar_event_id stayed null.Both now read from the call object. The webhook wire shape is not yet confirmed against a live payload, so several plausible key spellings are accepted and the key that actually supplied the value is logged as meeting_time_source / google_calendar_event_id_source. One real call turns both inferences into facts.
Removes no_user_found_for_owner_email from expectedFailureReasons in app.ts; caretta was its only producer. The replacement no_users_found_matching_participants is already on that list, so the Slack alert spam from the 34 daily rejections stops.
call.id, and meetingHasValidExecution keys on dedupeExternalId = call.id, so the first event wins. If `call.coLibra has not measured any production surfaces for this change yet.