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.
This commit is contained in:
Mohamed Boudra
2026-11-01 10:01:36 +01:00
parent 67e4f17dee
commit a144b5c7af
4 changed files with 41 additions and 5 deletions
@@ -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 {
@@ -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 {
@@ -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",
@@ -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")],