diff --git a/apps/web/test/agents-page.test.tsx b/apps/web/test/agents-page.test.tsx
index a26156df5..f313024f0 100644
--- a/apps/web/test/agents-page.test.tsx
+++ b/apps/web/test/agents-page.test.tsx
@@ -144,6 +144,43 @@ describe("agentModelSettledContent (CL-6848)", () => {
).toEqual({ kind: "model", label: "Default" });
});
+ test("an unset model stays Default when the catalog winner changes (CL-6782)", () => {
+ const unset = {
+ status: "ready" as const,
+ data: { name: "triage-bot" },
+ };
+ expect(
+ agentModelSettledContent(unset, [
+ { canonicalName: "claude-sonnet-4", displayName: "Claude Sonnet 4" },
+ ]),
+ ).toEqual({ kind: "model", label: "Default" });
+ expect(
+ agentModelSettledContent(unset, [
+ { canonicalName: "claude-haiku-5", displayName: "Claude Haiku 5" },
+ { canonicalName: "claude-sonnet-4", displayName: "Claude Sonnet 4" },
+ ]),
+ ).toEqual({ kind: "model", label: "Default" });
+ });
+
+ test("a stored model is unchanged when the catalog default becomes a different winner (CL-6782)", () => {
+ const stored = {
+ status: "ready" as const,
+ data: { name: "triage-bot", model: "claude-sonnet-4" },
+ };
+ expect(
+ agentModelSettledContent(stored, [
+ { canonicalName: "claude-sonnet-4", displayName: "Claude Sonnet 4" },
+ { canonicalName: "claude-haiku-5", displayName: "Claude Haiku 5" },
+ ]),
+ ).toEqual({ kind: "model", label: "Claude Sonnet 4" });
+ expect(
+ agentModelSettledContent(stored, [
+ { canonicalName: "claude-haiku-5", displayName: "Claude Haiku 5" },
+ { canonicalName: "claude-sonnet-4", displayName: "Claude Sonnet 4" },
+ ]),
+ ).toEqual({ kind: "model", label: "Claude Sonnet 4" });
+ });
+
test("a ready model maps to the catalog displayName, not the raw id", () => {
expect(
agentModelSettledContent(
diff --git a/packages/settings-ui/src/connections-section.tsx b/packages/settings-ui/src/connections-section.tsx
index efe799633..9dd06bc0f 100644
--- a/packages/settings-ui/src/connections-section.tsx
+++ b/packages/settings-ui/src/connections-section.tsx
@@ -465,6 +465,10 @@ function ModelRoutePanel({
}
function chooseModel(canonicalName: string) {
+ // Tenant default is an offering-priority write only (CL-6782). Existing
+ // agents keep whatever `capabilities.model` they already store; new
+ // agents may still pick up the default at create. Never PATCH
+ // `/agent-definitions`.
const target = ordered.find((row) => row.canonicalName === canonicalName);
applyPatches(
target === undefined
@@ -485,8 +489,8 @@ function ModelRoutePanel({
Default model & fallbacks
- Used by Myra and new workbenches unless an agent chooses its own
- model.
+ Used by Myra and new agents. Changing the default does not rewrite
+ models already stored on existing agents.
diff --git a/packages/settings-ui/src/strings.ts b/packages/settings-ui/src/strings.ts
index a1ecf43a0..2929c8e80 100644
--- a/packages/settings-ui/src/strings.ts
+++ b/packages/settings-ui/src/strings.ts
@@ -193,7 +193,7 @@ export const SETTINGS_STRINGS = {
connectionsSectionTitle: "AI providers",
connectionsSectionDescription:
- "Choose the model route Myra and new workbenches use, then connect the providers behind it.",
+ "Choose the model route Myra and new agents use, then connect the providers behind it. Changing the default does not rewrite existing agents.",
connectionsLoadError: "connections",
connectionsStatusNotConnected: "Not connected",
connectionsStatusConnected: "Connected",
diff --git a/packages/settings-ui/test/connections-default-model-pick.test.tsx b/packages/settings-ui/test/connections-default-model-pick.test.tsx
index 7a0cc3cc2..8424e232f 100644
--- a/packages/settings-ui/test/connections-default-model-pick.test.tsx
+++ b/packages/settings-ui/test/connections-default-model-pick.test.tsx
@@ -202,4 +202,78 @@ describe("Global model route", () => {
container.remove();
}
});
+
+ test("choosing a default model PATCHes offerings only, never agent capabilities (CL-6782)", async () => {
+ const calls: { url: string; method: string }[] = [];
+ let patched = false;
+
+ globalThis.fetch = ((url: string, init?: RequestInit) => {
+ calls.push({
+ url,
+ method: init?.method ?? "GET",
+ });
+ if (url === "/api/tenants/ten_1/credentials") {
+ return Promise.resolve(
+ json({ data: [ANTHROPIC_CREDENTIAL], nextCursor: null }),
+ );
+ }
+ if (url === "/api/tenants/ten_1/providers") {
+ return Promise.resolve(
+ json({ data: [ANTHROPIC_PROVIDER], nextCursor: null }),
+ );
+ }
+ if (url === "/api/tenants/ten_1/connections/oauth-configured") {
+ return Promise.resolve(json({}));
+ }
+ if (url === "/api/tenants/ten_1/models") {
+ return Promise.resolve(
+ json(resolvedModels(patched ? 0 : 0, patched ? -1 : 1)),
+ );
+ }
+ if (url === "/api/tenants/ten_1/catalog/offerings") {
+ return Promise.resolve(json({ data: OWN_OFFERINGS, nextCursor: null }));
+ }
+ if (url === "/api/tenants/ten_1/catalog/offerings/offering_haiku") {
+ patched = true;
+ return Promise.resolve(json({ ...OWN_OFFERINGS[1], priority: -1 }));
+ }
+ throw new Error(`unexpected fetch: ${url}`);
+ }) as unknown as typeof fetch;
+
+ const { container, root } = renderSection();
+ try {
+ await settle();
+
+ expect(container.textContent).toContain(
+ "does not rewrite models already stored on existing agents",
+ );
+
+ const defaultModel = container.querySelector(
+ 'select[aria-label="Default model"]',
+ ) as HTMLSelectElement | null;
+ act(() => {
+ if (defaultModel !== null) {
+ defaultModel.value = "claude-haiku-5";
+ defaultModel.dispatchEvent(new Event("change", { bubbles: true }));
+ }
+ });
+ await settle();
+
+ const writes = calls.filter(
+ (call) => call.method !== "GET" && call.method !== "HEAD",
+ );
+ expect(writes.length).toBeGreaterThan(0);
+ expect(
+ writes.every((call) =>
+ call.url.startsWith("/api/tenants/ten_1/catalog/offerings/"),
+ ),
+ ).toBe(true);
+ expect(
+ calls.some((call) => call.url.includes("/agent-definitions")),
+ ).toBe(false);
+ } finally {
+ act(() => root.unmount());
+ container.remove();
+ }
+ });
});