Plan · In progress
One conversation per person
2026-08-05 · from the “multiple creations for convos” ticket + Monday’s standup · full technical doc ships with the PR as docs/planning/conversations-one-thread-per-person.md
Why the conversations page shows the same person over and over, why a message sent today could land on page 3, and the fix that's already being built.
The short version. Half the conversations list is repeats — the same person shown again for every phone call they ever made. We're collapsing the list to one row per person: open it and you see their calls, texts, and emails as one message history, newest activity always on top. Nothing gets deleted — every call and its recording stays underneath. The first piece is written and tested; the PR goes up for review now.
What the data says (production, measured Aug 5)
801conversation rows for real customers
395actual people behind them
50.7%of the list is a repeat of someone already on it
126rows for one single repeat caller
Also checked: every multi-row person maps to one phone number (or their own email), so grouping by person can never merge two different humans into one row.
Why it happens — three separate problems
1 · Every phone call opens a brand-new conversation
The moment a phone rings, a new conversation row is created — no check for whether that person already has one. A repeat caller gets a new row every single time. Those per-call rows are also our call log (recording, summary, review links), which is exactly why the fix must not delete them.
2 · There's no single place that decides which thread a message lands in
Six different pieces of code independently answer “which conversation does this message go into?”, each keyed differently. The manual follow-up text from this week landed inside a months-old phone-call thread because the picker matches on bare phone number with no preference for the right kind of thread. This is exactly the “single point of entry” Fede called for at standup.
3 · The list can sort a fresh message months back
The message did bump the conversation’s “last activity” correctly on the server — but the page sorts unknown-caller threads by an older captured-date field that shadows it. That thread sorted as June 1, fell outside the default 30-day window, and needed “all time” + page 3 to find. A second, quieter bug let a stale background save move a thread backwards in the list.
The fix — two layers, riding what we already built
Now: the page shows people, not rows (PR going up)
- One row per person — the list groups by person; a repeat caller shows once with a small “×N”. Search and topic filters find the person if any of their threads matches, and junk (hang-ups, spam) never hides someone who also has a real conversation.
- One timeline per person — opening a row shows calls, texts, and emails merged in order, using the same person-merge machinery the prospect page already uses (same security scoping, already reviewed and shipped).
- Sort is honest — newest real activity wins, always; the two sort bugs above are fixed with regression tests.
- Nothing is lost — the per-call rows, recordings, transcripts, traces, and existing deep links all keep working underneath.
Next: one single point of entry for writing to a conversation
One shared person-first resolver decides the target thread for every outbound message — person record first, right property, right kind of thread, most recent wins — replacing the six independent pickers. That's what stops new wrong-thread messages at the source. Tracked in issue #5004; ships as its own PR right after.
What we deliberately did not do
- No physical merging of the data. Squashing rows together would destroy the per-call log and need a risky production migration — and the duplicates would just come back while the writers keep minting new threads. Group on display, fix the writers: cheaper, safer, reversible.
- Renewal threads keep their own container per cycle (that's deliberate, from the earlier renewal-thread fix) — they simply fold into the person's row visually.
- The handful of leftover duplicate renewal rows and one runaway test conversation get a separate small cleanup.
How we'll know it worked
Unit tests pin the grouping, the sort fixes, and the never-bury rule. After deploy: the conversations page at Camellia shows ~one row per person instead of 730 rows, and this week's buried follow-up sits on page 1 with its real send time.