From 4d289eb3a6e9e5fcffed1de15af06f7f32868b49 Mon Sep 17 00:00:00 2001 From: Slowper Date: Tue, 6 Oct 2026 16:13:56 +0530 Subject: [PATCH] fix(codex): omit collaboration mode until a model is known Codex 0.160 rejects turn/start when settings.model is null. --- .../providers/codex/codexProtocol.test.ts | 22 ++++++++++++++++ .../harness/providers/codex/codexProtocol.ts | 26 ++++++++++++------- 2 files changed, 39 insertions(+), 9 deletions(-) diff --git a/src/integrations/harness/providers/codex/codexProtocol.test.ts b/src/integrations/harness/providers/codex/codexProtocol.test.ts index 4602559cc..6aacb3e3f 100644 --- a/src/integrations/harness/providers/codex/codexProtocol.test.ts +++ b/src/integrations/harness/providers/codex/codexProtocol.test.ts @@ -157,6 +157,28 @@ describe("buildThreadStartParams / buildTurnStartParams", () => { }); }); + it("leaves the model to Codex before one has been selected", () => { + const params = buildTurnStartParams({ + threadId: "t", + runtimeMode: "auto", + model: "", + }); + expect(params).not.toHaveProperty("model"); + expect(params).not.toHaveProperty("collaborationMode"); + }); + + it("does not send a null collaboration model for a plan turn either", () => { + const params = buildTurnStartParams({ + threadId: "t", + runtimeMode: "auto", + model: "", + intent: "plan", + }); + expect(params).not.toHaveProperty("collaborationMode"); + expect(params.sandboxPolicy).toEqual({ type: "readOnly" }); + expect(params.approvalPolicy).toBe("never"); + }); + it("uses native plan mode with a non-escalating read-only sandbox", () => { const turn = buildTurnStartParams({ threadId: "thr_1", diff --git a/src/integrations/harness/providers/codex/codexProtocol.ts b/src/integrations/harness/providers/codex/codexProtocol.ts index b46f69c9c..691e178c2 100644 --- a/src/integrations/harness/providers/codex/codexProtocol.ts +++ b/src/integrations/harness/providers/codex/codexProtocol.ts @@ -158,21 +158,29 @@ export function buildTurnStartParams(input: { input.controlsAgents, ) : runtimeConfig; + // settings.model is a required string. Null is rejected + // ("invalid type: null, expected a string") and omitting it is + // "missing field `model`". Skip the override until a model is known + // so Codex keeps the one it chose when the thread started. + const model = input.model?.trim() ?? ""; + const collaborationMode = model + ? { + mode: input.intent === "plan" ? "plan" : "default", + settings: { + model, + reasoning_effort: input.effort ?? null, + developer_instructions: null, + }, + } + : undefined; return { threadId: input.threadId, input: codexInput(input.prompt, input.attachments), approvalPolicy: config.approvalPolicy, approvalsReviewer: config.approvalsReviewer, sandboxPolicy: config.sandboxPolicy, - collaborationMode: { - mode: input.intent === "plan" ? "plan" : "default", - settings: { - model: input.model ?? null, - reasoning_effort: input.effort ?? null, - developer_instructions: null, - }, - }, - ...(input.model ? { model: input.model } : {}), + ...(collaborationMode ? { collaborationMode } : {}), + ...(model ? { model } : {}), ...(input.effort ? { effort: input.effort } : {}), ...(input.serviceTier && input.serviceTier !== "default" ? { serviceTier: input.serviceTier }