Same person, two channels

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.

The three words this page uses

What is already settled — and is not re-opened here

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.

What is true today

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:

  1. The guest-card mirror. Every guest card the PMS sends is asked "who is this?" by looking up its email and its phone. When the two lookups return two different live records, the resolver returns ambiguous, the mirror stamps the card as a miss and moves on. The strongest evidence the system will ever hold — the renter's own card naming both — is thrown away, and the card is left with no person at all. The lead path that composes Clara's first email from that card therefore cannot see either thread.The refusal is deliberate and was right for its original purpose: a household card with one person's phone and another's email must not be guessed at. But "ambiguous" today covers the one-human case and the two-humans case with one word, and the merge policy that can tell them apart is never consulted.
  2. The mirror's lead-activity pass — same resolver, same two-record answer, same silent return.
  3. The person tells Clara their other contact, in a thread that already matched them. A texter who books a tour and gives an email, or a caller whose email is captured after the call: the resolver matches the phone-first record, then tries to fill the email onto it. The fill-only writer sees the address is actively owned by another record and stops: "merge candidate, nothing written". A warning is logged. Nobody reads it.

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.

Technical detail — the exact lines (main branch, 2026-09-23, cb909e636f)
WhereWhat it doesEffect 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 claimeach 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_mismatchthe policy exists; the paths below never reach it
identity-merge-heal.ts (healIdentityCollision) → merge-persons.ts (mergePersons) → merge-move-references.tsthe one sanctioned merge: re-mint claims on the survivor, deactivate roles, soft-redirect the loser, eagerly move conversations / occupancies / inquiriesthe 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 tryIdentityMergethe rent-roll sync and the prospect CREATE branch fold the pair
resolve-known-applicant-person.ts:75–136unions 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–604calls that resolver; on ambiguous stamps personMiss (597–604) — the card gets no personthe first-email composer cannot find either thread
prospects-from-mirror.ts:728–730same 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 onlythe same texter joins or stays split depending on whether they already have a prospect row at that home
backfill-person-contact.ts:204–212an 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:233the three callers that hand a phone + volunteered email to the resolverall 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 contradictionan 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.tsstaff-only manual merge from Atlas; wraps softRedirectPerson only, not mergePersonsa hand merge today redirects the row but leaves the loser's claims live and moves no rows
scripts/report-split-identity-pairs.tsread-only, one-off: proposes pairs with hard evidence (PMS record / own partition) and runs them through the shipped decisionthe instrument for the unmeasured remainder
identity-merge-telemetry.tsstructured identity_merge_event lines: merged / skipped(reason) / failed; no queue, no UI, by rulinghow 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.

What it does to a real person

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.

The question, and the four honest answers

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.

A The PMS record decides recommended

The rule
A guest card, rental application or tenant row that carries both a phone and an email, where the phone resolves to one live record and the email to another, folds the two — through the shipped merge decision and its guards (two lease-anchored records never merge; a guest-card pair whose names disagree never merges), plus one new requirement: both records carry a real, structured name, and the names agree. A name the system derived from an email address does not count. Anything short of that stays split and is reported as a skipped merge for staff.
What the PM sees
Nothing to do. Within one sync tick (about fifteen minutes) the two people become one, the two threads sit under one person, and Clara's next opening message knows the earlier conversation. Threads already split on production are joined by the dock's merge tool (its row 13 / 26), not by this.
What changes
The applicant resolver returns the pair instead of "ambiguous" when the email maps to exactly one live record and the phone to exactly one other; the guest-card mirror and the lead-activity pass hand that pair to the existing collision heal, which decides and merges; the mirror then stamps the survivor. Two small PRs, dark behind the same company setting the dock uses.
Safety
the one real wrong-merge shape A household card: one partner's phone, the other's email. Today's name guard alone would let it through, because an email-first record often has no structured name at all and "no name" reads as compatible with any name. The new requirement closes that: no real name on both sides, no automatic merge. The lease-anchored guard stays structural. And a false merge here is cheaper than the same false merge elsewhere, because the card names both values — the evidence is inspectable after the fact.
Cost
small Two PRs of a few hundred lines; unit tests over the pure decision; a replay of the mirror over a scrubbed guest-card corpus that lists every pair it would fold. No new policy — this is wiring two paths to the rule that already exists.
Reversibility
same as every merge today There is no un-merge primitive: the loser is a tombstone with a redirect, its claims are deprecated (kept for audit), its threads and inquiries are moved. Undo is a hand repair. The Aug-30 merge page asked for reversible merges and has no recorded answer; A inherits that gap and does not widen it.
Seams
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.

