From a315838d4336b44eb0836e96dd072301344d89d8 Mon Sep 17 00:00:00 2001 From: dotta Date: Sun, 22 Mar 2026 07:05:08 -0500 Subject: [PATCH] fix: preserve agent instructions on adapter switch Co-Authored-By: Paperclip --- .../agent-instructions-routes.test.ts | 39 +++++++++++++++++++ server/src/routes/agents.ts | 38 +++++++++++++++++- 2 files changed, 75 insertions(+), 2 deletions(-) diff --git a/server/src/__tests__/agent-instructions-routes.test.ts b/server/src/__tests__/agent-instructions-routes.test.ts index 4f6ca414..f47f8dcc 100644 --- a/server/src/__tests__/agent-instructions-routes.test.ts +++ b/server/src/__tests__/agent-instructions-routes.test.ts @@ -197,4 +197,43 @@ describe("agent instructions bundle routes", () => { expect.any(Object), ); }); + + it("preserves managed instructions config when switching adapters", async () => { + mockAgentService.getById.mockResolvedValue({ + ...makeAgent(), + adapterType: "codex_local", + adapterConfig: { + instructionsBundleMode: "managed", + instructionsRootPath: "/tmp/agent-1", + instructionsEntryFile: "AGENTS.md", + instructionsFilePath: "/tmp/agent-1/AGENTS.md", + model: "gpt-5.4", + }, + }); + + const res = await request(createApp()) + .patch("/api/agents/11111111-1111-4111-8111-111111111111?companyId=company-1") + .send({ + adapterType: "claude_local", + adapterConfig: { + model: "claude-sonnet-4", + }, + }); + + expect(res.status, JSON.stringify(res.body)).toBe(200); + expect(mockAgentService.update).toHaveBeenCalledWith( + "11111111-1111-4111-8111-111111111111", + expect.objectContaining({ + adapterType: "claude_local", + adapterConfig: expect.objectContaining({ + model: "claude-sonnet-4", + instructionsBundleMode: "managed", + instructionsRootPath: "/tmp/agent-1", + instructionsEntryFile: "AGENTS.md", + instructionsFilePath: "/tmp/agent-1/AGENTS.md", + }), + }), + expect.any(Object), + ); + }); }); diff --git a/server/src/routes/agents.ts b/server/src/routes/agents.ts index af5a6574..6bacb46e 100644 --- a/server/src/routes/agents.ts +++ b/server/src/routes/agents.ts @@ -73,6 +73,13 @@ export function agentRoutes(db: Db) { }; const DEFAULT_MANAGED_INSTRUCTIONS_ADAPTER_TYPES = new Set(Object.keys(DEFAULT_INSTRUCTIONS_PATH_KEYS)); const KNOWN_INSTRUCTIONS_PATH_KEYS = new Set(["instructionsFilePath", "agentsMdPath"]); + const KNOWN_INSTRUCTIONS_BUNDLE_KEYS = [ + "instructionsBundleMode", + "instructionsRootPath", + "instructionsEntryFile", + "instructionsFilePath", + "agentsMdPath", + ] as const; const router = Router(); const svc = agentService(db); @@ -303,6 +310,24 @@ export function agentRoutes(db: Db) { return trimmed.length > 0 ? trimmed : null; } + function preserveInstructionsBundleConfig( + existingAdapterConfig: Record, + nextAdapterConfig: Record, + ) { + const nextKeys = new Set(Object.keys(nextAdapterConfig)); + if (KNOWN_INSTRUCTIONS_BUNDLE_KEYS.some((key) => nextKeys.has(key))) { + return nextAdapterConfig; + } + + const merged = { ...nextAdapterConfig }; + for (const key of KNOWN_INSTRUCTIONS_BUNDLE_KEYS) { + if (merged[key] === undefined && existingAdapterConfig[key] !== undefined) { + merged[key] = existingAdapterConfig[key]; + } + } + return merged; + } + function parseBooleanLike(value: unknown): boolean | null { if (typeof value === "boolean") return value; if (typeof value === "number") { @@ -1710,9 +1735,18 @@ export function agentRoutes(db: Db) { Object.prototype.hasOwnProperty.call(patchData, "adapterType") || Object.prototype.hasOwnProperty.call(patchData, "adapterConfig"); if (touchesAdapterConfiguration) { - const rawEffectiveAdapterConfig = Object.prototype.hasOwnProperty.call(patchData, "adapterConfig") + const existingAdapterConfig = asRecord(existing.adapterConfig) ?? {}; + const changingAdapterType = + typeof patchData.adapterType === "string" && patchData.adapterType !== existing.adapterType; + let rawEffectiveAdapterConfig = Object.prototype.hasOwnProperty.call(patchData, "adapterConfig") ? (asRecord(patchData.adapterConfig) ?? {}) - : (asRecord(existing.adapterConfig) ?? {}); + : existingAdapterConfig; + if (changingAdapterType) { + rawEffectiveAdapterConfig = preserveInstructionsBundleConfig( + existingAdapterConfig, + rawEffectiveAdapterConfig, + ); + } const effectiveAdapterConfig = applyCreateDefaultsByAdapterType( requestedAdapterType, rawEffectiveAdapterConfig,