Connect by JBRH Open Connect

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.

Status
Available What this means
Audience
both
In the app
#/relationships, #/timeline
Last verified
Product version
6.3.2

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 with people.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 canonical threads, messages and contacts. 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_key from 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.

ThingWhere it livesConsequence
A blockA connect_memory row tagged block:<channel> in tagsIt holds across channels and across future conversations, because directives() reads only the tag list
MemoryFour tiers: workspace → channel → endpoint → contactThe narrowest applicable tier wins, the same hierarchy autonomy uses
AutonomyScopes: contact, endpoint, channel, workspaceOne customer can need approval while everything else runs
HistoryCustomer 360 and the timelineOne person's whole history, every channel, in one view
Follow-upsAny channel, plus any and taskA 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.

Where a shared identity breaks the model#

  • A general enquiries address or a shop counter phone belongs to an organisation, not a person. Treating it as a person's identity merges strangers into one record.
  • A person who changes employer keeps their mobile and loses their work address, which now reaches somebody else. The identity is stale rather than wrong, and nothing detects that automatically.
  • Names are the other leak. identity.PROTECTED_MARKERS with name_rejected, clean_name and scrub_names guard every door a name passes through — summariser output, lead capture, person upsert, memory — and every door it is read back through. A form of address is never a name, and fact_rejected refuses a name-keyed fact that is a suffix, a pronoun or *the caller*.
  • Resolution accuracy is not measured here in either direction: UNKNOWN. What is countable is how many contacts carry a key per channel, which is the first thing to look at when continuity is failing.

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.