Why the machine proposes a merge and a person disposes
A Person is the canonical human, an Identity is one address on one channel, and a Person may hold many. That split is what lets a WhatsApp message, an email thread and an inbound call land on the same record without any of them being the record. Merging two Persons stays a human decision, because it is the least reversible operation a relationship store has.
Three records rather than one#
- Person
- The human. Created once, kept whatever changes around them.
- Identity
- One address on one channel, keyed by
value_key(kind, value)and attached withpeople.add_identity. An email address, a phone number, a messaging handle. - Company
- The organisation. Several people attach to one, and a person can move between them.
The temptation is to skip the middle record and put the email address on the person. It works until the second channel arrives, at which point either the phone number becomes a second person or one channel becomes the *real* one and the others become attributes of it. Both outcomes make the history unreadable, which is the only thing a relationship record is for.
What an inbound message actually does#
The shape is the same everywhere, which is what makes cross-channel continuity possible at all: inbound event, workspace resolution, person resolution, canonical conversation, then a reply governed by the same autonomy rule as any other channel.
- Email arrives into provider-shaped
tenant_*tables and is bridged into canonicalthreads,messagesandcontacts. The engine never reads the provider tables, so adding a provider changes nothing downstream and a bug in one adapter cannot corrupt a workspace's history. - Phone resolves the caller by number. A contact created by lead capture carries
phone_keyfrom the start, and a number matching two rows resolves to the oldest so the same call resolves the same way twice. - WhatsApp resolves the handle to a person and continues the same conversation, under provider session-window rules that Connect exposes rather than simulates.
What follows the person, not the conversation#
Once identity is a first-class record, several things stop being per-channel settings and become properties of a relationship.
| Thing | Where it lives | Consequence |
|---|---|---|
| A block | A connect_memory row tagged block:<channel> in tags | It holds across channels and across future conversations, because directives() reads only the tag list |
| Memory | Four tiers: workspace → channel → endpoint → contact | The narrowest applicable tier wins, the same hierarchy autonomy uses |
| Autonomy | Scopes: contact, endpoint, channel, workspace | One customer can need approval while everything else runs |
| History | Customer 360 and the timeline | One person's whole history, every channel, in one view |
| Follow-ups | Any channel, plus any and task | A commitment belongs to the person, not to the thread it was made on |
Merging is proposed by the machine and decided by a person#
duplicates proposes candidates. merge_people executes one, and it is deliberately a human decision. When it runs it preserves identities, stages, follow-ups, deals, demos, cases and onboarding from both sides — a merge that quietly drops the losing record's open case is worse than no merge.
The reason for the human is reversibility. Almost everything else in a relationship store can be undone by editing a field. A merge combines two histories, and separating them afterwards means deciding which follow-up and which deal belonged to which side — a judgement nobody can make from the merged record.
Questions#
Why keep a separate Identity record instead of columns for email and phone?
Because people have more than one of each, and the count changes. Columns force a primary, and a primary forces every later channel to be secondary — which is exactly backwards when the customer's next message arrives on the channel you called secondary.
If two people share a phone number, whose call is it?
Deterministically the older record, so the resolution is stable and reviewable, and then a duplicate proposal if the two look like one person. A stable wrong answer can be corrected; an unstable one cannot even be described.
Does a block on WhatsApp stop email as well?
It depends on the tag. The directive is block:<channel>, so blocking a channel blocks that channel — but because the row lives against the person rather than the conversation, it survives the thread ending and applies to future conversations too.