An inbound message could create a second conversation for a contact
under a race (Meta retries a delivery, or a batch fans out to concurrent
after() runs). There was no UNIQUE index and no unique-violation
backstop on conversations, unlike contacts (migration 022). Worse, once
two conversations existed the `.single()` lookup errored on every later
message and the webhook created yet another conversation each time,
snowballing into a wall of duplicate chats.
- findOrCreateConversation now resolves oldest-first with .limit(1)
instead of .single(), so pre-existing duplicates converge, and catches
23505 on insert to re-resolve the winning row.
- Same hardening applied to the public-API resolver (resolve-conversation).
- Migration 036 merges existing duplicate conversations into the oldest
thread (re-pointing messages, reactions, deals, flow_runs,
notifications, ai_usage_log — no data loss) then adds a
UNIQUE (account_id, contact_id) index as the authoritative guarantee.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>