diff --git a/packages/server/src/server/agent/providers/claude/agent.test.ts b/packages/server/src/server/agent/providers/claude/agent.test.ts index ecfc29cd30..7ed7d359e5 100644 --- a/packages/server/src/server/agent/providers/claude/agent.test.ts +++ b/packages/server/src/server/agent/providers/claude/agent.test.ts @@ -867,6 +867,34 @@ describe("ClaudeAgentSession features", () => { await session.close(); }); + test("applying options before the first query keeps the session's restrictions", async () => { + const { queryFactory } = createQueryMock(); + const client = new ClaudeAgentClient({ + logger, + queryFactory, + resolveBinary: async () => "/test/claude/bin", + }); + const session = await client.createSession({ + provider: "claude", + cwd: process.cwd(), + model: "claude-opus-4-8", + providerOptions: { + disallowedTools: ["Bash"], + sandbox: { enabled: true, failIfUnavailable: true }, + }, + }); + + // The manager hands the session only the agent's own options. + await session.applyOptions?.({ fast_mode: true }); + await (session as unknown as { ensureQuery(): Promise }).ensureQuery(); + + expect(queryFactory.mock.calls[0]?.[0].options).toMatchObject({ + disallowedTools: ["Bash"], + sandbox: { enabled: true, failIfUnavailable: true }, + }); + await session.close(); + }); + test("passes extra Claude Code CLI arguments to the SDK", async () => { const { queryFactory, launches } = createQueryMock(); const client = new ClaudeAgentClient({ diff --git a/packages/server/src/server/agent/providers/claude/agent.ts b/packages/server/src/server/agent/providers/claude/agent.ts index de70858db5..6d483aa2b6 100644 --- a/packages/server/src/server/agent/providers/claude/agent.ts +++ b/packages/server/src/server/agent/providers/claude/agent.ts @@ -2800,9 +2800,10 @@ class ClaudeAgentSession implements AgentSession { async applyOptions(options: ProviderOptions): Promise { const providerOptions = validateProviderOptions("claude", ClaudeProviderOptionsSchema, options) ?? {}; + // Supplied keys update the running options; absent keys keep their running value. + const running = { ...this.config.providerOptions, ...providerOptions }; const hadFast = this.config.providerOptions.fast_mode === true; - // An absent option leaves Fast as it is; only an explicit value changes it. - const wantsFast = providerOptions.fast_mode === undefined ? hadFast : providerOptions.fast_mode; + const wantsFast = running.fast_mode === true; const modelSupportsFast = claudeModelSupportsFastMode(this.config.model); if (wantsFast && !hadFast && !modelSupportsFast) { throw new Error( @@ -2817,7 +2818,7 @@ class ClaudeAgentSession implements AgentSession { this.cachedRuntimeInfo = null; } // Held until the runtime accepts, so a rejected change is retried instead of skipped. - this.config.providerOptions = { ...providerOptions, ...(wantsFast ? { fast_mode: true } : {}) }; + this.config.providerOptions = running; } getPendingPermissions(): AgentPermissionRequest[] { 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 01c2b87ce9..d01fe5e739 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 @@ -559,6 +559,18 @@ describe("Codex app-server provider controls", () => { }); }); + test("an explicit tier still beats a legacy fast_mode on a patch that supplies neither", async () => { + const { session, appServer } = await createConnectedSession({ + providerOptions: { service_tier: "default", fast_mode: true }, + }); + + // The manager hands the session only the agent's own options. + await session.applyOptions?.({ fast_mode: true, plan_mode: true }); + await session.startTurn("hello"); + + await expect(appServer.waitForTurnStart()).resolves.toMatchObject({ serviceTier: "default" }); + }); + 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 40315c05b3..052fca7cc7 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 @@ -4661,8 +4661,11 @@ export class CodexAppServerAgentSession implements AgentSession { async applyOptions(options: ProviderOptions): Promise { const next = validateProviderOptions("codex", CodexProviderOptionsSchema, options) ?? {}; // An absent speed or plan option leaves the session as it is. + // Supplied keys update the running options; absent keys keep their running value, so an + // explicit tier still beats a legacy `fast_mode` on a patch that supplies neither. + const running = { ...this.providerOptions, ...next }; const speedGiven = next.service_tier !== undefined || next.fast_mode !== undefined; - const requestedTier = speedGiven ? readCodexServiceTier(next) : this.requestedServiceTier; + const requestedTier = speedGiven ? readCodexServiceTier(running) : this.requestedServiceTier; if ( requestedTier !== this.requestedServiceTier && !this.isServiceTierAvailable(requestedTier) @@ -4671,8 +4674,7 @@ export class CodexAppServerAgentSession implements AgentSession { `Codex speed '${String(requestedTier)}' is not available for model '${this.config.model ?? "default"}'`, ); } - // Supplied keys update the running options; absent keys keep their running value. - this.providerOptions = { ...this.providerOptions, ...next }; + this.providerOptions = running; this.requestedServiceTier = requestedTier; this.setPlanMode(next.plan_mode === undefined ? this.planModeEnabled : next.plan_mode === true); this.cachedRuntimeInfo = null;