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.
This commit is contained in:
Mohamed Boudra
2026-11-01 17:01:54 +01:00
parent a8d539ca7c
commit 427c2d0743
3 changed files with 26 additions and 11 deletions
@@ -1956,6 +1956,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", () => {
@@ -2467,12 +2467,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<string, unknown>): Record<string, string> {
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),
);
}
@@ -3130,11 +3128,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);
}
/**
@@ -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 };