diff --git a/packages/server/src/server/agent/providers/codex-app-server-agent.test.ts b/packages/server/src/server/agent/providers/codex-app-server-agent.test.ts index d4a96b45e6..8437420340 100644 --- a/packages/server/src/server/agent/providers/codex-app-server-agent.test.ts +++ b/packages/server/src/server/agent/providers/codex-app-server-agent.test.ts @@ -1117,6 +1117,42 @@ describe("Codex app-server provider", () => { }); }); + test("starts a session without a model on the project's Codex model and effort", async () => { + const cwd = "/workspace/project"; + const appServer = createFakeCodexAppServer({ + "config/read": (params) => ({ + config: + (params as { cwd?: unknown }).cwd === cwd + ? { model: "project-model", model_reasoning_effort: "high" } + : { model: "global-model", model_reasoning_effort: "low" }, + }), + getUserSavedConfig: () => ({ + config: { model: "global-model", modelReasoningEffort: "low" }, + }), + }); + const session = new CodexAppServerAgentSession( + createConfig({ cwd, model: undefined }), + null, + createTestLogger(), + async () => appServer.child, + ); + + try { + await session.startTurn("use project defaults"); + + await expect(appServer.waitForRequest("thread/start")).resolves.toMatchObject({ + model: "project-model", + }); + await expect(appServer.waitForTurnStart()).resolves.toMatchObject({ + model: "project-model", + effort: "high", + }); + appServer.assertNoErrors(); + } finally { + await session.close(); + } + }); + test("preserves cwd-resolved Codex writable roots under an explicit workflow mode", async () => { const appServer = createFakeCodexAppServer({ "config/read": () => ({ 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 55d7886df8..ac927a8656 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 @@ -3100,10 +3100,32 @@ const CodexNotificationSchema = z.union([ ), ]); +// config/read resolves the effective layered config, including a trusted project's +// .codex/config.toml when given its cwd; getUserSavedConfig predates it. async function readCodexConfiguredDefaults( client: CodexAppServerClient, logger: Logger, + cwd: string | null, ): Promise { + let configReadDefaults: CodexConfiguredDefaults = {}; + try { + const response = toObjectRecord(await client.request("config/read", { cwd })); + const config = toObjectRecord(response?.config); + const modelValue = typeof config?.model === "string" ? config.model : undefined; + const thinkingOptionValue = + typeof config?.model_reasoning_effort === "string" ? config.model_reasoning_effort : null; + configReadDefaults = { + model: normalizeCodexModelId(modelValue), + thinkingOptionId: normalizeCodexThinkingOptionId(thinkingOptionValue), + }; + } catch (error) { + logger.debug({ error }, "Failed to read Codex config defaults"); + } + + if (configReadDefaults.model && configReadDefaults.thinkingOptionId) { + return configReadDefaults; + } + let savedConfigDefaults: CodexConfiguredDefaults = {}; try { const response = toObjectRecord(await client.request("getUserSavedConfig", {})); @@ -3119,26 +3141,7 @@ async function readCodexConfiguredDefaults( logger.debug({ error }, "Failed to read Codex saved config defaults"); } - if (savedConfigDefaults.model && savedConfigDefaults.thinkingOptionId) { - return savedConfigDefaults; - } - - let configReadDefaults: CodexConfiguredDefaults = {}; - try { - const response = toObjectRecord(await client.request("config/read", {})); - const config = toObjectRecord(response?.config); - const modelValue = typeof config?.model === "string" ? config.model : undefined; - const thinkingOptionValue = - typeof config?.model_reasoning_effort === "string" ? config.model_reasoning_effort : null; - configReadDefaults = { - model: normalizeCodexModelId(modelValue), - thinkingOptionId: normalizeCodexThinkingOptionId(thinkingOptionValue), - }; - } catch (error) { - logger.debug({ error }, "Failed to read Codex config defaults"); - } - - return mergeCodexConfiguredDefaults(savedConfigDefaults, configReadDefaults); + return mergeCodexConfiguredDefaults(configReadDefaults, savedConfigDefaults); } interface CodexSkillPromptBlock { @@ -5119,7 +5122,11 @@ export class CodexAppServerAgentSession implements AgentSession { let model = this.config.model; let thinkingOptionId = normalizeCodexThinkingOptionId(this.config.thinkingOptionId); if (!model || !thinkingOptionId) { - configuredDefaults = await readCodexConfiguredDefaults(this.client, this.logger); + configuredDefaults = await readCodexConfiguredDefaults( + this.client, + this.logger, + this.config.cwd ?? null, + ); } if (!model) { model = configuredDefaults.model; @@ -7323,7 +7330,7 @@ export class CodexAppServerAgentClient implements AgentClient { const parsedResponse = CodexModelListResponseSchema.safeParse(rawResponse); const models = parsedResponse.success ? (parsedResponse.data.data ?? []) : []; const configuredDefaults = await runProviderRefreshActivity(context, "config/read", () => - readCodexConfiguredDefaults(client!, this.logger), + readCodexConfiguredDefaults(client!, this.logger, null), ); const configuredDefaultModelId = configuredDefaults.model; const configuredDefaultThinkingOptionId = configuredDefaults.thinkingOptionId;