From acd2b1a3649a2533c457a94b8ec680cbd558322c Mon Sep 17 00:00:00 2001 From: Teknium <127238744+teknium1@users.noreply.github.com> Date: Fri, 14 Aug 2026 15:16:04 -0700 Subject: [PATCH 1/2] fix(desktop): the main agent's model pick persists as the profile default MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reported: the default bot switches to the OpenAI API account instead of the user's subscription, and doesn't retain the previous selection. Root cause: the composer model picker always sent the switch as --session scope, even for the PRIMARY profile's main agent. So the pick never wrote config.yaml model.provider — and with model.provider unset, resolve_provider('auto') falls through to a leftover OPENAI_API_KEY env var and picks OpenAI/OpenRouter. The subscription the user selected was only ever a per-session override that evaporated on the next session. Fix: when the pick targets the primary profile's main agent (touchesPrimary), send --global so it persists to config.yaml (model.default + model.provider) via the existing model-switch persist path. A SET model.provider already outranks the OPENAI_API_KEY env var in resolve_provider (tier 2 vs tier 3), so the main agent now keeps the chosen provider across restarts. Secondary chat tiles stay --session so picking a model in one chat never rewrites the profile default (the cross-session-contamination guard the old comment protected). No change to resolve_provider's priority chain, so #29285 (an explicit env key beating a STALE oauth login) is untouched — we simply make the user's explicit main-agent selection the config default it always should have been. --- .../src/app/session/hooks/use-model-controls.ts | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/apps/desktop/src/app/session/hooks/use-model-controls.ts b/apps/desktop/src/app/session/hooks/use-model-controls.ts index d3b9ee20a3dff..ed30bc5ebb6c2 100644 --- a/apps/desktop/src/app/session/hooks/use-model-controls.ts +++ b/apps/desktop/src/app/session/hooks/use-model-controls.ts @@ -207,10 +207,20 @@ export function useModelControls({ queryClient, requestGateway }: ModelControlsO } try { + // The PRIMARY profile's main agent is the profile's default — its + // model/provider choice IS the default, so persist it to config.yaml + // (model.default + model.provider) via --global. This is what makes + // the selection "stick": a set model.provider outranks a leftover + // OPENAI_API_KEY env var in resolve_provider(), so the main agent + // keeps the chosen (e.g. subscription) provider across restarts + // instead of silently falling back to an env key. A SECONDARY chat + // tile stays --session so picking a model there can't rewrite the + // profile default (the cross-session-contamination guard). + const scope = touchesPrimary ? '--global' : '--session' const result = await requestGateway<{ deferred?: boolean }>('config.set', { session_id: liveSessionId, key: 'model', - value: `${selection.model} --provider ${selection.provider} --session` + value: `${selection.model} --provider ${selection.provider} ${scope}` }) // A pick made DURING a turn is queued by the gateway and applied at the From ec2ebb3d3ded07d2ed0c32add26fc91defd0f12a Mon Sep 17 00:00:00 2001 From: Teknium <127238744+teknium1@users.noreply.github.com> Date: Fri, 14 Aug 2026 15:36:00 -0700 Subject: [PATCH 2/2] MoA presets stay session-scoped; update tests for primary-persist intent MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fix CI (ui shard 3of3): the primary main-agent pick now persists via --global, but MoA (mixture-of-agents) presets must NOT — a transient orchestration choice can't become the global gateway default. Exclude provider==='moa' from the persist path (stays --session). Update the primary-picker test to assert --global (the new intent) and keep the MoA + secondary-tile tests asserting --session (the guards that prove the narrowing). 19/19 green locally. --- .../app/session/hooks/use-model-controls.test.tsx | 7 +++++-- .../src/app/session/hooks/use-model-controls.ts | 14 ++++++++++---- 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/apps/desktop/src/app/session/hooks/use-model-controls.test.tsx b/apps/desktop/src/app/session/hooks/use-model-controls.test.tsx index 8a3ceb589abb0..f8fbeb511ab8d 100644 --- a/apps/desktop/src/app/session/hooks/use-model-controls.test.tsx +++ b/apps/desktop/src/app/session/hooks/use-model-controls.test.tsx @@ -219,7 +219,7 @@ describe('useModelControls', () => { }) }) - it('routes active-session picker changes through config.set with an explicit session-scoped provider', async () => { + it('persists an active primary-session picker change as the profile default via config.set --global', async () => { $activeSessionId.set('session-1') const requestGateway = vi.fn(async () => ({ key: 'model', value: 'claude-sonnet-4.6' }) as never) let controls!: Controls @@ -233,10 +233,13 @@ describe('useModelControls', () => { }) ).resolves.toBe(true) + // The primary main agent's pick IS the profile default, so it persists to + // config.yaml (model.default + model.provider) — which is what lets a + // chosen subscription provider outrank a leftover OPENAI_API_KEY env var. expect(requestGateway).toHaveBeenCalledWith('config.set', { session_id: 'session-1', key: 'model', - value: 'claude-sonnet-4.6 --provider anthropic --session' + value: 'claude-sonnet-4.6 --provider anthropic --global' }) expect(requestGateway).not.toHaveBeenCalledWith('slash.exec', expect.anything()) }) diff --git a/apps/desktop/src/app/session/hooks/use-model-controls.ts b/apps/desktop/src/app/session/hooks/use-model-controls.ts index ed30bc5ebb6c2..f2487e25ca65e 100644 --- a/apps/desktop/src/app/session/hooks/use-model-controls.ts +++ b/apps/desktop/src/app/session/hooks/use-model-controls.ts @@ -213,10 +213,16 @@ export function useModelControls({ queryClient, requestGateway }: ModelControlsO // the selection "stick": a set model.provider outranks a leftover // OPENAI_API_KEY env var in resolve_provider(), so the main agent // keeps the chosen (e.g. subscription) provider across restarts - // instead of silently falling back to an env key. A SECONDARY chat - // tile stays --session so picking a model there can't rewrite the - // profile default (the cross-session-contamination guard). - const scope = touchesPrimary ? '--global' : '--session' + // instead of silently falling back to an env key. + // + // Two things stay --session, deliberately: + // - a SECONDARY chat tile: picking a model there must not rewrite the + // profile default (the cross-session-contamination guard). + // - MoA (mixture-of-agents) presets: a transient orchestration choice + // that must never become the persisted global gateway default. + const isSessionOnlyPreset = (selection.provider || '').toLowerCase() === 'moa' + const persistsAsDefault = touchesPrimary && !isSessionOnlyPreset + const scope = persistsAsDefault ? '--global' : '--session' const result = await requestGateway<{ deferred?: boolean }>('config.set', { session_id: liveSessionId, key: 'model',