diff --git a/desktop/src/features/agents/lib/personaAdoption.test.mjs b/desktop/src/features/agents/lib/personaAdoption.test.mjs
new file mode 100644
index 0000000000..de0ad076f8
--- /dev/null
+++ b/desktop/src/features/agents/lib/personaAdoption.test.mjs
@@ -0,0 +1,82 @@
+import assert from "node:assert/strict";
+import test from "node:test";
+
+import {
+ countCatalogRepublishers,
+ personaDefinitionFingerprint,
+} from "./personaAdoption.ts";
+
+const WES = "a".repeat(64);
+const ALICE = "b".repeat(64);
+const BOB = "c".repeat(64);
+
+function publication(ownerPubkey, agent = {}) {
+ return {
+ eventId: "e".repeat(64),
+ ownerPubkey,
+ sourcePersonaId: "persona-1",
+ createdAt: 1_700_000_000,
+ agent: {
+ displayName: "Brain",
+ avatarUrl: null,
+ systemPrompt: "You review pull requests carefully.",
+ runtime: null,
+ model: null,
+ provider: null,
+ namePool: [],
+ respondTo: null,
+ parallelism: null,
+ ...agent,
+ },
+ };
+}
+
+test("fingerprint keys on the prompt, ignoring whitespace and case", () => {
+ const canonical = personaDefinitionFingerprint({
+ displayName: "Brain",
+ systemPrompt: "You review pull requests carefully.",
+ });
+ assert.equal(
+ personaDefinitionFingerprint({
+ displayName: "Renamed Brain",
+ systemPrompt: " You review pull\nrequests CAREFULLY. ",
+ }),
+ canonical,
+ );
+ // Promptless personas fall back to the display name.
+ assert.equal(
+ personaDefinitionFingerprint({ displayName: " Brain ", systemPrompt: "" }),
+ personaDefinitionFingerprint({ displayName: "brain", systemPrompt: " " }),
+ );
+});
+
+test("counts distinct other members with a matching publication", () => {
+ const count = countCatalogRepublishers(
+ [
+ publication(WES), // the original — never counts
+ publication(ALICE),
+ publication(ALICE, { displayName: "Brain (alice)" }), // same member twice
+ publication(BOB, { displayName: "Big Brain" }), // renamed copy, same prompt
+ publication(BOB.replace(/c/gu, "d"), {
+ systemPrompt: "A different persona entirely.",
+ }),
+ ],
+ {
+ displayName: "Brain",
+ systemPrompt: "You review pull requests carefully.",
+ publisherPubkey: WES,
+ },
+ );
+ assert.equal(count, 2);
+});
+
+test("no publisher and no matches yield zero", () => {
+ assert.equal(
+ countCatalogRepublishers([], {
+ displayName: "Brain",
+ systemPrompt: "prompt",
+ publisherPubkey: null,
+ }),
+ 0,
+ );
+});
diff --git a/desktop/src/features/agents/lib/personaAdoption.ts b/desktop/src/features/agents/lib/personaAdoption.ts
new file mode 100644
index 0000000000..78e9543b8b
--- /dev/null
+++ b/desktop/src/features/agents/lib/personaAdoption.ts
@@ -0,0 +1,61 @@
+/**
+ * Adoption signals for a catalog persona, derived from what the relay can
+ * actually show. Adding an entry to your library is a local-only write, so
+ * true duplication counts don't exist on the relay — but a member who adds a
+ * persona and then re-shares their copy publishes a second catalog head with
+ * the same definition. Counting those re-publications gives an honest lower
+ * bound on how many times the persona has been duplicated.
+ */
+
+import type { PersonaCatalogPublication } from "@/features/agents/lib/personaCatalogRelay";
+
+/**
+ * The identity of a persona definition for adoption matching: its operating
+ * prompt when it has one (whitespace-insensitive), otherwise its display name.
+ * Copies keep the source prompt verbatim, so this is the strongest signal a
+ * publication descends from the same definition.
+ */
+export function personaDefinitionFingerprint(definition: {
+ displayName: string;
+ systemPrompt: string;
+}): string {
+ const prompt = definition.systemPrompt.replace(/\s+/gu, " ").trim();
+ if (prompt.length > 0) {
+ return `prompt:${prompt.toLowerCase()}`;
+ }
+ return `name:${definition.displayName.trim().toLowerCase()}`;
+}
+
+/**
+ * Distinct members other than the publisher who have re-published this
+ * persona's definition to the community catalog. A lower bound on adoption:
+ * members who added the persona but never re-shared their copy stay
+ * invisible.
+ */
+export function countCatalogRepublishers(
+ publications: readonly PersonaCatalogPublication[],
+ entry: {
+ displayName: string;
+ systemPrompt: string;
+ publisherPubkey: string | null;
+ },
+): number {
+ const fingerprint = personaDefinitionFingerprint(entry);
+ const publisher = entry.publisherPubkey?.toLowerCase() ?? null;
+ const republishers = new Set
+