From 7de8559fdcc5314b1ae8cdb4aff4d12c05b03281 Mon Sep 17 00:00:00 2001 From: Chukwudi Nwobodo Date: Sun, 8 Mar 2026 03:11:15 +0000 Subject: [PATCH 1/6] refactor(contracts): export ProviderStartOptions schema --- packages/contracts/src/provider.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/contracts/src/provider.ts b/packages/contracts/src/provider.ts index 9ca7068ad3..8bb5ecf0a1 100644 --- a/packages/contracts/src/provider.ts +++ b/packages/contracts/src/provider.ts @@ -53,7 +53,7 @@ const CodexProviderStartOptions = Schema.Struct({ homePath: Schema.optional(TrimmedNonEmptyStringSchema), }); -const ProviderStartOptions = Schema.Struct({ +export const ProviderStartOptions = Schema.Struct({ codex: Schema.optional(CodexProviderStartOptions), }); From c430590cffa07403f97f64f963ae75a0afada725 Mon Sep 17 00:00:00 2001 From: Chukwudi Nwobodo Date: Sun, 8 Mar 2026 03:15:31 +0000 Subject: [PATCH 2/6] feat(contracts): add providerOptions to ThreadTurnStartCommand --- packages/contracts/src/orchestration.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/contracts/src/orchestration.ts b/packages/contracts/src/orchestration.ts index aa7bd827de..9e84cb53bc 100644 --- a/packages/contracts/src/orchestration.ts +++ b/packages/contracts/src/orchestration.ts @@ -1,5 +1,6 @@ import { Option, Schema, SchemaIssue, Struct } from "effect"; import { ProviderModelOptions } from "./model"; +import { ProviderStartOptions } from "./provider"; import { ApprovalRequestId, CheckpointRef, @@ -366,6 +367,7 @@ export const ThreadTurnStartCommand = Schema.Struct({ model: Schema.optional(TrimmedNonEmptyString), serviceTier: Schema.optional(Schema.NullOr(ProviderServiceTier)), modelOptions: Schema.optional(ProviderModelOptions), + providerOptions: Schema.optional(ProviderStartOptions), assistantDeliveryMode: Schema.optional(AssistantDeliveryMode), runtimeMode: RuntimeMode.pipe(Schema.withDecodingDefault(() => DEFAULT_RUNTIME_MODE)), interactionMode: ProviderInteractionMode.pipe( @@ -388,6 +390,7 @@ const ClientThreadTurnStartCommand = Schema.Struct({ model: Schema.optional(TrimmedNonEmptyString), serviceTier: Schema.optional(Schema.NullOr(ProviderServiceTier)), modelOptions: Schema.optional(ProviderModelOptions), + providerOptions: Schema.optional(ProviderStartOptions), assistantDeliveryMode: Schema.optional(AssistantDeliveryMode), runtimeMode: RuntimeMode, interactionMode: ProviderInteractionMode, From d70eda92bdcd4fa2a26d0a0feea6178c2a0f6679 Mon Sep 17 00:00:00 2001 From: Chukwudi Nwobodo Date: Sun, 8 Mar 2026 03:23:15 +0000 Subject: [PATCH 3/6] feat(web): send codex binary/home path settings to server --- apps/web/src/components/ChatView.tsx | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/apps/web/src/components/ChatView.tsx b/apps/web/src/components/ChatView.tsx index c1693f4199..ea03765457 100644 --- a/apps/web/src/components/ChatView.tsx +++ b/apps/web/src/components/ChatView.tsx @@ -2611,6 +2611,15 @@ export default function ChatView({ threadId }: ChatViewProps) { beginSendPhase("sending-turn"); const turnAttachments = await turnAttachmentsPromise; + const providerOptionsForDispatch = + settings.codexBinaryPath || settings.codexHomePath + ? { + codex: { + ...(settings.codexBinaryPath ? { binaryPath: settings.codexBinaryPath } : {}), + ...(settings.codexHomePath ? { homePath: settings.codexHomePath } : {}), + }, + } + : undefined; await api.orchestration.dispatchCommand({ type: "thread.turn.start", commandId: newCommandId(), @@ -2626,6 +2635,9 @@ export default function ChatView({ threadId }: ChatViewProps) { ...(selectedModelOptionsForDispatch ? { modelOptions: selectedModelOptionsForDispatch } : {}), + ...(providerOptionsForDispatch + ? { providerOptions: providerOptionsForDispatch } + : {}), provider: selectedProvider, assistantDeliveryMode: settings.enableAssistantStreaming ? "streaming" : "buffered", runtimeMode, From f0dc2d39e6c78a80b0b097223215e1fc3499f490 Mon Sep 17 00:00:00 2001 From: Chukwudi Nwobodo Date: Sun, 8 Mar 2026 03:30:54 +0000 Subject: [PATCH 4/6] feat(server): thread providerOptions through to session startup --- .../Layers/ProviderCommandReactor.ts | 6 ++++ apps/server/src/orchestration/decider.ts | 1 + apps/web/src/components/ChatView.tsx | 36 ++++++++++++------- packages/contracts/src/orchestration.ts | 1 + packages/contracts/src/provider.ts | 1 + 5 files changed, 32 insertions(+), 13 deletions(-) diff --git a/apps/server/src/orchestration/Layers/ProviderCommandReactor.ts b/apps/server/src/orchestration/Layers/ProviderCommandReactor.ts index d34791bc20..d4fdfc8432 100644 --- a/apps/server/src/orchestration/Layers/ProviderCommandReactor.ts +++ b/apps/server/src/orchestration/Layers/ProviderCommandReactor.ts @@ -6,6 +6,7 @@ import { type ProviderModelOptions, type ProviderKind, type ProviderServiceTier, + type ProviderStartOptions, type OrchestrationSession, ThreadId, type ProviderSession, @@ -203,6 +204,7 @@ const make = Effect.gen(function* () { readonly model?: string; readonly modelOptions?: ProviderModelOptions; readonly serviceTier?: ProviderServiceTier | null; + readonly providerOptions?: ProviderStartOptions; }, ) { const readModel = yield* orchestrationEngine.getReadModel(); @@ -239,6 +241,7 @@ const make = Effect.gen(function* () { ...(desiredModel ? { model: desiredModel } : {}), ...(options?.serviceTier !== undefined ? { serviceTier: options.serviceTier } : {}), ...(options?.modelOptions !== undefined ? { modelOptions: options.modelOptions } : {}), + ...(options?.providerOptions !== undefined ? { providerOptions: options.providerOptions } : {}), ...(input?.resumeCursor !== undefined ? { resumeCursor: input.resumeCursor } : {}), runtimeMode: desiredRuntimeMode, }); @@ -325,6 +328,7 @@ const make = Effect.gen(function* () { readonly model?: string; readonly serviceTier?: ProviderServiceTier | null; readonly modelOptions?: ProviderModelOptions; + readonly providerOptions?: ProviderStartOptions; readonly interactionMode?: "default" | "plan"; readonly createdAt: string; }) { @@ -337,6 +341,7 @@ const make = Effect.gen(function* () { ...(input.model !== undefined ? { model: input.model } : {}), ...(input.serviceTier !== undefined ? { serviceTier: input.serviceTier } : {}), ...(input.modelOptions !== undefined ? { modelOptions: input.modelOptions } : {}), + ...(input.providerOptions !== undefined ? { providerOptions: input.providerOptions } : {}), }); const normalizedInput = toNonEmptyProviderInput(input.messageText); const normalizedAttachments = input.attachments ?? []; @@ -472,6 +477,7 @@ const make = Effect.gen(function* () { ...(event.payload.model !== undefined ? { model: event.payload.model } : {}), ...(event.payload.serviceTier !== undefined ? { serviceTier: event.payload.serviceTier } : {}), ...(event.payload.modelOptions !== undefined ? { modelOptions: event.payload.modelOptions } : {}), + ...(event.payload.providerOptions !== undefined ? { providerOptions: event.payload.providerOptions } : {}), interactionMode: event.payload.interactionMode, createdAt: event.payload.createdAt, }); diff --git a/apps/server/src/orchestration/decider.ts b/apps/server/src/orchestration/decider.ts index f036416429..1c1ca4dd79 100644 --- a/apps/server/src/orchestration/decider.ts +++ b/apps/server/src/orchestration/decider.ts @@ -303,6 +303,7 @@ export const decideOrchestrationCommand = Effect.fn("decideOrchestrationCommand" ...(command.model !== undefined ? { model: command.model } : {}), ...(command.serviceTier !== undefined ? { serviceTier: command.serviceTier } : {}), ...(command.modelOptions !== undefined ? { modelOptions: command.modelOptions } : {}), + ...(command.providerOptions !== undefined ? { providerOptions: command.providerOptions } : {}), assistantDeliveryMode: command.assistantDeliveryMode ?? DEFAULT_ASSISTANT_DELIVERY_MODE, runtimeMode: readModel.threads.find((entry) => entry.id === command.threadId)?.runtimeMode ?? diff --git a/apps/web/src/components/ChatView.tsx b/apps/web/src/components/ChatView.tsx index ea03765457..70e1c8fa02 100644 --- a/apps/web/src/components/ChatView.tsx +++ b/apps/web/src/components/ChatView.tsx @@ -830,6 +830,17 @@ export default function ChatView({ threadId }: ChatViewProps) { }; return Object.keys(codexOptions).length > 0 ? { codex: codexOptions } : undefined; }, [selectedCodexFastModeEnabled, selectedEffort, selectedProvider, supportsReasoningEffort]); + const providerOptionsForDispatch = useMemo(() => { + if (!settings.codexBinaryPath && !settings.codexHomePath) { + return undefined; + } + return { + codex: { + ...(settings.codexBinaryPath ? { binaryPath: settings.codexBinaryPath } : {}), + ...(settings.codexHomePath ? { homePath: settings.codexHomePath } : {}), + }, + }; + }, [settings.codexBinaryPath, settings.codexHomePath]); const selectedModelForPicker = selectedModel; const modelOptionsByProvider = useMemo( () => getCustomModelOptionsByProvider(settings), @@ -2611,15 +2622,6 @@ export default function ChatView({ threadId }: ChatViewProps) { beginSendPhase("sending-turn"); const turnAttachments = await turnAttachmentsPromise; - const providerOptionsForDispatch = - settings.codexBinaryPath || settings.codexHomePath - ? { - codex: { - ...(settings.codexBinaryPath ? { binaryPath: settings.codexBinaryPath } : {}), - ...(settings.codexHomePath ? { homePath: settings.codexHomePath } : {}), - }, - } - : undefined; await api.orchestration.dispatchCommand({ type: "thread.turn.start", commandId: newCommandId(), @@ -2915,6 +2917,9 @@ export default function ChatView({ threadId }: ChatViewProps) { ...(selectedModelOptionsForDispatch ? { modelOptions: selectedModelOptionsForDispatch } : {}), + ...(providerOptionsForDispatch + ? { providerOptions: providerOptionsForDispatch } + : {}), assistantDeliveryMode: settings.enableAssistantStreaming ? "streaming" : "buffered", runtimeMode, interactionMode: nextInteractionMode, @@ -2945,6 +2950,7 @@ export default function ChatView({ threadId }: ChatViewProps) { runtimeMode, selectedModel, selectedModelOptionsForDispatch, + providerOptionsForDispatch, selectedProvider, setComposerDraftInteractionMode, setThreadError, @@ -2999,8 +3005,8 @@ export default function ChatView({ threadId }: ChatViewProps) { worktreePath: activeThread.worktreePath, createdAt, }) - .then(() => - api.orchestration.dispatchCommand({ + .then(() => { + return api.orchestration.dispatchCommand({ type: "thread.turn.start", commandId: newCommandId(), threadId: nextThreadId, @@ -3015,12 +3021,15 @@ export default function ChatView({ threadId }: ChatViewProps) { ...(selectedModelOptionsForDispatch ? { modelOptions: selectedModelOptionsForDispatch } : {}), + ...(providerOptionsForDispatch + ? { providerOptions: providerOptionsForDispatch } + : {}), assistantDeliveryMode: settings.enableAssistantStreaming ? "streaming" : "buffered", runtimeMode, interactionMode: "default", createdAt, - }), - ) + }); + }) .then(() => api.orchestration.getSnapshot()) .then((snapshot) => { syncServerReadModel(snapshot); @@ -3064,6 +3073,7 @@ export default function ChatView({ threadId }: ChatViewProps) { runtimeMode, selectedModel, selectedModelOptionsForDispatch, + providerOptionsForDispatch, selectedProvider, settings.enableAssistantStreaming, syncServerReadModel, diff --git a/packages/contracts/src/orchestration.ts b/packages/contracts/src/orchestration.ts index 9e84cb53bc..11af1f5b15 100644 --- a/packages/contracts/src/orchestration.ts +++ b/packages/contracts/src/orchestration.ts @@ -671,6 +671,7 @@ export const ThreadTurnStartRequestedPayload = Schema.Struct({ model: Schema.optional(TrimmedNonEmptyString), serviceTier: Schema.optional(Schema.NullOr(ProviderServiceTier)), modelOptions: Schema.optional(ProviderModelOptions), + providerOptions: Schema.optional(ProviderStartOptions), assistantDeliveryMode: Schema.optional(AssistantDeliveryMode), runtimeMode: RuntimeMode.pipe(Schema.withDecodingDefault(() => DEFAULT_RUNTIME_MODE)), interactionMode: ProviderInteractionMode.pipe( diff --git a/packages/contracts/src/provider.ts b/packages/contracts/src/provider.ts index 8bb5ecf0a1..5e551c5579 100644 --- a/packages/contracts/src/provider.ts +++ b/packages/contracts/src/provider.ts @@ -56,6 +56,7 @@ const CodexProviderStartOptions = Schema.Struct({ export const ProviderStartOptions = Schema.Struct({ codex: Schema.optional(CodexProviderStartOptions), }); +export type ProviderStartOptions = typeof ProviderStartOptions.Type; export const ProviderSessionStartInput = Schema.Struct({ threadId: ThreadId, From 7e06dc4b37a93782f9c86f6b92c31451c778eba3 Mon Sep 17 00:00:00 2001 From: Chukwudi Nwobodo Date: Sun, 8 Mar 2026 03:55:36 +0000 Subject: [PATCH 5/6] fix(contracts): define ProviderStartOptions inline to avoid circular dep The import from provider.ts caused a ReferenceError at runtime because of module initialization order in the bundled output. --- apps/server/scripts/cli.ts | 2 ++ packages/contracts/src/orchestration.ts | 8 +++++++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/apps/server/scripts/cli.ts b/apps/server/scripts/cli.ts index ccf1f5a91a..e5b5c0d00a 100644 --- a/apps/server/scripts/cli.ts +++ b/apps/server/scripts/cli.ts @@ -132,6 +132,7 @@ const buildCmd = Command.make( cwd: serverDir, stdout: config.verbose ? "inherit" : "ignore", stderr: "inherit", + shell: process.platform === "win32", })`bun tsdown`, ); @@ -225,6 +226,7 @@ const publishCmd = Command.make( cwd: serverDir, stdout: config.verbose ? "inherit" : "ignore", stderr: "inherit", + shell: process.platform === "win32", }), ); }), diff --git a/packages/contracts/src/orchestration.ts b/packages/contracts/src/orchestration.ts index 11af1f5b15..e90ddd4b59 100644 --- a/packages/contracts/src/orchestration.ts +++ b/packages/contracts/src/orchestration.ts @@ -1,6 +1,5 @@ import { Option, Schema, SchemaIssue, Struct } from "effect"; import { ProviderModelOptions } from "./model"; -import { ProviderStartOptions } from "./provider"; import { ApprovalRequestId, CheckpointRef, @@ -46,6 +45,13 @@ export type ProviderSandboxMode = typeof ProviderSandboxMode.Type; export const ProviderServiceTier = Schema.Literals(["fast", "flex"]); export type ProviderServiceTier = typeof ProviderServiceTier.Type; export const DEFAULT_PROVIDER_KIND: ProviderKind = "codex"; +const CodexProviderStartOptions = Schema.Struct({ + binaryPath: Schema.optional(TrimmedNonEmptyString), + homePath: Schema.optional(TrimmedNonEmptyString), +}); +const ProviderStartOptions = Schema.Struct({ + codex: Schema.optional(CodexProviderStartOptions), +}); export const RuntimeMode = Schema.Literals(["approval-required", "full-access"]); export type RuntimeMode = typeof RuntimeMode.Type; export const DEFAULT_RUNTIME_MODE: RuntimeMode = "full-access"; From ec7664eca4242491e13260fc7519f8752904cbec Mon Sep 17 00:00:00 2001 From: Chukwudi Nwobodo Date: Sun, 8 Mar 2026 05:44:17 +0000 Subject: [PATCH 6/6] fix(server): preserve providerOptions across runtime mode changes and session recovery Runtime mode toggles and server restarts silently dropped the custom codex binary/home path, falling back to the default binary. Cache providerOptions per thread in ProviderCommandReactor and persist it in the session binding's runtimePayload so recovery can restore it. Closes #486 --- .../Layers/ProviderCommandReactor.ts | 10 ++++++- .../src/provider/Layers/ProviderService.ts | 26 ++++++++++++++++--- 2 files changed, 32 insertions(+), 4 deletions(-) diff --git a/apps/server/src/orchestration/Layers/ProviderCommandReactor.ts b/apps/server/src/orchestration/Layers/ProviderCommandReactor.ts index d4fdfc8432..2a72d59027 100644 --- a/apps/server/src/orchestration/Layers/ProviderCommandReactor.ts +++ b/apps/server/src/orchestration/Layers/ProviderCommandReactor.ts @@ -145,6 +145,8 @@ const make = Effect.gen(function* () { ), ); + const threadProviderOptions = new Map(); + const appendProviderFailureActivity = (input: { readonly threadId: ThreadId; readonly kind: @@ -336,6 +338,9 @@ const make = Effect.gen(function* () { if (!thread) { return; } + if (input.providerOptions !== undefined) { + threadProviderOptions.set(input.threadId, input.providerOptions); + } yield* ensureSessionForThread(input.threadId, input.createdAt, { ...(input.provider !== undefined ? { provider: input.provider } : {}), ...(input.model !== undefined ? { model: input.model } : {}), @@ -633,7 +638,10 @@ const make = Effect.gen(function* () { if (!thread?.session || thread.session.status === "stopped") { return; } - yield* ensureSessionForThread(event.payload.threadId, event.occurredAt); + const cachedProviderOptions = threadProviderOptions.get(event.payload.threadId); + yield* ensureSessionForThread(event.payload.threadId, event.occurredAt, { + ...(cachedProviderOptions !== undefined ? { providerOptions: cachedProviderOptions } : {}), + }); return; } case "thread.turn-start-requested": diff --git a/apps/server/src/provider/Layers/ProviderService.ts b/apps/server/src/provider/Layers/ProviderService.ts index 398a26fb7b..05a1de1495 100644 --- a/apps/server/src/provider/Layers/ProviderService.ts +++ b/apps/server/src/provider/Layers/ProviderService.ts @@ -86,15 +86,30 @@ function toRuntimeStatus(session: ProviderSession): "starting" | "running" | "st } } -function toRuntimePayloadFromSession(session: ProviderSession): Record { +function toRuntimePayloadFromSession( + session: ProviderSession, + extra?: { readonly providerOptions?: unknown }, +): Record { return { cwd: session.cwd ?? null, model: session.model ?? null, activeTurnId: session.activeTurnId ?? null, lastError: session.lastError ?? null, + ...(extra?.providerOptions !== undefined ? { providerOptions: extra.providerOptions } : {}), }; } +function readPersistedProviderOptions( + runtimePayload: ProviderRuntimeBinding["runtimePayload"], +): Record | undefined { + if (!runtimePayload || typeof runtimePayload !== "object" || Array.isArray(runtimePayload)) { + return undefined; + } + const raw = "providerOptions" in runtimePayload ? runtimePayload.providerOptions : undefined; + if (!raw || typeof raw !== "object" || Array.isArray(raw)) return undefined; + return raw as Record; +} + function readPersistedCwd( runtimePayload: ProviderRuntimeBinding["runtimePayload"], ): string | undefined { @@ -137,6 +152,7 @@ const makeProviderService = (options?: ProviderServiceLiveOptions) => const upsertSessionBinding = ( session: ProviderSession, threadId: ThreadId, + extra?: { readonly providerOptions?: unknown }, ) => directory.upsert({ threadId, @@ -144,7 +160,7 @@ const makeProviderService = (options?: ProviderServiceLiveOptions) => runtimeMode: session.runtimeMode, status: toRuntimeStatus(session), ...(session.resumeCursor !== undefined ? { resumeCursor: session.resumeCursor } : {}), - runtimePayload: toRuntimePayloadFromSession(session), + runtimePayload: toRuntimePayloadFromSession(session, extra), }); const providers = yield* registry.listProviders(); @@ -197,11 +213,13 @@ const makeProviderService = (options?: ProviderServiceLiveOptions) => } const persistedCwd = readPersistedCwd(input.binding.runtimePayload); + const persistedProviderOptions = readPersistedProviderOptions(input.binding.runtimePayload); const resumed = yield* adapter.startSession({ threadId: input.binding.threadId, provider: input.binding.provider, ...(persistedCwd ? { cwd: persistedCwd } : {}), + ...(persistedProviderOptions ? { providerOptions: persistedProviderOptions } : {}), ...(hasResumeCursor ? { resumeCursor: input.binding.resumeCursor } : {}), runtimeMode: input.binding.runtimeMode ?? "full-access", }); @@ -273,7 +291,9 @@ const makeProviderService = (options?: ProviderServiceLiveOptions) => ); } - yield* upsertSessionBinding(session, threadId); + yield* upsertSessionBinding(session, threadId, { + ...(input.providerOptions !== undefined ? { providerOptions: input.providerOptions } : {}), + }); yield* analytics.record("provider.session.started", { provider: session.provider, runtimeMode: input.runtimeMode,