diff --git a/packages/server/src/server/agent/agent-manager.test.ts b/packages/server/src/server/agent/agent-manager.test.ts index 347954a787..f8fc24a7f4 100644 --- a/packages/server/src/server/agent/agent-manager.test.ts +++ b/packages/server/src/server/agent/agent-manager.test.ts @@ -12831,6 +12831,55 @@ describe("agent options", () => { ]); }); + test("after a removal the snapshot shows the live setting, not the configured default", async () => { + class LiveSession extends OptionsSession { + private tier = "priority"; + override async applyOptions(options: ProviderOptions): Promise { + await super.applyOptions(options); + if (typeof options.service_tier === "string") this.tier = options.service_tier; + } + get controls(): AgentOptionControl[] { + return [ + { + type: "select", + key: "service_tier", + label: "Speed", + defaultValue: this.tier, + choices: [ + { value: "default", label: "Normal" }, + { value: "priority", label: "Fast" }, + ], + }, + ]; + } + } + class LiveClient extends OptionsClient { + override async createSession(config: AgentSessionConfig): Promise { + return new LiveSession(config); + } + } + const workdir = mkdtempSync(join(tmpdir(), "agent-manager-options-")); + const manager = new AgentManager({ + clients: { codex: new LiveClient() }, + registry: new AgentStorage(join(workdir, "agents"), logger), + logger, + }); + const created = await manager.createAgent( + { provider: "codex", cwd: workdir, providerOptions: { service_tier: "priority" } }, + undefined, + { workspaceId: undefined }, + ); + + await manager.setAgentOptions(created.id, { service_tier: null }); + + const agent = manager.getAgent(created.id)!; + expect(agent.config.providerOptions).toBeUndefined(); + expect(toAgentPayload(agent).options).toEqual({ service_tier: "priority" }); + expect(toAgentPayload(agent).features).toEqual([ + expect.objectContaining({ id: "service_tier", value: "priority" }), + ]); + }); + test("an option without a control is settable through the patch and readable in the snapshot", async () => { const { manager, client, storage, agentId } = await createOptionsAgent(); diff --git a/packages/server/src/server/agent/agent-manager.ts b/packages/server/src/server/agent/agent-manager.ts index f82eae0665..d9968df48b 100644 --- a/packages/server/src/server/agent/agent-manager.ts +++ b/packages/server/src/server/agent/agent-manager.ts @@ -5274,7 +5274,17 @@ export class AgentManager { if ("session" in agent && agent.session?.controls) { agent.controls = agent.session.controls; } - agent.effectiveOptions = this.effectiveOptionsFor(agent.provider, agent.config.providerOptions); + const own = agent.config.providerOptions; + const effective = { ...this.effectiveOptionsFor(agent.provider, own) }; + // After a removal the running provider keeps its setting, so a control it reports for a key + // the agent no longer holds shows that live value rather than the `config.json` default. + for (const control of agent.controls ?? []) { + const live = control.defaultValue; + if (own?.[control.key] === undefined && control.key in effective && live !== undefined) { + effective[control.key] = live; + } + } + agent.effectiveOptions = effective; } private checkAndSetAttention(agent: ManagedAgent): void { diff --git a/packages/server/src/server/agent/providers/codex-app-server-agent.features.test.ts b/packages/server/src/server/agent/providers/codex-app-server-agent.features.test.ts index be3af8332f..01c2b87ce9 100644 --- a/packages/server/src/server/agent/providers/codex-app-server-agent.features.test.ts +++ b/packages/server/src/server/agent/providers/codex-app-server-agent.features.test.ts @@ -533,6 +533,32 @@ describe("Codex app-server provider controls", () => { await expect(session.applyOptions?.({ sandbox_mode: "read-only" })).resolves.toBeUndefined(); }); + test("applying unrelated options keeps the running sandbox", async () => { + const { session, appServer } = await createConnectedSession({ + providerOptions: { sandbox_mode: "read-only" }, + }); + + await session.applyOptions?.({ plan_mode: true }); + await session.startTurn("hello"); + + await expect(appServer.waitForTurnStart()).resolves.toMatchObject({ + sandboxPolicy: { type: "readOnly" }, + }); + }); + + test("removing the sandbox option keeps the running sandbox", async () => { + const { session, appServer } = await createConnectedSession({ + providerOptions: { sandbox_mode: "read-only" }, + }); + + await session.applyOptions?.({}); + await session.startTurn("hello"); + + await expect(appServer.waitForTurnStart()).resolves.toMatchObject({ + sandboxPolicy: { type: "readOnly" }, + }); + }); + test("startTurn includes serviceTier when fast mode is enabled", async () => { const { session, appServer } = await createConnectedSession(); diff --git a/packages/server/src/server/agent/providers/codex-app-server-agent.ts b/packages/server/src/server/agent/providers/codex-app-server-agent.ts index 4e4e63d046..40315c05b3 100644 --- a/packages/server/src/server/agent/providers/codex-app-server-agent.ts +++ b/packages/server/src/server/agent/providers/codex-app-server-agent.ts @@ -4671,7 +4671,8 @@ export class CodexAppServerAgentSession implements AgentSession { `Codex speed '${String(requestedTier)}' is not available for model '${this.config.model ?? "default"}'`, ); } - this.providerOptions = next; + // Supplied keys update the running options; absent keys keep their running value. + this.providerOptions = { ...this.providerOptions, ...next }; this.requestedServiceTier = requestedTier; this.setPlanMode(next.plan_mode === undefined ? this.planModeEnabled : next.plan_mode === true); this.cachedRuntimeInfo = null;