diff --git a/server/src/__tests__/agent-instructions-routes.test.ts b/server/src/__tests__/agent-instructions-routes.test.ts index f35ef61060..6d36a820d3 100644 --- a/server/src/__tests__/agent-instructions-routes.test.ts +++ b/server/src/__tests__/agent-instructions-routes.test.ts @@ -328,6 +328,45 @@ describe("agent instructions bundle routes", () => { ); }); + it("preserves paperclip skill-sync selections when switching adapters", async () => { + // Desired skills live inside the per-adapter config under + // `paperclipSkillSync`, yet they are adapter-agnostic company-level + // selections. Switching adapter type must not silently wipe them — the + // server carries them over from the existing config the same way it + // preserves env/cwd and the instructions bundle. + mockAgentService.getById.mockResolvedValue({ + ...makeAgent(), + adapterType: "claude_local", + adapterConfig: { + model: "claude-sonnet-4", + paperclipSkillSync: { desiredSkills: ["research", "code-review"] }, + }, + }); + + const res = await requestApp(await createApp(), (baseUrl) => request(baseUrl) + .patch("/api/agents/11111111-1111-4111-8111-111111111111?companyId=company-1") + .send({ + adapterType: "codex_local", + replaceAdapterConfig: true, + adapterConfig: { + model: "gpt-5.4", + }, + })); + + expect(res.status, JSON.stringify(res.body)).toBe(200); + expect(mockAgentService.update).toHaveBeenCalledWith( + "11111111-1111-4111-8111-111111111111", + expect.objectContaining({ + adapterType: "codex_local", + adapterConfig: expect.objectContaining({ + model: "gpt-5.4", + paperclipSkillSync: { desiredSkills: ["research", "code-review"] }, + }), + }), + expect.any(Object), + ); + }); + it("merges same-adapter config patches so instructions metadata is not dropped", async () => { mockAgentService.getById.mockResolvedValue({ ...makeAgent(), diff --git a/server/src/routes/agents.ts b/server/src/routes/agents.ts index e86851d871..1ac42fe9fa 100644 --- a/server/src/routes/agents.ts +++ b/server/src/routes/agents.ts @@ -2870,9 +2870,13 @@ export function agentRoutes( // Preserve adapter-agnostic keys (env, cwd, etc.) from the existing config // when the adapter type changes. Without this, a PATCH that includes // adapterConfig but omits these keys would silently drop them. + // `paperclipSkillSync` holds the agent's desired-skill selection, which is + // a company-level (adapter-agnostic) choice even though it is persisted + // inside the per-adapter config; switching adapters must not wipe it. const ADAPTER_AGNOSTIC_KEYS = [ "env", "cwd", "timeoutSec", "graceSec", "promptTemplate", "bootstrapPromptTemplate", + "paperclipSkillSync", ] as const; for (const key of ADAPTER_AGNOSTIC_KEYS) { if (rawEffectiveAdapterConfig[key] === undefined && existingAdapterConfig[key] !== undefined) { diff --git a/ui/src/lib/agent-config-patch.test.ts b/ui/src/lib/agent-config-patch.test.ts index 7cdc79e2a2..46e6e85829 100644 --- a/ui/src/lib/agent-config-patch.test.ts +++ b/ui/src/lib/agent-config-patch.test.ts @@ -220,4 +220,28 @@ describe("buildAgentUpdatePatch", () => { replaceAdapterConfig: true, }); }); + + it("preserves paperclip skill-sync selections when changing adapter types", () => { + // Desired skills are adapter-agnostic (company-level selections) but are + // persisted inside the per-adapter config under `paperclipSkillSync`. A + // patch that switches adapters must carry them over instead of wiping the + // agent's skills. + const agent = makeAgent(); + agent.adapterConfig = { + ...agent.adapterConfig, + paperclipSkillSync: { desiredSkills: ["research", "code-review"] }, + }; + + const patch = buildAgentUpdatePatch( + agent, + makeOverlay({ + adapterType: "codex_local", + adapterConfig: { model: "gpt-5.4" }, + }), + ); + + expect((patch.adapterConfig as Record).paperclipSkillSync).toEqual({ + desiredSkills: ["research", "code-review"], + }); + }); }); diff --git a/ui/src/lib/agent-config-patch.ts b/ui/src/lib/agent-config-patch.ts index e57e0c05ab..325d8d8833 100644 --- a/ui/src/lib/agent-config-patch.ts +++ b/ui/src/lib/agent-config-patch.ts @@ -27,6 +27,10 @@ const ADAPTER_AGNOSTIC_KEYS = [ "timeoutSec", "graceSec", "bootstrapPromptTemplate", + // Desired-skill selection is a company-level, adapter-agnostic choice even + // though it is persisted inside the per-adapter config; keep it when the + // adapter type changes so switching adapters does not wipe the agent's skills. + "paperclipSkillSync", ] as const; function omitUndefinedEntries(value: Record) {