mirror of
https://github.com/block/buzz.git
synced 2026-08-18 06:50:31 +02:00
fix(desktop): relay-scope the channel agent reuse finders
Complete the cross-community reuse fix on the *creation* path. The attach guards (commitb49523e7f) stopped a foreign-relay agent from being silently attached, but the reuse finders that decide "reuse existing vs create new" were still relay-unfiltered: a persona/generic agent pinned to community A's relay would surface as a reuse candidate while Desktop viewed community B, so creating a B agent could return the deaf A agent instead of a fresh B one — user-visible wrongness (Eva's scope call). Relay-scope all three reuse finders with the existing `agentBelongsToRelay` helper: 1. `findReusablePersonaAgent` / `findReusableGenericAgent` gain a REQUIRED `activeRelayUrl` and filter candidates to the active community's relay before preference selection. A foreign-relay candidate is excluded, so the caller falls through to creating a fresh agent on the active relay. 2. `findReusableAgent` (the routing wrapper) threads `activeRelayUrl` through to both finders. 3. Both consumers pass it: `provisionChannelManagedAgent`'s context (via `createChannelManagedAgents(..., activeRelayUrl)` and the provision mutation hook) and the `useReusableAgentDetection` UI guardrail, each sourcing the relay from `useActiveRelayUrl()`. The param is REQUIRED so the compiler finds every caller and no future call can silently bypass the scope. `agentBelongsToRelay` stays permissive for blank pins / blank active relay, so existing tests and legacy blank-pin records are unaffected. Four unit tests cover the new behavior: a B-pinned persona/generic candidate is excluded when the active relay is A (fresh agent created, no error), a same-relay candidate is still reused, and the wrapper routes the foreign-relay persona case to a fresh agent. 42/42 agentReuse tests pass; tsc --noEmit and biome check clean. Stacked on wren/agents-every-community-successor (commit #4 onb49523e7f). Co-authored-by: npub17jjz49l9jjmhhk7cac63j8yt9z555n9cw8vk7v5jz4vzw4ppld5qgj57cc <f4a42a97e594b77bdbd8ee35191c8b28a94a4cb871d96f32921558275421fb68@sprout-oss.stage.blox.sqprod.co> Signed-off-by: npub17jjz49l9jjmhhk7cac63j8yt9z555n9cw8vk7v5jz4vzw4ppld5qgj57cc <f4a42a97e594b77bdbd8ee35191c8b28a94a4cb871d96f32921558275421fb68@sprout-oss.stage.blox.sqprod.co>
This commit is contained in:
parent
b49523e7f7
commit
a332b5e534
@@ -308,6 +308,71 @@ test("findReusableGenericAgent: command matching uses normalization", () => {
|
||||
assert.equal(result, agent);
|
||||
});
|
||||
|
||||
test("findReusablePersonaAgent: excludes agent pinned to a different relay", () => {
|
||||
const agent = makeAgent({
|
||||
personaId: "persona-1",
|
||||
pubkey: PUB_A,
|
||||
relayUrl: "wss://relay-a.example",
|
||||
});
|
||||
const channelMembers = new Set([PUB_B]);
|
||||
const result = findReusablePersonaAgent(
|
||||
[agent],
|
||||
"persona-1",
|
||||
channelMembers,
|
||||
"wss://relay-b.example",
|
||||
);
|
||||
assert.equal(result, undefined);
|
||||
});
|
||||
|
||||
test("findReusablePersonaAgent: reuses agent pinned to the active relay", () => {
|
||||
const agent = makeAgent({
|
||||
personaId: "persona-1",
|
||||
pubkey: PUB_A,
|
||||
relayUrl: "wss://relay-b.example",
|
||||
});
|
||||
const channelMembers = new Set([PUB_B]);
|
||||
const result = findReusablePersonaAgent(
|
||||
[agent],
|
||||
"persona-1",
|
||||
channelMembers,
|
||||
"wss://relay-b.example",
|
||||
);
|
||||
assert.equal(result, agent);
|
||||
});
|
||||
|
||||
test("findReusableGenericAgent: excludes agent pinned to a different relay", () => {
|
||||
const agent = makeAgent({
|
||||
agentCommand: "goose",
|
||||
personaId: null,
|
||||
systemPrompt: null,
|
||||
relayUrl: "wss://relay-a.example",
|
||||
});
|
||||
const channelMembers = new Set([PUB_B]);
|
||||
const result = findReusableGenericAgent(
|
||||
[agent],
|
||||
"goose",
|
||||
channelMembers,
|
||||
"wss://relay-b.example",
|
||||
);
|
||||
assert.equal(result, undefined);
|
||||
});
|
||||
|
||||
test("findReusableAgent: foreign-relay persona candidate yields fresh agent (no reuse)", () => {
|
||||
const agent = makeAgent({
|
||||
personaId: "p1",
|
||||
pubkey: PUB_A,
|
||||
relayUrl: "wss://relay-a.example",
|
||||
});
|
||||
const channelMembers = new Set([PUB_B]);
|
||||
const result = findReusableAgent(
|
||||
[agent],
|
||||
channelMembers,
|
||||
{ personaId: "p1", command: "goose" },
|
||||
"wss://relay-b.example",
|
||||
);
|
||||
assert.equal(result, undefined);
|
||||
});
|
||||
|
||||
test("findReusableAgent: routes to persona search when personaId provided", () => {
|
||||
const agent = makeAgent({ personaId: "p1", pubkey: PUB_A });
|
||||
const channelMembers = new Set([PUB_B]);
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { agentBelongsToRelay } from "@/features/agents/agentRelayScope";
|
||||
import type { ManagedAgent } from "@/shared/api/types";
|
||||
|
||||
/** Inline normalization — avoids runtime dependency on @/shared/lib/pubkey. */
|
||||
@@ -50,11 +51,13 @@ export function findReusablePersonaAgent(
|
||||
agents: ManagedAgent[],
|
||||
personaId: string,
|
||||
channelMemberPubkeys: ReadonlySet<string>,
|
||||
activeRelayUrl: string | null | undefined,
|
||||
): ManagedAgent | undefined {
|
||||
const candidates = agents.filter(
|
||||
(agent) =>
|
||||
agent.personaId === personaId &&
|
||||
!channelMemberPubkeys.has(normalizePubkey(agent.pubkey)),
|
||||
!channelMemberPubkeys.has(normalizePubkey(agent.pubkey)) &&
|
||||
agentBelongsToRelay(agent.relayUrl, activeRelayUrl),
|
||||
);
|
||||
return pickPreferredManagedAgent(candidates);
|
||||
}
|
||||
@@ -63,13 +66,15 @@ export function findReusableGenericAgent(
|
||||
agents: ManagedAgent[],
|
||||
command: string,
|
||||
channelMemberPubkeys: ReadonlySet<string>,
|
||||
activeRelayUrl: string | null | undefined,
|
||||
): ManagedAgent | undefined {
|
||||
const candidates = agents.filter(
|
||||
(agent) =>
|
||||
!agent.personaId &&
|
||||
!agent.systemPrompt?.trim() &&
|
||||
commandsMatch(agent.agentCommand, command) &&
|
||||
!channelMemberPubkeys.has(normalizePubkey(agent.pubkey)),
|
||||
!channelMemberPubkeys.has(normalizePubkey(agent.pubkey)) &&
|
||||
agentBelongsToRelay(agent.relayUrl, activeRelayUrl),
|
||||
);
|
||||
return pickPreferredManagedAgent(candidates);
|
||||
}
|
||||
@@ -86,12 +91,14 @@ export function findReusableAgent(
|
||||
systemPrompt?: string;
|
||||
command: string;
|
||||
},
|
||||
activeRelayUrl: string | null | undefined,
|
||||
): ManagedAgent | undefined {
|
||||
if (input.personaId) {
|
||||
return findReusablePersonaAgent(
|
||||
agents,
|
||||
input.personaId,
|
||||
channelMemberPubkeys,
|
||||
activeRelayUrl,
|
||||
);
|
||||
}
|
||||
if (!input.systemPrompt?.trim()) {
|
||||
@@ -99,6 +106,7 @@ export function findReusableAgent(
|
||||
agents,
|
||||
input.command,
|
||||
channelMemberPubkeys,
|
||||
activeRelayUrl,
|
||||
);
|
||||
}
|
||||
return undefined;
|
||||
|
||||
@@ -299,6 +299,7 @@ export async function provisionChannelManagedAgent(
|
||||
context?: {
|
||||
managedAgents?: ManagedAgent[];
|
||||
channelMemberPubkeys?: ReadonlySet<string>;
|
||||
activeRelayUrl?: string | null;
|
||||
},
|
||||
): Promise<ProvisionChannelManagedAgentResult> {
|
||||
const trimmedName = input.name.trim();
|
||||
@@ -319,6 +320,7 @@ export async function provisionChannelManagedAgent(
|
||||
context.managedAgents,
|
||||
input.personaId,
|
||||
context.channelMemberPubkeys,
|
||||
context.activeRelayUrl,
|
||||
);
|
||||
if (reusable) {
|
||||
// Apply the caller's respondTo settings so the user's permission
|
||||
@@ -359,6 +361,7 @@ export async function provisionChannelManagedAgent(
|
||||
context.managedAgents,
|
||||
input.runtime.command,
|
||||
context.channelMemberPubkeys,
|
||||
context.activeRelayUrl,
|
||||
);
|
||||
if (reusable) {
|
||||
const needsRespondToUpdate =
|
||||
@@ -464,7 +467,7 @@ export async function createChannelManagedAgents(
|
||||
const channelMemberPubkeys = new Set(
|
||||
members.map((m) => normalizePubkey(m.pubkey)),
|
||||
);
|
||||
const context = { managedAgents, channelMemberPubkeys };
|
||||
const context = { managedAgents, channelMemberPubkeys, activeRelayUrl };
|
||||
|
||||
// Sequential loop: each agent must be fully created and its relay membership
|
||||
// written before the next starts. Concurrent writes to the replaceable
|
||||
|
||||
@@ -711,6 +711,7 @@ export function useProvisionChannelManagedAgentMutation(
|
||||
channelId: string | null,
|
||||
) {
|
||||
const queryClient = useQueryClient();
|
||||
const activeRelayUrl = useActiveRelayUrl();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async (
|
||||
@@ -731,6 +732,7 @@ export function useProvisionChannelManagedAgentMutation(
|
||||
channelMemberPubkeys: new Set(
|
||||
members.map((member) => normalizePubkey(member.pubkey)),
|
||||
),
|
||||
activeRelayUrl,
|
||||
});
|
||||
},
|
||||
onSuccess: (result) => {
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
useManagedAgentsQuery,
|
||||
} from "@/features/agents/hooks";
|
||||
import { useChannelMembersQuery } from "@/features/channels/hooks";
|
||||
import { useActiveRelayUrl } from "@/features/communities/useCommunities";
|
||||
import { normalizePubkey } from "@/shared/lib/pubkey";
|
||||
import type { AcpRuntime, ManagedAgent } from "@/shared/api/types";
|
||||
|
||||
@@ -25,6 +26,7 @@ export function useReusableAgentDetection(
|
||||
): ManagedAgent | undefined {
|
||||
const managedAgentsQuery = useManagedAgentsQuery();
|
||||
const channelMembersQuery = useChannelMembersQuery(channelId, enabled);
|
||||
const activeRelayUrl = useActiveRelayUrl();
|
||||
|
||||
return React.useMemo(() => {
|
||||
const agents = managedAgentsQuery.data;
|
||||
@@ -36,10 +38,15 @@ export function useReusableAgentDetection(
|
||||
|
||||
// For persona selection: check the first selected persona
|
||||
if (selectedPersonas.length === 1 && !includeGeneric) {
|
||||
return findReusableAgent(agents, memberPubkeys, {
|
||||
personaId: selectedPersonas[0].id,
|
||||
command: selectedRuntime.command,
|
||||
});
|
||||
return findReusableAgent(
|
||||
agents,
|
||||
memberPubkeys,
|
||||
{
|
||||
personaId: selectedPersonas[0].id,
|
||||
command: selectedRuntime.command,
|
||||
},
|
||||
activeRelayUrl,
|
||||
);
|
||||
}
|
||||
|
||||
// For generic agent with no custom prompt
|
||||
@@ -48,10 +55,15 @@ export function useReusableAgentDetection(
|
||||
selectedPersonas.length === 0 &&
|
||||
!customPrompt.trim()
|
||||
) {
|
||||
return findReusableAgent(agents, memberPubkeys, {
|
||||
command: selectedRuntime.command,
|
||||
systemPrompt: customPrompt,
|
||||
});
|
||||
return findReusableAgent(
|
||||
agents,
|
||||
memberPubkeys,
|
||||
{
|
||||
command: selectedRuntime.command,
|
||||
systemPrompt: customPrompt,
|
||||
},
|
||||
activeRelayUrl,
|
||||
);
|
||||
}
|
||||
|
||||
return undefined;
|
||||
@@ -62,5 +74,6 @@ export function useReusableAgentDetection(
|
||||
selectedPersonas,
|
||||
includeGeneric,
|
||||
customPrompt,
|
||||
activeRelayUrl,
|
||||
]);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user