B A, plus what the person says about themselves in their own thread

The rule
Everything in A, and: a texter who gives Clara their email, or a caller who states one, whose value belongs to a different live record, folds onto it under the same guards and the same real-name requirement — instead of today's "merge candidate, nothing written".
What the PM sees
The person who says "email me at …" becomes one record on the spot, mid-conversation, and the showing they booked by email shows up in the text thread.
What changes
The fill-only contact writer's "owned by another person" branch routes through the collision heal. One PR; three callers (the tour tool, the save-prospect tool, the post-call capture).
Safety
weaker evidence than a card Two ways it goes wrong: a model transcribes an address off a call slightly wrong (the volunteered-email gate checks shape, not ownership), and people give someone else's contact on purpose ("use my wife's email"). The conveyed-contact lane already exists for the second case and refuses to merge on a third party's say-so; B must stay out of it. Same guards as A; the real-name rule does most of the work. Asking the person ("are you the same person who emailed about home A?") is not a separate option: it is a rung-2 conversational surface, and a household member says yes as readily as the card would — it adds no evidence the record does not already carry.
Cost
small One PR, but it is a Clara-path change on the inbound reply, so it carries multi-turn evals and the real-path check.
Reversibility
Same as A.
Seams
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).

C Staff-confirmed only

The rule
Never automatic. PropFlow staff — never the property manager, by the owner's ruling — run the split-pair report, read each pair, and merge in Atlas.
What the PM sees
Two people stay two people until a sweep. At a scattered-site company the pile grows faster than anyone sweeps it; every duplicate the PM notices before staff do is a support conversation.
What changes
Little code — but the Atlas merge route today only redirects the row; it does not re-mint claims or move threads, so it would have to call the real merge primitive first. And the "pile" needs a recurring instrument, which the one-off report was explicitly built not to be.
Safety
highest per merge A human reads both records. The risk moves: not a wrong merge, but an unmeasured, growing set of wrong "two people".
Cost
unbounded human time Small code, a standing staff chore that scales with customers.
Reversibility
Same primitive, same gap.
Seams
report-split-identity-pairs.ts · POST /api/admin/identity/merge (would need mergePersons).

D Never join across channels — today

The rule
Keep the records apart. Residents fold on the rent-roll sync; prospects never do.
What the PM sees
Two people whenever a prospect uses two channels — the story in the section above, at every scattered-site company, forever.
Safety
zero wrong merges and a guaranteed wrong "two people", which is the exact thing the dock exists to remove.
Cost
Zero. Reversibility: trivially. This is the baseline the other three are measured against.

The recommendation, and why

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.

For the decider

Question

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?

Options

  1. The PMS record decides. A guest card, application or tenant row carrying both values folds the two automatically, through the existing merge rule and guards, plus a new requirement that both records carry a real, structured name that agrees. Anything short of that stays split and is reported for staff.
  2. A, plus what the person says about themselves in their own thread. A texter who gives Clara their email, or a caller who states one, folds onto the email-first record under the same guards, instead of "merge candidate, nothing written".
  3. Staff-confirmed only. Never automatic. PropFlow staff (never the PM) review the pairs the split-pair report finds and merge in Atlas.
  4. Never join across channels (today). A person who emails and also texts stays two people until they become a resident.

Recommendation

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.

PropFlow Docs