fix(server): a loaded agent's options stay what its session opened with when a provider default is edited

effectiveOptions was recomputed from the current config.json defaults on every state emission, so editing a default changed the snapshot of an agent whose session never received it. It is now computed when the session opens and moves only through an apply or an options_changed. Editing a default affects agents the next time they open.
This commit is contained in:
Mohamed Boudra
2026-11-01 17:05:09 +01:00
parent 427c2d0743
commit 51a87b98b9
3 changed files with 43 additions and 2 deletions
+3 -1
View File
@@ -41,7 +41,9 @@ or without a control.
differs from what it was given, including a setting nobody asked for (a native default such as
Cursor opening with Fast on). It is set before `createSession`/`resumeSession` resolves and the
daemon stores it before the agent is published. A reopened agent resumes that state, so a
native default the provider later changes does not follow.
native default the provider later changes does not follow. `options` on the snapshot is what
the session opened with, and afterwards it moves only through an apply or an `options_changed`.
Editing a provider default in `config.json` affects agents the next time they open.
- **Live apply.** `applyOptions` receives the keys the patch touches, resolved against the
`config.json` defaults. A removal arrives as the default the key falls back to, or `null` when
there is none; the provider returns the setting to its own default if it can, or throws to
@@ -12939,6 +12939,22 @@ describe("agent options", () => {
expect(states.at(-1)?.effectiveOptions).toEqual({ service_tier: "default", fast_mode: true });
});
test("editing a provider default does not change the options of a loaded agent", async () => {
const { manager, client, agentId } = await createOptionsAgent({ fast_mode: true });
expect(manager.getAgent(agentId)?.effectiveOptions).toEqual({
service_tier: "default",
fast_mode: true,
});
// The user edits the default in config.json while the agent is running.
client.effectiveOptions = (own) => ({ service_tier: "fast", ...own });
await manager.markAgentUnread(agentId);
const agent = manager.getAgent(agentId);
expect(agent?.effectiveOptions).toEqual({ service_tier: "default", fast_mode: true });
expect(toAgentPayload(agent!).options).toEqual({ service_tier: "default", fast_mode: true });
});
test("a provider that rejects an option leaves the stored options unchanged", async () => {
const { manager, client, agentId } = await createOptionsAgent({ fast_mode: false });
client.session!.rejectWith = new Error("fast mode unavailable");
@@ -2284,6 +2284,7 @@ export class AgentManager {
async setAgentOptions(agentId: string, patch: ProviderOptionsPatch): Promise<void> {
const agent = this.requireAgent(agentId);
let ended: ProviderOptionsPatch = {};
let resolvedForApply: ProviderOptions = {};
if (Object.keys(patch).length > 0) {
// A provider that cannot apply options to a running agent must not store them either.
if (!agent.session.applyOptions) {
@@ -2297,6 +2298,7 @@ export class AgentManager {
}
const set = Object.fromEntries(Object.entries(patch).filter(([, value]) => value != null));
const resolved = this.effectiveOptionsFor(agent.provider, set);
resolvedForApply = resolved;
ended =
(await agent.session.applyOptions(
Object.fromEntries(Object.keys(patch).map((key) => [key, resolved[key] ?? null])),
@@ -2305,6 +2307,7 @@ export class AgentManager {
await this.drainSessionEvents(agentId);
}
this.storeOptionsPatch(agent, { ...patch, ...ended });
this.updateEffectiveOptions(agent, { ...patch, ...ended }, resolvedForApply);
this.touchUpdatedAt(agent);
this.emitState(agent);
}
@@ -2322,6 +2325,24 @@ export class AgentManager {
agent.config.providerOptions = Object.keys(own).length > 0 ? own : undefined;
}
/**
* What the running session has: the options it opened with, moved only by an apply or a change
* the provider reports. A removal shows the default it fell back to, as the provider was told.
*/
private updateEffectiveOptions(
agent: ManagedAgent,
patch: ProviderOptionsPatch,
fallbacks: ProviderOptions = {},
): void {
const moved = Object.fromEntries(
Object.entries(patch).map(([key, value]) => [key, value ?? fallbacks[key] ?? null]),
);
agent.effectiveOptions = applyProviderOptionsPatch({
options: agent.effectiveOptions,
patch: moved,
});
}
private effectiveOptionsFor(
provider: AgentProvider,
own: ProviderOptions | undefined,
@@ -4068,6 +4089,8 @@ export class AgentManager {
session,
capabilities: session.capabilities,
config,
// What the session opened with. Provider defaults edited later reach it when it next opens.
effectiveOptions: this.effectiveOptionsFor(config.provider, config.providerOptions),
runtimeInfo: undefined,
lifecycle: "initializing",
createdAt: options?.createdAt ?? now,
@@ -4726,6 +4749,7 @@ export class AgentManager {
}): void {
const { agent, patch, flags } = params;
this.storeOptionsPatch(agent, patch);
this.updateEffectiveOptions(agent, patch);
flags.shouldDispatchEvent = false;
this.emitState(agent);
}
@@ -5285,7 +5309,6 @@ export class AgentManager {
if ("session" in agent && agent.session?.controls) {
agent.controls = agent.session.controls;
}
agent.effectiveOptions = this.effectiveOptionsFor(agent.provider, agent.config.providerOptions);
}
private checkAndSetAttention(agent: ManagedAgent): void {