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.
This commit is contained in:
Taylor Ho
2026-06-15 15:35:51 -07:00
parent c8d85ef3d8
commit 0851322d9f
2 changed files with 93 additions and 2 deletions
@@ -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));
});
@@ -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;