A renter who emails about one home and later phones about another shows up as two people, with two threads, and today nothing joins them unless they become a resident. This page says exactly where the join is missing, lays out four ways to close it, and recommends one.
2026-09-23 · proposed — waiting on the decider · question (c) of issue #11098, handed to the identity lane · code facts checked against the main branch that evening (cb909e636f) · roles only, no customer names · answer on the decision link the Driver raises, not here
Recommendation: A now, B as part 2. When a record the renter filled in themselves — a guest card, an application, a tenant row — carries both a phone and an email, and the phone belongs to one of our records while the email belongs to another, treat that as proof they are one human and fold the two, under the merge guards that already exist plus one new one (a real name on both sides that agrees). Then, as a second step, let the same rule fire when the person gives Clara their other contact in their own thread. Never ask the property manager to adjudicate. Never join on a name alone.
On 2026-08-14 the owner ruled that within one company, the same phone number or the same email address means the same human, and the two records merge automatically wherever the collision surfaces — no review queue, no property-manager approval, ever. Two guards ride on it: two records that are each anchored to their own lease are two real humans who share a contact (a couple, a co-signer) and never merge; and a pair whose only PMS evidence is a guest card refuses to merge when the two names disagree. That rule is shipped and is not the question.
The dock One person, one conversation fixed the case that opened #11098: one person record with two inquiries at two homes was getting two threads. Its join rule keys a thread on the person and the company. It cannot help here, because here there are two person records — the rule sees two people and correctly gives them two threads.
How the two records are born. Every inbound thread stamps a person at write time through one adapter, which dispatches on the shape of the sender: a phone-shaped sender mints (or reuses) a person by phone; an email-shaped sender does the same by email. At the moment a thread is created only the sender's own number or address is recorded, so each record is born with exactly one contact fact; an email a texter volunteers later, or a phone a caller states, travels a different path (path 3 below), and nothing on the inbound-email path harvests a phone from the message body at all. Both records are marked as inbound-asserted (the lower trust tier).
When they are ever joined. Only when a later signal carries both values and flows through a path that is wired to the merge rule:
Where a bridge arrives and the join is refused. Three paths carry both values and stop short — and they are the ones prospects actually travel:
What can never be joined. A person who only ever emails and only ever texts, with no record anywhere carrying both, is two people under every automatic option on this page. A matching name is not an anchor — that ruling (the lead-pipeline audit, PR #5625) stands and nothing here weakens it. That remainder is staff work, and its size in production is unmeasured: the read-only split-pair report is the instrument, and it has not been run for this question.
Two asymmetries worth saying out loud. The same human is folded on the next sync tick if they are a resident, and never if they are a prospect — the population the dock is about. And the same texter is folded if they write in about a home where they have no record, and left split if they write in about a home where they do — the join depends on which home they mention, which no one would design on purpose.
cb909e636f)| Where | What it does | Effect on a phone-first + email-first pair |
|---|---|---|
conversation-spine-stamp.ts:1–30 (ensurePersonForConversation) | dispatches on the sender's shape: E.164 → phone signal; email-shaped → email signal; relay addresses → session claim | each inbound record is born with exactly one claim type |
identity-merge-decision.ts:10–25, 376, 417 (decideIdentityMerge) | the 2026-08-14 rule: same value = same human, merge; permanent refusal both_lease_anchored; guest_card_pair_name_mismatch | the policy exists; the paths below never reach it |
identity-merge-heal.ts (healIdentityCollision) → merge-persons.ts (mergePersons) → merge-move-references.ts | the one sanctioned merge: re-mint claims on the survivor, deactivate roles, soft-redirect the loser, eagerly move conversations / occupancies / inquiries | the mechanism every option below reuses |
person-stamp.ts:1253–1300 (PMS-anchored top-ups), :1436, :1474 (phone-reuse), :1594, :1638 (email-reuse), :1972 (mint) | every collision inside ensurePersonForSignals calls tryIdentityMerge | the rent-roll sync and the prospect CREATE branch fold the pair |
resolve-known-applicant-person.ts:75–136 | unions the ids behind the email and the phone; survivors.size > 1 → { ambiguous: true } (line 136) | the bridge is refused here |
guest-card-mirror.ts:581–604 | calls that resolver; on ambiguous stamps personMiss (597–604) — the card gets no person | the first-email composer cannot find either thread |
prospects-from-mirror.ts:728–730 | same resolver; if (!match || 'ambiguous' in match) return; | same silent stop |
resolve.ts:1261 (create branch → ensurePersonForProspectSignals) vs resolve.ts:1291 (attach branch → backfillPersonContact) | the create branch merges on collision; the attach branch fills only | the same texter joins or stays split depending on whether they already have a prospect row at that home |
backfill-person-contact.ts:204–212 | an active claim on another person → 'owned_by_other_person', "merge candidate, nothing written" | the volunteered contact never joins |
tools-leasing.ts:3116, :6347; postcall-prospect-capture.ts:233 | the three callers that hand a phone + volunteered email to the resolver | all land on the attach branch when the phone already matches |
name-compatibility.ts:80–94 (isNameCompatiblePerField) | a name part absent on either side is an absence, never a contradiction | an email-first record with no structured name is "compatible" with anyone — see the wrong-merge mechanism under option A |
src/app/api/admin/identity/merge/route.ts | staff-only manual merge from Atlas; wraps softRedirectPerson only, not mergePersons | a hand merge today redirects the row but leaves the loser's claims live and moves no rows |
scripts/report-split-identity-pairs.ts | read-only, one-off: proposes pairs with hard evidence (PMS record / own partition) and runs them through the shipped decision | the instrument for the unmeasured remainder |
identity-merge-telemetry.ts | structured identity_merge_event lines: merged / skipped(reason) / failed; no queue, no UI, by ruling | how A and B are watched |
Searched for a phone-claim writer on the inbound-email path (src/lib/integrations/email/, the tenant-sender extraction rescue in webhook-processors.ts:2594): the rescue harvests operational signals (unit, work order), not contact. None found. Positive control: the email-claim writer on the same path (conversation-spine-stamp.ts) was found by the same search.
A renter sees a listing for home A on a listing site and sends an inquiry. It arrives as an email; Clara answers from the company mailbox, asks about move-in date and pets, and books a showing. That is thread 1, on an email-first record.
Ten days later the same renter drives past home B, run by the same company, and calls the number on the sign. The number is new to the system, so Clara greets them as a stranger, introduces herself again, and asks move-in date and pets again. That is thread 2, on a phone-first record. The renter is mildly annoyed and answers the same questions twice.
The PMS then creates a guest card for the home-B inquiry — and the renter, being a normal person, fills in both their number and their email. The card arrives with a bridge. The mirror asks "who is this?", gets two answers, calls it ambiguous, and stamps the card with no person. So the first-email path, which composes Clara's opening message from the card and now knows how to hand it the person's earlier thread, cannot find thread 1 or thread 2.
What the PM sees: two people in the list with the same name, each holding half a history. If they text "the prospect" from the email-first record, there is no number. If they read the phone thread, there is no showing. When the renter later applies, the application lands on whichever record the sync happens to match, and the other one keeps sending "did you want to apply?" follow-ups — the shape of the two incidents the Aug-30 merge page recorded.
The company's "one thread per person" switch changes none of this. It joins threads by person; this renter is two persons.
For each: what the property manager actually sees, what changes underneath, the wrong-merge risk (a wrong merge is one person's messages, tour and application shown to another — a privacy leak and a fair-housing exposure, not a data nit), roughly how big it is, how you get back out, and which existing seams it uses.
resolve-known-applicant-person.ts:136 · guest-card-mirror.ts:581–604 · prospects-from-mirror.ts:728 · healIdentityCollision → mergePersons → merge-move-references · telemetry identity_merge_event · the dock's person-first join then owns the thread.resolve.ts:1291 · backfill-person-contact.ts:204–212 · tools-leasing.ts:3116, :6347 · postcall-prospect-capture.ts:233 · volunteered-email.ts (shape gate, kept) · conveyed-contact.ts (kept separate).report-split-identity-pairs.ts · POST /api/admin/identity/merge (would need mergePersons).resolve-known-applicant-person.ts:136 today and is discarded there. Nothing new has to be collected.What A does not fix, stated plainly: the never-bridged remainder (two channels, no record ever carrying both). Its size is unknown; measuring it with the split-pair report is the first task after A ships, and C is how it gets worked.
When one record carries both a phone number and an email address — a guest card, a rental application, or something the person tells Clara in their own thread — and the phone resolves to one person record while the email resolves to a different one, should that record count as proof the two records are one human, and fold them automatically?
A now, B as part 2 once A's telemetry shows the guards holding; C as the floor for what A and B refuse; D rejected. A is the one case where a record the renter themselves filled in asserts the pairing, it already reaches the line that gives up today, and the 2026-08-14 merge policy and its guards were written for it.
Prepared by the identity research lane for Driver phases-zero-ten, 2026-09-23. Sources: docs/architecture/person-lookup.md, docs/architecture/spine-stamp-pattern.md, docs/runbooks/spine-stamp.md, docs/runbooks/identity-ops.md, the identity modules named above, the person-conversations dock and its driver brief, the 2026-08-30 duplicate-leads merge page, and the founders' 2026-09-21 standup transcript. No production data was read.