From 25a3efe0ace58702264d18e4a0aeba1294f2868b Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Mon, 2 Nov 2026 01:01:24 +0100 Subject: [PATCH] fix(server): a plugin that stops reporting an option has reset it The adapter only visited keys present in a new report, so a key the plugin dropped to return to its default kept its stored value and its rewind value. A previously reported key that disappears is now reported as removed. --- .../src/server/agent/plugin-provider.test.ts | 24 +++++++++++++++++++ .../src/server/agent/plugin-provider.ts | 5 ++++ 2 files changed, 29 insertions(+) diff --git a/packages/server/src/server/agent/plugin-provider.test.ts b/packages/server/src/server/agent/plugin-provider.test.ts index c79039a4a8..ecfcd440b6 100644 --- a/packages/server/src/server/agent/plugin-provider.test.ts +++ b/packages/server/src/server/agent/plugin-provider.test.ts @@ -1209,6 +1209,30 @@ describe("PluginAgentClientRegistry", () => { await session.close(); }); + test.runIf(shape === "controls")( + "reports an option the plugin stops reporting as removed, alone or with another change", + async () => { + const { session, events, changeOnItsOwn } = await openStubbornVoiceSession({}); + changeOnItsOwn({ tokenBudget: 500 }); + + changeOnItsOwn({ tokenBudget: undefined }); + changeOnItsOwn({ tokenBudget: 700 }); + changeOnItsOwn({ tokenBudget: undefined, loud: true }); + + expect(eventsOfType(events, "options_changed")).toEqual([ + { type: "options_changed", provider: "plugin-direct", patch: { token_budget: 500 } }, + { type: "options_changed", provider: "plugin-direct", patch: { token_budget: null } }, + { type: "options_changed", provider: "plugin-direct", patch: { token_budget: 700 } }, + { + type: "options_changed", + provider: "plugin-direct", + patch: { token_budget: null, loud: true }, + }, + ]); + 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 }); diff --git a/packages/server/src/server/agent/plugin-provider.ts b/packages/server/src/server/agent/plugin-provider.ts index 07181abc1c..e1560d2fa7 100644 --- a/packages/server/src/server/agent/plugin-provider.ts +++ b/packages/server/src/server/agent/plugin-provider.ts @@ -1675,6 +1675,11 @@ class PluginAgentSession implements AgentSession { 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; + } if (Object.keys(patch).length === 0) return []; this.commitOptions(); return [{ type: "options_changed", provider: this.provider, patch }];