Libra CodeHub

CedarCopilot/cedar-mail

Branch: staging

fix(before-meeting): re-arm prep when a meeting moves after the prep already ran

merged#2635CedarCopilot

CedarCopilot wants to merge 2 commits into staging from fix/before-meeting-prep-lost-on-reschedule

Live on prod, no production signal yetTimeline and evidence
  1. Opened
    Aug 31, 2026, 9:53 AM
  2. Aug 31, 2026, 10:50 AM
  3. Merged
    Aug 31, 2026, 12:43 PM
  4. Live on staging
    Aug 31, 2026, 12:43 PM
  5. Live on prod
    Aug 31, 2026, 12:43 PM
  6. Observed 0 hours
    Aug 31, 2026, 12:43 PM
  7. Unobserved

    Live on prod, no production signal yet

    Aug 31, 2026, 12:43 PM
  8. Pipelines steady after this deploy
    Aug 31, 2026, 12:43 PM
  9. Pipelines steady after this deploy
    Aug 31, 2026, 12:43 PM

Behaviors Libra is checking

Calendar sync behavior from crm/calendar-events.ts keeps working in staging.Inconclusivelow confidence

Strict CloudWatch fallback found 23 recent prod failure log lines for [SCHEDULED] Failed to enrich unenriched calendar event (Failed query: insert into \), but the same failure had 60 log lines in the comparable pre-deploy baseline. Libra is not blaming this PR intent without a.

staging, checked Sep 2, 2026, 2:42 AM
Meeting import behavior from scripts/backfill-lost-before-meeting-preps.ts, scripts/run-backfill-lost-before-meeting-preps.mjs keeps working in staging.Inconclusivelow confidence

Strict CloudWatch fallback found 20 recent prod failure log lines for [SCHEDULED] Failed to enrich unenriched calendar event (Failed query: insert into \), but the same failure had 60 log lines in the comparable pre-deploy baseline. Libra is not blaming this PR intent without a.

staging, checked Sep 2, 2026, 3:42 AM
Calendar sync behavior from crm/calendar-events.ts keeps working in prod.Not checked
prod
Meeting import behavior from scripts/backfill-lost-before-meeting-preps.ts, scripts/run-backfill-lost-before-meeting-preps.mjs keeps working in prod.Not checked
prod

Failures attributed to this change

No prod customers are affected while this is only in staging. If promoted, prod impact is unknown because Libra still needs a concrete exception, route, and failed user action before assigning severity. 0 hits · no retained affected-user count · no retained trace sample.Introducedmedium confidence

internal_only

staging, first seen Aug 31, 2026, 2:27 PM
No prod customers are affected while this is only in staging. If promoted, prod impact is unknown because Libra still needs a concrete exception, route, and failed user action before assigning severity. 0 hits · no retained affected-user count · no retained trace sample.Introducedmedium confidence

internal_only

staging, first seen Aug 31, 2026, 2:27 PM
No prod customers are affected while this is only in staging. If promoted, prod impact is unknown because Libra still needs a concrete exception, route, and failed user action before assigning severity. 8 hits · no retained affected-user count · no retained trace sample.Introducedmedium confidence

internal_only

staging, first seen Aug 31, 2026, 2:27 PM
No prod customers are affected while this is only in staging. If promoted, prod impact is unknown because Libra still needs a concrete exception, route, and failed user action before assigning severity. 8 hits · no retained affected-user count · no retained trace sample.Introducedmedium confidence

internal_only

staging, first seen Aug 31, 2026, 2:27 PM
No prod customers are affected while this is only in staging. If promoted, prod impact is unknown because Libra still needs a concrete exception, route, and failed user action before assigning severity. 8 hits · no retained affected-user count · no retained trace sample.Introducedmedium confidence

internal_only

staging, first seen Aug 31, 2026, 2:27 PM
No prod customers are affected while this is only in staging. If promoted, prod impact is unknown because Libra still needs a concrete exception, route, and failed user action before assigning severity. 0 hits · no retained affected-user count · no retained trace sample.Introducedmedium confidence

internal_only

staging, first seen Aug 31, 2026, 3:29 PM
No prod customers are affected while this is only in staging. If promoted, prod impact is unknown because Libra still needs a concrete exception, route, and failed user action before assigning severity. 0 hits · no retained affected-user count · no retained trace sample.Introducedmedium confidence

internal_only

staging, first seen Aug 31, 2026, 3:43 PM
No prod customers are affected while this is only in staging. If promoted, prod impact is unknown because Libra still needs a concrete exception, route, and failed user action before assigning severity. 0 hits · no retained affected-user count · no retained trace sample.Introducedmedium confidence

internal_only

staging, first seen Aug 31, 2026, 3:43 PM
No prod customers are affected while this is only in staging. If promoted, prod impact is unknown because Libra still needs a concrete exception, route, and failed user action before assigning severity. 0 hits · no retained affected-user count · no retained trace sample.Introducedmedium confidence

internal_only

