CedarCopilot wants to merge 1 commit into staging from fix/meeting-dedup-hides-transcripts
Live on prod, no production signal yet
Libra has verdicts on 0 of 1 tracked behaviors on prod; 1 is still being checked. Libra checks hourly for 3 days after each deploy.
Two attendees' notetakers in one room produce two genuinely distinct recordings: different
provider external_ids, each with its own transcript , or none, when the bot joined and
captured nothing. Read-time dedup collapsed those into a single row, and the row that survived
could be the empty one. The transcript then disappeared from every reader, agent included.
Live example, conversation 2925df6c-9de2-4c8a-acb3-8cda006d8185, both rows ingested by the
same backfill run and owned by the same user:
| event | external_id | transcript |
|---|---|---|
a3764348… | 01M10A6SAJVBEF3797VRPS3P78 | ✅ 2,976 words, notes + ai_notes |
e37a9431… | 01M1512M2AKF7YY05XXFXSQPJ4 | ❌ none, no notes |
conversation-read with expandEventId on the good row answered "Event with ID … not found
in conversation" , it was filtered off the timeline before the search ran.
services/crm/dedup.ts1. Inverted key hierarchy. getDedupKey returned the title + 10-minute-slot key whenever
both were present, and only fell back to meeting:${externalId} when one was missing. The
email branch does the opposite and is correct: the definitive rfcMessageId is tier 1, the
approximate key tier 2.
2. The approximate key was applied with tier-1 semantics. isWeakKey matched only the
email-approx: prefix, so the meeting key was not recognised as weak and dedupEventsForReader
collapsed a group even within a single owner , precisely the failure the module's own doc
comment warns about at length for email, where collapsing a same-owner group would "silently
hide 27 real emails". The meeting branch had no such guard.
3. The survivor was picked by array position. pickWinner step 1 short-circuited on the
first row matching the primary owner, which matches both rows when they share an owner. Step
2 ("prefer a row with agent-enriched data") was never reached and could not have discriminated
anyway , both live rows carry a non-null summary.
Worth being precise about #3: this was not "the empty one always wins", it was
nondeterministic. The pair ties on is_significant and on occurred_at, which is the
entire ORDER BY the reader query applies (conversations.ts:3444), so the survivor was
decided by the query plan. Verified in prod , both rows: is_significant = t,
occurred_at = 2026-08-28 20:30:00.
externalId promoted to tier 1 for meetings. One recording keeps one id however many
members mirror it; two notetakers produce two ids because they are two recordings.meeting-approx: so isWeakKey
recognises it , a same-owner group now passes through intact, matching the email tier-2
contract. meetingUrl keeps its position as the last-resort fallback.EMAIL_APPROX_PREFIX,
MEETING_APPROX_PREFIX, WEAK_KEY_PREFIXES), so a new weak key cannot be introduced without
isWeakKey learning about it. That drift is what let the meeting key be treated as exact.pickWinner prefers content over position. Ownership now narrows the field rather than
deciding the winner outright , the reader's copy still carries the per-mailbox ids they can
actually open, but when the reader owns several rows in the group, the richest of those wins
(contentRank: transcript > notes > summary), ties breaking deterministically on the oldest
createdAt.Every recording still collapses across owners. Measured over 90 days of live data, on
per-conversation (title, 10-min slot) groups holding more than one distinct external_id:
| groups | events | rows a reader sees | |
|---|---|---|---|
| same-owner (the bug) | 160 | 327 | 160 → 327 |
| cross-owner (must keep collapsing) | 110 | 372 | 110 → 232 |
The same-owner row is the fix: 167 genui
Libra has not measured any production surfaces for this change yet.