Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions desktop/src/features/agents/agentReuse.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
commandsMatch,
parseTimestamp,
pickPreferredManagedAgent,
findPersonaAgentInChannel,
findReusablePersonaAgent,
findReusableGenericAgent,
findReusableAgent,
Expand Down Expand Up @@ -178,6 +179,26 @@ test("findReusablePersonaAgent: excludes agent already in channel", () => {
assert.equal(result, undefined);
});

test("findPersonaAgentInChannel: finds the matching persona already attached", () => {
const agent = makeAgent({ personaId: "persona-1", pubkey: PUB_A });
const result = findPersonaAgentInChannel(
[agent],
"persona-1",
new Set([PUB_A]),
);
assert.equal(result, agent);
});

test("findPersonaAgentInChannel: ignores another persona in the channel", () => {
const agent = makeAgent({ personaId: "persona-2", pubkey: PUB_A });
const result = findPersonaAgentInChannel(
[agent],
"persona-1",
new Set([PUB_A]),
);
assert.equal(result, undefined);
});

test("findReusablePersonaAgent: excludes agent with different personaId", () => {
const agent = makeAgent({ personaId: "persona-2", pubkey: PUB_A });
const channelMembers = new Set([PUB_B]);
Expand Down
13 changes: 13 additions & 0 deletions desktop/src/features/agents/agentReuse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,19 @@ export function findReusablePersonaAgent(
return pickPreferredManagedAgent(candidates);
}

export function findPersonaAgentInChannel(
agents: ManagedAgent[],
personaId: string,
channelMemberPubkeys: ReadonlySet<string>,
): ManagedAgent | undefined {
const candidates = agents.filter(
(agent) =>
agent.personaId === personaId &&
channelMemberPubkeys.has(normalizePubkey(agent.pubkey)),
);
return pickPreferredManagedAgent(candidates);
}

export function findReusableGenericAgent(
agents: ManagedAgent[],
command: string,
Expand Down
199 changes: 199 additions & 0 deletions desktop/src/features/agents/channelAgents.teamReuse.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
import assert from "node:assert/strict";
import test from "node:test";

import { createChannelManagedAgents } from "./channelAgents.ts";

const AGENT_PUBKEY = "a".repeat(64);

function rawAgent(overrides = {}) {
return {
pubkey: AGENT_PUBKEY,
name: "corsai",
persona_id: "persona-corsai",
runtime: "codex",
team_id: "team-dpwai",
relay_url: "wss://relay.example",
acp_command: "buzz-acp",
agent_command: "codex-acp",
agent_args: [],
mcp_command: "",
turn_timeout_seconds: 0,
idle_timeout_seconds: 0,
max_turn_duration_seconds: 0,
parallelism: 1,
system_prompt: "Support the team.",
model: null,
provider: null,
persona_out_of_date: false,
persona_orphaned: false,
needs_restart: false,
status: "running",
pid: null,
created_at: "2026-01-15T00:00:00Z",
updated_at: "2026-01-15T00:00:00Z",
last_started_at: null,
last_stopped_at: null,
last_exit_code: null,
last_error: null,
last_error_code: null,
log_path: "",
start_on_app_launch: true,
backend: { type: "local" },
backend_agent_id: null,
respond_to: "owner-only",
respond_to_allowlist: [],
...overrides,
};
}

function input() {
return {
runtime: {
id: "codex",
label: "Codex",
command: "codex-acp",
defaultArgs: [],
mcpCommand: "",
},
name: "corsai",
personaId: "persona-corsai",
teamId: "team-dpwai",
systemPrompt: "Support the team.",
respondTo: "owner-only",
ensureRunning: false,
};
}

function installTauriInvoke(handler) {
const prior = globalThis.window;
globalThis.window ??= {};
window.__TAURI_INTERNALS__ = { invoke: handler };
return () => {
globalThis.window = prior;
};
}

test("re-deploying a team reuses its persona already in the channel", async (t) => {
const commands = [];
t.after(
installTauriInvoke((command) => {
commands.push(command);
if (command === "list_managed_agents")
return Promise.resolve([rawAgent()]);
if (command === "get_channel_members") {
return Promise.resolve({
members: [
{
pubkey: AGENT_PUBKEY,
role: "bot",
is_agent: true,
joined_at: "2026-01-15T00:00:00Z",
display_name: "corsai",
},
],
next_cursor: null,
});
}
if (command === "add_channel_members") {
return Promise.resolve({ added: [], errors: [] });
}
throw new Error(`Unexpected Tauri command: ${command}`);
}),
);

const result = await createChannelManagedAgents("channel-1", [input()]);

assert.equal(result.failures.length, 0);
assert.equal(result.successes.length, 1);
assert.equal(result.successes[0].created, false);
assert.equal(result.successes[0].agent.pubkey, AGENT_PUBKEY);
assert.equal(commands.includes("create_managed_agent"), false);
});

test("one batch never mints two keys for the same persona", async (t) => {
const commands = [];
t.after(
installTauriInvoke((command) => {
commands.push(command);
if (command === "list_managed_agents") return Promise.resolve([]);
if (command === "get_channel_members") {
return Promise.resolve({ members: [], next_cursor: null });
}
if (command === "create_managed_agent") {
return Promise.resolve({
agent: rawAgent(),
private_key_nsec: "nsec-test",
profile_sync_error: null,
spawn_error: null,
});
}
if (command === "add_channel_members") {
return Promise.resolve({ added: [AGENT_PUBKEY], errors: [] });
}
throw new Error(`Unexpected Tauri command: ${command}`);
}),
);

const result = await createChannelManagedAgents("channel-1", [
input(),
input(),
]);

assert.equal(result.failures.length, 0);
assert.equal(result.successes.length, 2);
assert.equal(result.successes[0].created, true);
assert.equal(result.successes[1].created, false);
assert.equal(
commands.filter((command) => command === "create_managed_agent").length,
1,
);
});

test("an explicit fresh-instance request still mints a new key", async (t) => {
const commands = [];
t.after(
installTauriInvoke((command) => {
commands.push(command);
if (command === "list_managed_agents")
return Promise.resolve([rawAgent()]);
if (command === "get_channel_members") {
return Promise.resolve({
members: [
{
pubkey: AGENT_PUBKEY,
role: "bot",
is_agent: true,
joined_at: "2026-01-15T00:00:00Z",
display_name: "corsai",
},
],
next_cursor: null,
});
}
if (command === "create_managed_agent") {
return Promise.resolve({
agent: rawAgent({ pubkey: "b".repeat(64) }),
private_key_nsec: "nsec-fresh-test",
profile_sync_error: null,
spawn_error: null,
});
}
if (command === "add_channel_members") {
return Promise.resolve({ added: ["b".repeat(64)], errors: [] });
}
throw new Error(`Unexpected Tauri command: ${command}`);
}),
);

const freshInput = { ...input(), forceNewInstance: true };
const result = await createChannelManagedAgents("channel-1", [freshInput]);

assert.equal(result.failures.length, 0);
assert.equal(result.successes.length, 1);
assert.equal(result.successes[0].created, true);
assert.equal(result.successes[0].agent.pubkey, "b".repeat(64));
assert.equal(
commands.filter((command) => command === "create_managed_agent").length,
1,
);
});
51 changes: 49 additions & 2 deletions desktop/src/features/agents/channelAgents.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {
commandsMatch,
findPersonaAgentInChannel,
findReusableGenericAgent,
findReusablePersonaAgent,
pickPreferredManagedAgent,
Expand Down Expand Up @@ -72,7 +73,7 @@ export type CreateChannelManagedAgentInput = {
systemPrompt?: string;
avatarUrl?: string;
personaId?: string | null;
/** Team this instance is deployed from; prevents cross-team reuse. */
/** Team recorded on a newly created instance. */
teamId?: string | null;
/**
* True when `runtime` is a runtime the user deliberately picked to override
Expand Down Expand Up @@ -326,6 +327,36 @@ export async function provisionChannelManagedAgent(
throw new Error("Agent name is required.");
}

// A repeated team deployment must be idempotent. Prefer the persona's
// existing channel member before looking elsewhere or minting a new key.
if (
input.personaId &&
!input.forceNewInstance &&
context?.managedAgents &&
context.channelMemberPubkeys
) {
const inChannel = findPersonaAgentInChannel(
context.managedAgents,
input.personaId,
context.channelMemberPubkeys,
);
if (inChannel) {
const definition = context.personas.find(
(persona) => persona.id === input.personaId,
);
const { agent: updatedAgent } = await applyReusableAgentAccessPolicy(
inChannel,
input,
definition,
);
return {
agent: updatedAgent,
created: false,
runtimeId: input.runtime.id,
};
}
}

// Smart reuse: if a managed agent with the same personaId already exists
// and is not already in this channel, attach it instead of creating a new one.
if (
Expand Down Expand Up @@ -466,7 +497,11 @@ export async function createChannelManagedAgents(
const channelMemberPubkeys = new Set(
members.map((m) => normalizePubkey(m.pubkey)),
);
const context = { managedAgents, channelMemberPubkeys, personas };
const context: ChannelAgentReuseContext = {
managedAgents: [...managedAgents],
channelMemberPubkeys,
personas,
};

// Sequential loop: each agent must be fully created and its relay membership
// written before the next starts. Concurrent writes to the replaceable
Expand All @@ -479,6 +514,18 @@ export async function createChannelManagedAgents(
try {
const result = await createChannelManagedAgent(channelId, input, context);
successes.push(result);
const normalizedPubkey = normalizePubkey(result.agent.pubkey);
context.channelMemberPubkeys = new Set(context.channelMemberPubkeys).add(
normalizedPubkey,
);
const existingIndex = context.managedAgents.findIndex(
(agent) => normalizePubkey(agent.pubkey) === normalizedPubkey,
);
if (existingIndex === -1) {
context.managedAgents.push(result.agent);
} else {
context.managedAgents[existingIndex] = result.agent;
}
} catch (error) {
failures.push({
kind: input.personaId ? "persona" : "generic",
Expand Down
2 changes: 0 additions & 2 deletions desktop/src/features/agents/ui/AddTeamToChannelDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -148,8 +148,6 @@ export function AddTeamToChannelDialog({
model: persona.model ?? undefined,
personaId: persona.id,
teamId: team.id,
// One persona can be deployed under multiple teams with different instructions.
forceNewInstance: true,
role,
};
});
Expand Down