A huddle is a conversation between the agents of people who all mutually trust each other. Before it can happen, somebody has to work out who those groups are. The obvious answer is a server, because the maximal cliques of a graph (the largest groups in which everyone is connected to everyone) seem to need the whole graph. This article is the argument that they do not, and the four pieces that let every phone find its own cohorts from facts it is already entitled to know.
This is a mechanism deep-dive under The Agent in Your Day, which introduces the cohort and the huddle it holds. Here we go underneath one sentence of it: how the set of people is decided, by whom, and what anyone else learns along the way.
Two words, because both recur. A cohort is the group: the set of people. A huddle is a session that cohort holds. Cohorts are durable; huddles happen and end.
The membership rule is strict on purpose. You mark someone inner-circle; if they mark you back, an edge exists: a mutual link between the two of you. Picture a dot for every person and a line for every mutual pair: that picture is the mutual-consent graph. A cohort is a set of people who are all pairwise mutual (every two members share an edge) and as large as it can get without breaking that rule. In graph terms, that is a maximal clique. The rule is decisive because a huddle shares sensitive inferred context: things the agents have worked out about their users. It guarantees nothing about you is ever spoken in front of an agent whose user you do not mutually trust.
Now the difficulty. Alice can see her own edges: she knows who she has marked and who has
marked her back. She cannot see whether Bob and Cindy have marked each other. Yet that one
fact decides whether {Alice, Bob, Cindy} is a cohort or merely two separate pairs. So it
seems a device cannot compute its own cliques without being told about edges between other
people, and telling it those edges means somebody, somewhere, holds the social graph.
That reasoning is correct about edges in general. It is wrong about the edges that matter, and the gap between those two statements is the whole mechanism.
A clique containing Alice lives entirely inside {Alice} ∪ N(Alice): Alice plus her own neighbours. (N(Alice) is standard notation for Alice's neighbourhood, the people she has mutually marked; the ∪ just merges the two sets.) The clique cannot contain anyone she has not already mutually marked, because then it would not be a clique containing Alice. So the only third-party fact she needs is which of the people she already trusts trust each other. Not the graph. Just an intersection (an overlap) inside a set she is already a member of.
Alice needs N(Alice) ∩ N(Bob): the intersection, meaning the people who appear in both her
circle and Bob's. She must not learn the rest of Bob's circle, and Bob must not learn hers.
Cryptographers call this problem private set intersection: two parties compute the overlap of their lists while revealing nothing else about them. There is a shortcut available here that a general private-set-intersection protocol does not get to assume: every mutual pair already shares a channel key. That key exists because they message each other: it is the key that encrypts their conversation. It gives the two of them a secret nobody else has, and a shared secret is all a keyed pseudorandom function needs. (That is a function which scrambles an input into output that looks like random noise to anyone without the key, while anyone holding the key gets the same output for the same input every time.)
So Bob sends Alice his mutual set, blinded (disguised entry by entry) under the key only he and Alice hold. Each person in his set becomes a short tag, computed with HMAC, a standard keyed hash built for exactly this job:
tag = HMAC(k_AB, domain ‖ BE64(epoch) ‖ member_hash)[..16]
Reading the formula: k_AB is the pair key only Alice and Bob hold, domain is a fixed
label that keeps these tags from colliding with any other use of the same key,
BE64(epoch) is the current time period written as a fixed-width number,
member_hash identifies one person, ‖ joins the pieces, and [..16] keeps the first
sixteen bytes of the result. Alice computes the same function over her own mutuals.
Anything that matches is in both sets. Anything that does not match is sixteen opaque bytes:
a person she cannot name, cannot count toward anything, and cannot recognise if she meets the
same tag under a different pair, because a different pair means a different key and therefore
an unrelated tag.
Three properties fall out of that construction, each doing a job:
The exchange is one-directional by construction. Alice learns her side; Bob runs the same thing in reverse to learn his. Neither ever holds the other's raw set.
With the neighbourhood assembled, finding the maximal cliques is a job for the textbook
algorithm: Bron–Kerbosch, the classic recursive procedure for listing every maximal clique in
a graph, run with pivoting (a standard trick that prunes redundant branches) and restricted
to {v} ∪ N(v), you plus your neighbours. It is the same algorithm and the same output as
the server-side version it replaces. What changes when it moves onto a handset is that the
worst case suddenly belongs to someone who is holding the device.
Bron–Kerbosch is exponential in the worst case: a classic result of Moon and Moser says a
neighbourhood of degree d (one with d members) can hold up to 3^(d/3) maximal
cliques, a count that roughly triples every time d grows by three. Two caps bound the
work:
The result is every maximal clique containing you. Each clique is sorted, and the list is ordered largest-first with ties broken lexicographically, so two devices that computed the same graph also pick the same cohort out of it.
The identifier is derived, not allocated. Every member hashes the same ingredients (H is
a cryptographic hash, a one-way fingerprint function) and keeps the first sixteen bytes:
cohort_id = H(domain ‖ sorted member set)[..16]
Every member computes it independently and gets the same sixteen bytes. That is what lets it double as the group id for MLS (Messaging Layer Security, the standard protocol for end-to-end-encrypted group messaging) with nobody adjudicating. The convener (the member who establishes the MLS group and seeds the session nonce, a fresh random value used exactly once) is simply the member whose hash sorts first. No election, because no election is needed for a value everyone can compute.
The derivation is byte-identical to the one the server used, deliberately: same domain separator (the fixed label mixed into the hash so it can never collide with a hash computed for some other purpose), same sort, same truncation. A cohort that already had a persisted MLS group keeps its id across the migration instead of silently forking into a second group.
Devices compute cliques from their own edge views, and views diverge. Cindy revokes Bob; for a while only Cindy knows. Her clique set and Alice's no longer match, and both are sincerely reporting what they see.
The settling rule is one sentence: an edge is real if and only if both of its endpoints affirm it. Only X and Y are authoritative about the X–Y edge. A member reporting on someone else's edge is ignored outright, so no group of members can drag a pair into a cohort that pair does not consent to. Each member reports only the edges touching itself that its own view currently supports; anything else it believes is hearsay.
Running that rule over the collected reports is deterministic (the same reports in always
produce the same answer out), so every participant reaches the identical result. That is what
lets cohort_id agree with nobody adjudicating.
An earlier design worked from objections instead: it pooled every member's objections and
subtracted the lot. It is worth recording why that was wrong, because it was consistent and
still unfair. When Cindy had revoked Bob and only Cindy knew, subtracting objections dropped
Bob (the member with the stale view) and kept Cindy, even though {Alice, Bob} and
{Alice, Cindy} were equally valid outcomes. It silently cost Bob a huddle, and which of
the two survived depended on who happened to object first. Affirmation is symmetric where
objection is not.
Nothing about who knows whom.
The service that held the mutual-consent edges is gone. Consent now travels as a sealed message between the two people it concerns. Blinded mutual sets ride the channel those two already share. Cliques are computed on each handset, and the cohort id and convener are derived rather than assigned. The relay (the server that forwards traffic between devices) carries sealed envelopes it cannot read, addressed by values that mean nothing to it: the same position it holds for everything else that rides it.
It is worth being precise about the direction of that change, because the intuition runs the other way. Moving a computation onto the device did not merely relocate the disclosure. Alice learns which of her own mutuals are mutual with each other; the server previously saw every edge among everyone. The decentralised version discloses strictly less, to strictly fewer parties, than the arrangement it replaced.
The code is ios/Pixie/Pixie/Cognitive/Huddle/HuddleGraph.swift: HuddlePeerTags for the
blinded exchange, HuddleCliqueGraph for Bron–Kerbosch and its caps, HuddleCohort for the
derived id and convener, and HuddleCohortAgreement for settling divergent views. What the
cohort then does (the rounds, the floor, the turns) is
The Agent in Your Day; how a device is admitted to the
resulting MLS group is
Who Gets to Put You in a Group.