CedarCopilot wants to merge 2 commits into staging from fix/hot-lead-webhook-duplicate-and-transient-db-retry
Live on prod, no production signal yet
Libra has verdicts on 0 of 8 tracked behaviors on prod; 8 are still being checked. Libra checks hourly for 3 days after each deploy.
Traced from Aspire's inbound-lead-research webhook (2026-09-03), two related gaps in the playbook-webhook automation path:
1. No idempotency anchor for webhook triggers. resolveAutomationDedupeScope had keyed dedup for cron (tick + optional conversation) and event_occurred (CRM event id), but webhook fell through to the plain time-window fallback , and unlike every other time-window caller, a webhook trigger is conversationless, so the query was just "any execution for this agent+user in the last 5 minutes?" with nothing to tell two different deliveries apart. This produced two live symptoms:
[runAgent] Skipping duplicate agent execution / `dedupe_key=[redacted] Neither lead ever got a Cedar response.2. No retry on a transient Postgres write failure. A ~9-second Postgres failover blip hit updateExecution's finalization write with 25006 read_only_sql_transaction. The agent's Slack post had already succeeded, but the status write failing made the queue consumer treat the whole dispatch as failed and retry it , reproducing the Slack post a second time.
services/agent-action-queue/automation-dedupe.ts , new webhook branch, keyed on (agentId, webhookId, firedAt), no minute-bucketing (unlike cron) since firedAt is now an exact per-delivery anchor, not a resampled clock reading.services/playbook/playbook-webhook.ts , dispatchPlaybookWebhookMessage now passes firedAt derived from the message's own receivedAt (fixed once at first enqueue) instead of leaving it unset, which previously defaulted to Date.now() on every attempt.db/transient-retry.ts (new) , the retry helper promoted from services/crm/email-threads.ts, with 25006 added to its transient-error code list. email-threads.ts now imports it instead of keeping its own copy.services/agent-action-queue/helpers.ts / executions.ts , updateExecution, createImmediateExecution, and createSubagentExecution (same file/shape as updateExecution, on the same hot per-event path) now retry on a transient Postgres error.db/__tests__/transient-retry.test.ts (new), automation-dedupe.test.ts (4 new webhook cases), playbook-webhook.test.ts (firedAt threading), automations-service.test.ts (DB-mock wiring test proving runAgent extracts webhookId and writes the composed dedupe key).pnpm --filter @zero/server run types and pnpm deps:check both clean.#hot-lead-demo-request for a burst of 2+ leads within 5 minutes , every one should now get its own inbound-lead-research reply, and a redelivered webhook message should no longer double-post.🤖 Generated with Claude Code
https://claude.ai/code/session_01HBS8XHFE8jypQBgSzJimRy
This PR gives webhook-triggered agent runs a stable deduplication key and adds transient PostgreSQL retries around execution writes while extracting the existing CRM retry helper.
The retry behavior appears sound, but t
Libra has not measured any production surfaces for this change yet.