CedarCopilot wants to merge 2 commits into staging from fix/before-meeting-prep-lost-on-reschedule
Live on prod, no production signal yet
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.
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.
internal_only
internal_only
internal_only
internal_only
internal_only
internal_only
internal_only
internal_only
internal_only
internal_only
internal_only
internal_only
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.
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.
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.
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.
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.
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.
The test double makeDb is duplicated with schedule-before-meeting-participants.test.ts and both couple to the order of select() call
Libra has not measured any production surfaces for this change yet.