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.
| Provider | Scope | Identifier |
|---|---|---|
slack | workspace or enterprise id | user id (U…), unique within a workspace |
telegram | — | numeric user id; mutable usernames are not stored |
whatsapp | business phone number id (phone_number_id) | sender WhatsApp id (wa_id), in international format without a leading + |
email | — | address |
api-key | — | key 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:
GET /api/personas/resolve # external identity -> persona
GET /api/personas/{id} # the persona record
GET /api/personas/{id}/conversations # every conversation it touchedThe 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.
| Record | Keyed by | Contents | Lifecycle |
|---|---|---|---|
| Identity record | identity tuple | persona id, link method, verified_at, linked_by, first/last seen | Created at first contact; reassigned to the surviving persona atomically during a merge |
| Profile | persona id | display 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 provenance | One row per participation relationship; reassigned on merge with original attribution provenance retained |
| Participation index | persona id | conversation ids associated with the persona | Updated when a new participation relationship is recorded |
| Link nonce | digest of the code | issuing persona, expiration | Created by StartLink; consumed atomically with the link to prevent reuse |
| Link attempts | completing identity | failure count, window start | Incremented 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.
| Ceremony | Trust basis |
|---|---|
| Workspace-asserted email auto-link | The workspace admin asserts the email |
| Deep-link token (chat platforms) | Possession of both sessions |
| Cross-channel one-time code | Possession of both channels; the receiving edge must support code completion |
| API-key issuance | The issued key links to the requesting persona |
| Email magic link | Mailbox 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.