From a44f2735223c7ee5342e9b0d84c05b8542d1cd3b Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Sun, 1 Nov 2026 17:01:54 +0100 Subject: [PATCH] fix(server): ACP reports a select that appears after startup with its native value A model switch can introduce a select (Cursor Fast) that the session did not have at open. Its value is now stored like any other native selection instead of being skipped because the previous snapshot did not name it. --- .../server/agent/providers/acp-agent.test.ts | 18 ++++++++++++++++++ .../src/server/agent/providers/acp-agent.ts | 12 +++--------- .../providers/test-utils/fake-acp-agent.mjs | 7 +++++-- 3 files changed, 26 insertions(+), 11 deletions(-) diff --git a/packages/server/src/server/agent/providers/acp-agent.test.ts b/packages/server/src/server/agent/providers/acp-agent.test.ts index 85fb019081..acab07ae5e 100644 --- a/packages/server/src/server/agent/providers/acp-agent.test.ts +++ b/packages/server/src/server/agent/providers/acp-agent.test.ts @@ -1977,6 +1977,24 @@ describe("ACPAgentSession options against an ACP agent", () => { ]), ); }); + + test("a select that appears after startup is reported with its native value", async () => { + const { session, optionEvents } = await openSession({ + scenario: { + options: [thoughtOption("low")], + adds: { thought: [copilotAgentConfigOption("Probe Agent")] }, + }, + }); + expect(session.openOptionsPatch).toEqual({}); + + await session.setThinkingOption("high"); + + await vi.waitFor(() => + expect(optionEvents).toEqual([ + { type: "options_changed", provider: "copilot", patch: { agent: "Probe Agent" } }, + ]), + ); + }); }); describe("deriveModelDefinitionsFromACP", () => { diff --git a/packages/server/src/server/agent/providers/acp-agent.ts b/packages/server/src/server/agent/providers/acp-agent.ts index e95aa1d0ee..e28d8caa88 100644 --- a/packages/server/src/server/agent/providers/acp-agent.ts +++ b/packages/server/src/server/agent/providers/acp-agent.ts @@ -2512,12 +2512,10 @@ export class ACPAgentSession implements AgentSession, ACPClient { return values; } - /** The selects whose value is not what `expected` says; selects it does not name are skipped. */ + /** The selects whose value is not what `expected` says, including ones it does not name. */ private featureDifferences(expected: Record): Record { return Object.fromEntries( - Object.entries(this.featureValues()).filter( - ([id, value]) => id in expected && expected[id] !== value, - ), + Object.entries(this.featureValues()).filter(([id, value]) => expected[id] !== value), ); } @@ -3176,11 +3174,7 @@ export class ACPAgentSession implements AgentSession, ACPClient { } // The options are the state, so every select the session opened with is reported, including // ones nobody set (a native default is stored like any other value). - this.openOptionsPatch = Object.fromEntries( - Object.entries(this.featureValues()).filter( - ([id, value]) => configuredFeatureValues[id] !== value, - ), - ); + this.openOptionsPatch = this.featureDifferences(configuredFeatureValues); } /** diff --git a/packages/server/src/server/agent/providers/test-utils/fake-acp-agent.mjs b/packages/server/src/server/agent/providers/test-utils/fake-acp-agent.mjs index 1ea77896f2..e284a326da 100644 --- a/packages/server/src/server/agent/providers/test-utils/fake-acp-agent.mjs +++ b/packages/server/src/server/agent/providers/test-utils/fake-acp-agent.mjs @@ -5,6 +5,7 @@ // "notify-then-reject" does the same and then fails the write // moves: { [configId]: { [id]: value } } writes that also move another select, announced // by a config_option_update after the write is answered +// adds: { [configId]: option[] } writes that make more selects appear, announced the same way import { Readable, Writable } from "node:stream"; import { AgentSideConnection, ndJsonStream } from "@agentclientprotocol/sdk"; @@ -39,8 +40,10 @@ export const connection = new AgentSideConnection( return { configOptions: options }; } const moves = scenario.moves?.[configId]; - if (moves) { - for (const [id, moved] of Object.entries(moves)) set(id, moved); + const adds = scenario.adds?.[configId]; + if (moves || adds) { + for (const [id, moved] of Object.entries(moves ?? {})) set(id, moved); + options = [...options, ...(adds ?? [])]; setTimeout(() => void announce(), 20); } return { configOptions: options };