fix(server): Codex keeps unsupplied running options, snapshots show the live setting after a removal

- Codex applyOptions merges the supplied options into the running ones, so an
  unrelated patch or a removal no longer drops the sandbox and approval settings
  that were merged in when the session opened.
- A control the running session reports for a key the agent no longer holds
  shows its live value in the effective options, instead of the config.json
  default, so the snapshot and unattended-parent checks agree with the provider.
This commit is contained in:
Mohamed Boudra
2026-11-01 13:02:32 +01:00
parent 70a01a5c38
commit e4269f5059
4 changed files with 88 additions and 2 deletions
@@ -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<void> {
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<AgentSession> {
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();
@@ -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 {
@@ -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();
@@ -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;