diff --git a/desktop/src/features/channels/cleanupChannelAgents.ts b/desktop/src/features/channels/cleanupChannelAgents.ts new file mode 100644 index 0000000..9fce5ba --- /dev/null +++ b/desktop/src/features/channels/cleanupChannelAgents.ts @@ -0,0 +1,51 @@ +/** + * Best-effort cleanup of managed agents when a channel is deleted. + * + * Each agent added via the "Add agents" dialog is a unique process scoped to + * the channel. When the channel is deleted these orphaned agents should be + * removed — but only if they are not members of any other channel. + */ +import { + deleteManagedAgent, + getChannelMembers, + listManagedAgents, + listRelayAgents, +} from "@/shared/api/tauri"; + +export async function cleanupChannelAgents(channelId: string): Promise { + const [members, managedAgents, relayAgents] = await Promise.all([ + getChannelMembers(channelId), + listManagedAgents(), + listRelayAgents(), + ]); + + const memberPubkeys = new Set( + members.map((member) => member.pubkey.toLowerCase()), + ); + + // Find managed agents that are members of this channel. + const agentsInChannel = managedAgents.filter((agent) => + memberPubkeys.has(agent.pubkey.toLowerCase()), + ); + + // Only delete agents that are NOT members of any other channel. + const agentsToDelete = agentsInChannel.filter((agent) => { + const relayAgent = relayAgents.find( + (ra) => ra.pubkey.toLowerCase() === agent.pubkey.toLowerCase(), + ); + if (!relayAgent) { + // Not found in relay — safe to delete. + return true; + } + // Only delete if this is the agent's only channel. + const otherChannels = relayAgent.channelIds.filter( + (id) => id !== channelId, + ); + return otherChannels.length === 0; + }); + + // Delete orphaned agents (best-effort — don't block channel deletion). + await Promise.allSettled( + agentsToDelete.map((agent) => deleteManagedAgent(agent.pubkey)), + ); +} diff --git a/desktop/src/features/channels/hooks.ts b/desktop/src/features/channels/hooks.ts index 544c4be..c4a3921 100644 --- a/desktop/src/features/channels/hooks.ts +++ b/desktop/src/features/channels/hooks.ts @@ -25,6 +25,7 @@ import { unarchiveChannel, updateChannel, } from "@/shared/api/tauri"; +import { cleanupChannelAgents } from "@/features/channels/cleanupChannelAgents"; import type { AddChannelMembersInput, Channel, @@ -343,6 +344,13 @@ export function useDeleteChannelMutation(channelId: string | null) { throw new Error("No channel selected."); } + // Best-effort cleanup of managed agents scoped to this channel. + try { + await cleanupChannelAgents(channelId); + } catch (error) { + console.warn("Failed to clean up managed agents:", error); + } + await deleteChannel(channelId); }, onSuccess: () => { @@ -361,7 +369,11 @@ export function useDeleteChannelMutation(channelId: string | null) { }); }, onSettled: async () => { - await queryClient.invalidateQueries({ queryKey: channelsQueryKey }); + await Promise.all([ + queryClient.invalidateQueries({ queryKey: channelsQueryKey }), + queryClient.invalidateQueries({ queryKey: ["managed-agents"] }), + queryClient.invalidateQueries({ queryKey: ["relay-agents"] }), + ]); }, }); } @@ -478,7 +490,6 @@ export function useSelectedChannel( } // ── Canvas ──────────────────────────────────────────────────────────────────── - export function useCanvasQuery(channelId: string | null, enabled = true) { return useQuery({ queryKey: ["channel-canvas", channelId],