From 0851322d9f83e64f27ee60e2fd14eca2834c68fd Mon Sep 17 00:00:00 2001 From: Taylor Ho Date: Sun, 14 Jun 2026 23:51:07 -0700 Subject: [PATCH] fix(agents): classify agent members for activity ingress - Update channel activity candidate resolution to treat ChannelMember.isAgent the same as bot role membership. - Keep agent typing classification and Activity panel scoping aligned so agent typing does not fall through to the generic human typing row. - Add regression coverage for isAgent channel members being created as activity candidates and retained in channel scope. --- .../lib/agentSessionCandidates.test.mjs | 87 +++++++++++++++++++ .../channels/lib/agentSessionCandidates.ts | 8 +- 2 files changed, 93 insertions(+), 2 deletions(-) create mode 100644 desktop/src/features/channels/lib/agentSessionCandidates.test.mjs diff --git a/desktop/src/features/channels/lib/agentSessionCandidates.test.mjs b/desktop/src/features/channels/lib/agentSessionCandidates.test.mjs new file mode 100644 index 000000000..d02725117 --- /dev/null +++ b/desktop/src/features/channels/lib/agentSessionCandidates.test.mjs @@ -0,0 +1,87 @@ +import assert from "node:assert/strict"; +import test from "node:test"; + +import { + buildChannelAgentSessionCandidates, + getChannelAgentSessionAgents, +} from "./agentSessionCandidates.ts"; + +const CHANNEL = { + id: "channel-1", + name: "general", + channelType: "stream", + visibility: "private", + description: "", + topic: null, + purpose: null, + memberCount: 0, + memberPubkeys: [], + lastMessageAt: null, + archivedAt: null, + participants: [], + participantPubkeys: [], + isMember: true, + ttlSeconds: null, + ttlDeadline: null, +}; + +function member(overrides) { + return { + pubkey: "aa".repeat(32), + role: "member", + isAgent: false, + joinedAt: "2024-01-01T00:00:00Z", + displayName: "Agent", + ...overrides, + }; +} + +test("buildChannelAgentSessionCandidates includes members marked isAgent", () => { + const candidates = buildChannelAgentSessionCandidates({ + channelMembers: [ + member({ + pubkey: "11".repeat(32), + role: "member", + isAgent: true, + displayName: "Ned", + }), + ], + managedAgents: [], + relayAgents: [], + }); + + assert.deepEqual( + candidates.map((agent) => ({ + name: agent.name, + pubkey: agent.pubkey, + source: agent.agentSource, + })), + [{ name: "Ned", pubkey: "11".repeat(32), source: "member-bot" }], + ); +}); + +test("getChannelAgentSessionAgents keeps isAgent member candidates in channel scope", () => { + const channelMembers = [ + member({ + pubkey: "22".repeat(32), + role: "member", + isAgent: true, + displayName: "Ned", + }), + ]; + const candidates = buildChannelAgentSessionCandidates({ + channelMembers, + managedAgents: [], + relayAgents: [], + }); + + const scoped = getChannelAgentSessionAgents({ + activeChannel: CHANNEL, + activeChannelId: CHANNEL.id, + agents: candidates, + channelMembers, + }); + + assert.equal(scoped.length, 1); + assert.equal(scoped[0].pubkey, "22".repeat(32)); +}); diff --git a/desktop/src/features/channels/lib/agentSessionCandidates.ts b/desktop/src/features/channels/lib/agentSessionCandidates.ts index 9606eb706..a77db42da 100644 --- a/desktop/src/features/channels/lib/agentSessionCandidates.ts +++ b/desktop/src/features/channels/lib/agentSessionCandidates.ts @@ -22,6 +22,10 @@ function relayStatusToManagedStatus( return status === "offline" ? "stopped" : "deployed"; } +function isAgentChannelMember(member: ChannelMember) { + return member.role === "bot" || member.isAgent; +} + export function buildChannelAgentSessionCandidates({ channelMembers, managedAgents, @@ -61,7 +65,7 @@ export function buildChannelAgentSessionCandidates({ for (const member of channelMembers ?? []) { const key = normalizePubkey(member.pubkey); - if (member.role !== "bot" || byPubkey.has(key)) { + if (!isAgentChannelMember(member) || byPubkey.has(key)) { continue; } @@ -98,7 +102,7 @@ export function getChannelAgentSessionAgents({ const botMemberPubkeys = channelMembers ? new Set( channelMembers - .filter((member) => member.role === "bot") + .filter(isAgentChannelMember) .map((member) => normalizePubkey(member.pubkey)), ) : null;