From e4a49b414ff20cb1f7419e6c89737375954a6e4f Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Sun, 1 Nov 2026 21:09:10 +0100 Subject: [PATCH] fix(server): profile options convert against the provider graph a write commits A write that edited a provider and a profile together folded the profile against a shallow merge that dropped the provider's extends, so its legacy Fast was not converted. The fold now sees the same deep-merged providers the write commits. --- .../src/server/daemon-config-store.test.ts | 15 +++++++++++++++ .../server/src/server/daemon-config-store.ts | 19 +++++++++++++------ 2 files changed, 28 insertions(+), 6 deletions(-) diff --git a/packages/server/src/server/daemon-config-store.test.ts b/packages/server/src/server/daemon-config-store.test.ts index cf5677e744..1bb6680259 100644 --- a/packages/server/src/server/daemon-config-store.test.ts +++ b/packages/server/src/server/daemon-config-store.test.ts @@ -468,6 +468,21 @@ describe("DaemonConfigStore", () => { expect(store.get().agentProfiles).toEqual([{ id, name: id, provider: id, options: {} }]); }); + test("a provider edit in the same write does not hide which adapter a profile runs on", async () => { + const store = storeWithLegacyProfiles(); + + await store.patch({ + providers: { "my-codex": { label: "Renamed" } }, + agentProfiles: [ + { id: "p", name: "P", provider: "my-codex", featureValues: { fast_mode: false } }, + ], + }); + + expect(store.get().agentProfiles).toEqual([ + { id: "p", name: "P", provider: "my-codex", options: { service_tier: "default" } }, + ]); + }); + test("a malformed value rejects the write and leaves config reads working", async () => { const store = storeWithLegacyProfiles(); diff --git a/packages/server/src/server/daemon-config-store.ts b/packages/server/src/server/daemon-config-store.ts index 55b7cd0caa..c762999458 100644 --- a/packages/server/src/server/daemon-config-store.ts +++ b/packages/server/src/server/daemon-config-store.ts @@ -465,6 +465,18 @@ export class DaemonConfigStore { return this.enqueue(() => this.applySupportedPatch({ skills: { selection } })); } + private foldPatchProfiles< + T extends { agentProfiles?: AgentProfile[]; providers?: Record }, + >(submittedPatch: T): T { + return foldConfigProfiles({ + config: submittedPatch, + // The provider graph this request will commit, so `extends` survives a partial edit. + providers: deepMerge(this.current.providers ?? {}, submittedPatch.providers ?? {}), + source: "client write", + held: this.current.agentProfiles, + }); + } + private async applySupportedPatch( parsedPatch: SupportedMutableConfigPatch, ): Promise { @@ -474,12 +486,7 @@ export class DaemonConfigStore { ); } const { removeProviders = [], ...submittedPatch } = parsedPatch; - const patch = foldConfigProfiles({ - config: submittedPatch, - providers: { ...this.current.providers, ...submittedPatch.providers }, - source: "client write", - held: this.current.agentProfiles, - }); + const patch = this.foldPatchProfiles(submittedPatch); const { features: speechPatch, ...configPatch } = patch; const removedProviders = Array.from(new Set(removeProviders)); const merged = deepMerge(this.current, configPatch);