Personas

One person can use different accounts across chat, email, and the public API. A persona connects verified accounts to a durable identity for turn attribution, permissions, and memory.

An edge connects a client to the public API and identifies its caller. A persona is the stable internal identity, or principal, to which those external identities link. Polychrome uses it to attribute turns, resolve permissions and quotas, and select memory across conversations.

The principal model

Each persona receives a UUIDv7 PersonaId at creation. External identifiers do not determine this id. Linking preserves existing ids, including aliases for absorbed provisional personas. External identities use typed (provider, scope, id) tuples because some platform ids are unique only within a scope.

ProviderScopeIdentifier
slackworkspace or enterprise iduser id (U…), unique within a workspace
telegramnumeric user id; mutable usernames are not stored
whatsappbusiness phone number id (phone_number_id)sender WhatsApp id (wa_id), in international format without a leading +
emailaddress
api-keykey id, from the public API's bearer auth

First contact creates a provisional persona, allowing attribution and memory before account linking. It starts with the minimum permission set. A verified link records trust in an external identity; permissions belong to the persona.

Attribution on every turn

The EdgeAdapter trait maps inbound events to caller identities through caller(), alongside its conversation_id mapping. The shared SDK deduplicates participants consistently. The edge signs the AssertedAttribution envelope attached to each turn, providing evidence of the asserted caller identity.

The control plane resolves identities to durable personas before execution, creating provisional personas on first contact. It records caller and participant events in the turn's atomic batch. These identify who initiated the turn and who participated. Repeated attribution can upgrade a participant to an initiator without duplicate changes. Channel approvals record the approver's identity.

The forensics API exposes persona records and participation:

Shell
GET /api/personas/resolve              # external identity -> persona
GET /api/personas/{id}                 # the persona record
GET /api/personas/{id}/conversations   # every conversation it touched

The identity ledger

The identity directory uses an authenticated Commonware qmdb store. Domain-separated SHA-256 keys identify identity and profile rows. Related mutations commit atomically under one root transition. The store retains operation history in a Merkle structure to support integrity verification and verified replica synchronization.

The state plane is the production authority for identity, profile, participation, visibility, and administration operations. The control plane calls that authority rather than writing its own identity directory. A local reference host supports focused tests. Recovery replays committed records from durable storage.

Data model

Six record families share the directory store. Keys use domain-separated SHA-256 hashes over length-framed parts, preventing ambiguous identifiers and collisions between record families. Related mutations commit atomically, including identity reassignment with a profile tombstone and linking with nonce consumption.

RecordKeyed byContentsLifecycle
Identity recordidentity tuplepersona id, link method, verified_at, linked_by, first/last seenCreated at first contact; reassigned to the surviving persona atomically during a merge
Profilepersona iddisplay name, linked identities, status, merge audit (merged_into, actor, evidence, timestamp)Created as provisional; becomes a permanent alias to the surviving persona after a merge
Participation(persona, conversation)role, first-tie time, via_persona_id provenanceOne row per participation relationship; reassigned on merge with original attribution provenance retained
Participation indexpersona idconversation ids associated with the personaUpdated when a new participation relationship is recorded
Link noncedigest of the codeissuing persona, expirationCreated by StartLink; consumed atomically with the link to prevent reuse
Link attemptscompleting identityfailure count, window startIncremented after failed CompleteLink attempts; enforces lockout; cleared atomically after successful linking

A persona progresses from provisional to linked after verification, and to merged if another persona absorbs it. A merged id remains an alias. Conversation logs record caller on each turn and participant on first participation. The directory resolves current identity; the log records historical attribution. Alias traversal connects historical ids to the current persona.

Linking and trust

Linking requires verified evidence. Matching display names or shared channels cannot authorize it. Completing a link can absorb a provisional persona, preserving its id as an alias. It cannot absorb an established persona. To correct a mistaken provisional-persona link, unlink the identity and request a fresh invite.

PersonaService runs on the control plane's internal listener. StartLink creates a single-use six-digit code with a ten-minute lifetime. Storage retains only its domain-separated digest. CompleteLink consumes the code from the account being linked. Code consumption and linking commit atomically, preventing reuse after a restart. If the completing account has a provisional persona, the operation absorbs it into the persona that created the code. If it has an established persona, linking refuses without consuming the code. Start linking from the established account instead. Two established personas cannot be merged.

Failed guesses count against the completing identity. Exceeding the attempt budget blocks further linking during the lockout window, including attempts with a correct code. The former admin Merge RPC is no longer available. Historical persona ids continue to resolve through aliases.

The Slack edge handles link to create a code and link <code> to complete linking. In a channel, prefix the command with a mention; in a direct message, enter it directly. help lists commands. The command router parses linking requests before model execution and uses the signed inbound event's caller identity. Codes are credentials and appear only in direct messages. If a direct message cannot be opened, the request fails. Channels show only non-secret outcomes such as "invalid or expired" or "linked".

The Slack app's home view lists your persona and connected accounts. Its "Connect another account" button starts the same linking process. The view refreshes when opened. The code arrives only in a direct message, never in the home view.

To link WhatsApp, send the six-digit code from your established account to the business chat, or send the prefilled token from a “Connect WhatsApp” link. The edge consumes the credential before model execution and uses the sender's business-scoped WhatsApp identity. It does not provide a command to start linking or accept admin invites. Messaging a different business number creates a separate external identity. See WhatsApp support and limits.

CeremonyTrust basis
Workspace-asserted email auto-linkThe workspace admin asserts the email
Deep-link token (chat platforms)Possession of both sessions
Cross-channel one-time codePossession of both channels; the receiving edge must support code completion
API-key issuanceThe issued key links to the requesting persona
Email magic linkMailbox possession and sender verification

Memory

Turn assembly selects a persona's notes from their memory journal according to scope and the current audience. Post-turn extraction runs separately from reply generation. See Memory for scope rules, extraction, merging, and controls to inspect or erase notes.