From a144b5c7af3036b2f92addb74f6df0d6b68a1bca Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Sun, 1 Nov 2026 10:01:36 +0100 Subject: [PATCH] fix(server): reopen the created native plugin session, keep ACP reset acknowledgements from storing removed options When a plugin session declines configuration and is reopened with its settings, the reopen continues the native session the first open created instead of creating a second one. An ACP provider response to a write that only resets an absent option to its default no longer emits an option change, so a removed option stays absent. --- .../src/server/agent/plugin-provider.test.ts | 11 +++++++++- .../src/server/agent/plugin-provider.ts | 8 ++++--- .../src/server/agent/providers/acp-agent.ts | 5 ++++- .../agent/providers/cursor-acp-agent.test.ts | 22 +++++++++++++++++++ 4 files changed, 41 insertions(+), 5 deletions(-) diff --git a/packages/server/src/server/agent/plugin-provider.test.ts b/packages/server/src/server/agent/plugin-provider.test.ts index 709035f851..1b02eae00e 100644 --- a/packages/server/src/server/agent/plugin-provider.test.ts +++ b/packages/server/src/server/agent/plugin-provider.test.ts @@ -908,6 +908,7 @@ function startVoicePlugin(input: { ...CAPABILITIES.filter((name) => input.configurable !== false || name !== "session.configure"), "session.revert.conversation", ]; + let nativeSessions = 0; const harness = createProviderHarness({ capabilities, async handleInput(request, emit) { @@ -920,7 +921,10 @@ function startVoicePlugin(input: { ? capabilities.filter((name) => name !== "session.configure") : capabilities, restoration: "core", - persistence: request.persistence ?? { version: 1, data: { token: "root" } }, + persistence: request.persistence ?? { + version: 1, + data: { token: `native-${++nativeSessions}` }, + }, cwd: request.config.cwd, }); emit({ @@ -1808,6 +1812,11 @@ describe("PluginAgentClientRegistry", () => { input.type === "session.open" ? [input.config.settings] : [], ); expect(opens.at(-1)).toEqual({ voice: "soft" }); + // The second open continues the native session the first one created. + const persistence = harness.inputs.flatMap((input) => + input.type === "session.open" ? [input.persistence] : [], + ); + expect(persistence).toEqual([undefined, { version: 1, data: { token: "native-1" } }]); expect(configuredSettings(harness)).toEqual([]); expect(toAgentPayload(manager.getAgent(agent.id)!).options).toEqual({ voice: "soft" }); } finally { diff --git a/packages/server/src/server/agent/plugin-provider.ts b/packages/server/src/server/agent/plugin-provider.ts index 0150129d7f..9e54b8230b 100644 --- a/packages/server/src/server/agent/plugin-provider.ts +++ b/packages/server/src/server/agent/plugin-provider.ts @@ -1104,11 +1104,11 @@ class PluginAgentClient implements AgentClient { // removal returns to. So a plugin that can be configured opens without setting overrides and // has them applied after. One that cannot takes them in the open request. const openConfig = mapSessionConfig(input.config, input.launchContext, input.persist); - const openWith = (config: ProviderSessionConfig) => + const openWith = (config: ProviderSessionConfig, persistence = input.persistence) => this.runtime.openSession({ sessionId: randomUUID(), config, - persistence: input.persistence, + persistence, history: input.history, }); let bridge: ProviderRuntimeSession; @@ -1119,8 +1119,10 @@ class PluginAgentClient implements AgentClient { if (!bridge.negotiatedCapabilities.includes("session.configure")) { configurable = false; if (Object.keys(openConfig.settings ?? {}).length > 0) { + // Reopen the native session the first open created rather than creating another. + const created = input.persistence ?? bridge.persistence ?? undefined; await bridge.close(); - bridge = await openWith(openConfig); + bridge = await openWith(openConfig, created); } } } else { diff --git a/packages/server/src/server/agent/providers/acp-agent.ts b/packages/server/src/server/agent/providers/acp-agent.ts index a1d5e4298f..d9439e26a5 100644 --- a/packages/server/src/server/agent/providers/acp-agent.ts +++ b/packages/server/src/server/agent/providers/acp-agent.ts @@ -2523,8 +2523,11 @@ export class ACPAgentSession implements AgentSession, ACPClient { requestedValue, label: featureOption.label, }); + // A write that only returns an absent option to its default has no stored option to correct. + const held = this.optionsInFlight ?? this.config.providerOptions; + const isHeld = Object.prototype.hasOwnProperty.call(held ?? {}, featureId); this.config.providerOptions = { ...this.config.providerOptions, [featureId]: currentValue }; - if (currentValue !== requestedValue) { + if (isHeld && currentValue !== requestedValue) { // The provider kept a different selection than the one asked for. this.pushEvent({ type: "options_changed", diff --git a/packages/server/src/server/agent/providers/cursor-acp-agent.test.ts b/packages/server/src/server/agent/providers/cursor-acp-agent.test.ts index f2d221216d..5770ca1fb4 100644 --- a/packages/server/src/server/agent/providers/cursor-acp-agent.test.ts +++ b/packages/server/src/server/agent/providers/cursor-acp-agent.test.ts @@ -489,6 +489,28 @@ describe("CursorACPAgentClient session start", () => { expect(events.filter((event) => event.type === "options_changed")).toEqual([]); }); + test("a reset response that keeps the previous value does not store the removed option", async () => { + const setSessionConfigOption = vi.fn(async () => ({ + configOptions: [fastConfigOption("true")], + })); + const session = createCursorSession( + { model: "composer-2.5" }, + { + currentModelId: "composer-2.5", + configOptions: [fastConfigOption("false")], + setSessionConfigOption, + }, + ); + await session.initializeNewSession(); + const events: AgentStreamEvent[] = []; + session.subscribe((event) => events.push(event)); + + await session.applyOptions({ [CURSOR_FAST_FEATURE_OPTION.id]: "true" }); + await session.applyOptions({}); + + expect(events.filter((event) => event.type === "options_changed")).toEqual([]); + }); + test("removing the Fast option returns the session to the value it started with", async () => { const setSessionConfigOption = vi.fn(async (request: { value: string }) => ({ configOptions: [fastConfigOption(request.value === "true" ? "true" : "false")],