staging, first seen Aug 31, 2026, 3:43 PM
No prod customers are affected while this is only in staging. If promoted, prod impact is unknown because Libra still needs a concrete exception, route, and failed user action before assigning severity. 0 hits · no retained affected-user count · no retained trace sample.Introducedmedium confidence

internal_only

staging, first seen Aug 31, 2026, 3:43 PM
No prod customers are affected while this is only in staging. If promoted, prod impact is unknown because Libra still needs a concrete exception, route, and failed user action before assigning severity. 0 hits · no retained affected-user count · no retained trace sample.Introducedmedium confidence

internal_only

staging, first seen Aug 31, 2026, 3:43 PM
No prod customers are affected while this is only in staging. If promoted, prod impact is unknown because Libra still needs a concrete exception, route, and failed user action before assigning severity. 0 hits · no retained affected-user count · no retained trace sample.Introducedmedium confidence

internal_only

staging, first seen Aug 31, 2026, 3:43 PM

Libra has verdicts on 0 of 2 tracked behaviors on prod; 2 are still being checked. Libra checks hourly for 3 days after each deploy.

What's broken

Move a meeting to a later date after its before-meeting prep has already fired, and the new occurrence gets no prep at all. Nothing errors, nothing logs a failure, and no execution row exists , the brief simply never appears.

Reported by Willem Ebbinge (Listen Labs): his 9:00 AM "TPG <> Listen Labs pricing sync" on Mon Aug 31 had no prep. The identical meeting a week earlier did. The event had been moved from Aug 25 at 9:06 AM, six minutes after that call started and two hours after its prep had completed.

Fleet-wide this cost 571 preps across 57 users.

Root cause

The before-meeting dedupe key is ${googleEventId}:before-meeting:playbook:${orgAopId ?? aopId}:${stageKey}:${minutes} , no start time, so every occurrence of a Google event maps to one key forever. The duplicate guard skips scheduling when a row under that key exists in ('pending','executing','completed','final','canceled').

cancelPendingBeforeMeetingAutomationsForStartTime does retire superseded keys so a reschedule can re-create , but it only selects status = 'pending'. A prep that already completed is never retired, so the replacement hits the guard and is dropped.

The retirement mechanism landed in 2ae007da7 (Aug 21) to stop cancel/reschedule churn. It correctly covers the pending case; the completed case was never in scope.

Production rows for the affected event:

run_id     status     dedupe_external_id                            scheduled_for        completed_at
90c2d7fe   completed  hkomo2udun...:...:global:120                  2026-08-25 14:00:00  2026-08-25 14:03:21

That fired for the Aug 25 slot. The event then moved to Aug 31. There is no second row.

The fix

Retire the dedupe keys of terminal (completed/final) before-meeting runs on a genuine reschedule, reusing the existing SUPERSEDED_DEDUPE_PREFIX. The run itself is untouched, so the history of what was sent for the old time survives; only the key is released, freeing the guard to schedule the new occurrence.

When a replacement is scheduled over a retired key, the agent is told a prep already exists so it refreshes and re-delivers rather than starting cold:

"This meeting was rescheduled and you already wrote a prep for its previous time. Refresh that prep for the new time: keep what still holds, update anything that changed since, and deliver it again."

That probe filters on terminal status deliberately , the pending path also retires keys on runs it cancels (a meeting moved before its prep fired), and those preps never produced anything, so claiming "you already wrote this" there would be false.

Also removes a dead conversation lookup whose result was discarded on every reschedule.

Tests

before-meeting-reschedule-after-prep-ran.test.ts, built on the real TPG data. Against the unfixed code, 3 of 6 failed:

AssertionError: expected [] to deeply equal [ { dedupeExternalId: 'superseded:hkomo…:global:120' } ]
AssertionError: expected [] to have a length of 1 but got +0
AssertionError: expected "vi.fn()" to be called 1 times, but got 0 times

Now green. Verified: 68 tests across 6 suites (including the pre-existing participant and direct-fire guard suites), pnpm --filter @zero/server run types clean, pnpm deps:check clean at 1613 modules.

Backfill (not yet run)

Events already affected never change again, so the fix alone cannot recover them. scripts/backfill-lost-before-meeting-preps.ts retires their stale keys and schedules replacements; dry run by default. Currently 73 actionable meetings across 22 users. It must run where scheduleAgentExecution can reach the queue and KV , the internal /schedule-execution route does not forward dedupeExternalId, so rows created through it would carry no key and could double-book.

Follow-up worth considering

The test double makeDb is duplicated with schedule-before-meeting-participants.test.ts and both couple to the order of select() call

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/scripts/backfill-lost-before-meeting-preps.tsno production surface mapped
  • apps/server/scripts/run-backfill-lost-before-meeting-preps.mjsno production surface mapped
  • apps/server/src/services/crm/__tests__/before-meeting-reschedule-after-prep-ran.test.tsno production surface mapped
  • apps/server/src/services/crm/calendar-events.tsno production surface mapped
  • docs/bug-reports/before-meeting-prep-lost-on-reschedule.mdno production surface mapped