fix(server): an apply reports the options the plugin reset along the way

Applying and unsolicited reports now share one conversion from the plugin's
report to a patch, so a key the plugin stops reporting is a removal on both.
This commit is contained in:
Mohamed Boudra
2026-11-02 01:10:56 +01:00
parent 25a3efe0ac
commit 377b06be0d
2 changed files with 34 additions and 7 deletions
@@ -1171,6 +1171,8 @@ describe("PluginAgentClientRegistry", () => {
}
if (typeof loud === "boolean") state.loud = loud;
if (typeof tokenBudget === "number") state.tokenBudget = tokenBudget;
// Going loud returns an unrequested token budget to its default, by leaving it out.
if (loud === true && tokenBudget === undefined) state.tokenBudget = undefined;
emitConfig(emit, input.sessionId);
emit(
refuseAfterChanging
@@ -1233,6 +1235,23 @@ describe("PluginAgentClientRegistry", () => {
},
);
test.runIf(shape === "controls")(
"an apply returns the option the plugin reset along the way as removed",
async () => {
const { session, events, changeOnItsOwn } = await openStubbornVoiceSession({});
changeOnItsOwn({ tokenBudget: 500 });
const ended = await session.applyOptions?.({ loud: true });
expect(ended).toEqual({ token_budget: null });
// The apply reports it; the plugin's later identical report is not a second change.
expect(eventsOfType(events, "options_changed")).toEqual([
{ type: "options_changed", provider: "plugin-direct", patch: { token_budget: 500 } },
]);
await session.close();
},
);
test("a rejected apply reports what the plugin moved before refusing, since nothing is stored", async () => {
const { session, events } = await openStubbornVoiceSession({}, { refuseAfterChanging: true });
@@ -1462,7 +1462,7 @@ class PluginAgentSession implements AgentSession {
} finally {
this.applyingOptions = false;
}
const ended = this.optionDifferences({ ...before, ...settings });
const ended = this.optionChanges({ ...before, ...settings });
for (const [id, value] of Object.entries(settings)) {
if (value === null) delete this.committedOptions[id];
}
@@ -1672,14 +1672,22 @@ class PluginAgentSession implements AgentSession {
this.knownOptions = current;
}
/**
* What changed against the options `known` to be set: values that differ, and `null` for a key
* the plugin no longer reports, which went back to its default.
*/
private optionChanges(known: Readonly<Record<string, JsonValue>>): Record<string, JsonValue> {
const patch = this.optionDifferences(known);
const current = this.currentOptions();
for (const [id, value] of Object.entries(known)) {
if (value !== null && !(id in current)) patch[id] = null;
}
return patch;
}
private reportOptionChanges(): AgentStreamEvent[] {
if (this.applyingOptions) return [];
const patch = this.optionDifferences(this.knownOptions);
// A key the plugin reported before and no longer reports went back to its default.
const current = this.currentOptions();
for (const id of Object.keys(this.knownOptions)) {
if (!(id in current)) patch[id] = null;
}
const patch = this.optionChanges(this.knownOptions);
if (Object.keys(patch).length === 0) return [];
this.commitOptions();
return [{ type: "options_changed", provider: this.provider, patch }];