authorgravatar for 142016413+chuks-qua@users.noreply.github.com(CJ) Chukwudi Nwobodo <142016413+chuks-qua@users.noreply.github.com> 2026-03-08 22:55:49+00:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-03-08 15:55:49-07:00
loga7ec0179fb42b12ab7e1fd4c2cabd508c8f8de67
tree5d97745b5e18e9b6b39ff6ea2de9fa640dcf5897
parentc97c6b7836ce888b24a157de8eb4aea761028979
signature Signed by PGP key B5690EEEBB952194

Fix codex binary path setting having no effect (#493)

* refactor(contracts): export ProviderStartOptions schema * feat(contracts): add providerOptions to ThreadTurnStartCommand * feat(web): send codex binary/home path settings to server * feat(server): thread providerOptions through to session startup * 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. * 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

7 files changed, 79 insertions(+), 9 deletions(-)

apps/server/scripts/cli.ts+2
......@@ -132,6 +132,7 @@ const buildCmd = Command.make(
132132 cwd: serverDir,
133133 stdout: config.verbose ? "inherit" : "ignore",
134134 stderr: "inherit",
135 shell: process.platform === "win32",
135136 })`bun tsdown`,
136137 );
137138
......@@ -225,6 +226,7 @@ const publishCmd = Command.make(
225226 cwd: serverDir,
226227 stdout: config.verbose ? "inherit" : "ignore",
227228 stderr: "inherit",
229 shell: process.platform === "win32",
228230 }),
229231 );
230232 }),
apps/server/src/orchestration/Layers/ProviderCommandReactor.ts+15-1
......@@ -6,6 +6,7 @@ import {
66 type ProviderModelOptions,
77 type ProviderKind,
88 type ProviderServiceTier,
9 type ProviderStartOptions,
910 type OrchestrationSession,
1011 ThreadId,
1112 type ProviderSession,
......@@ -144,6 +145,8 @@ const make = Effect.gen(function* () {
144145 ),
145146 );
146147
148 const threadProviderOptions = new Map<string, ProviderStartOptions>();
149
147150 const appendProviderFailureActivity = (input: {
148151 readonly threadId: ThreadId;
149152 readonly kind:
......@@ -203,6 +206,7 @@ const make = Effect.gen(function* () {
203206 readonly model?: string;
204207 readonly modelOptions?: ProviderModelOptions;
205208 readonly serviceTier?: ProviderServiceTier | null;
209 readonly providerOptions?: ProviderStartOptions;
206210 },
207211 ) {
208212 const readModel = yield* orchestrationEngine.getReadModel();
......@@ -239,6 +243,7 @@ const make = Effect.gen(function* () {
239243 ...(desiredModel ? { model: desiredModel } : {}),
240244 ...(options?.serviceTier !== undefined ? { serviceTier: options.serviceTier } : {}),
241245 ...(options?.modelOptions !== undefined ? { modelOptions: options.modelOptions } : {}),
246 ...(options?.providerOptions !== undefined ? { providerOptions: options.providerOptions } : {}),
242247 ...(input?.resumeCursor !== undefined ? { resumeCursor: input.resumeCursor } : {}),
243248 runtimeMode: desiredRuntimeMode,
244249 });
......@@ -325,6 +330,7 @@ const make = Effect.gen(function* () {
325330 readonly model?: string;
326331 readonly serviceTier?: ProviderServiceTier | null;
327332 readonly modelOptions?: ProviderModelOptions;
333 readonly providerOptions?: ProviderStartOptions;
328334 readonly interactionMode?: "default" | "plan";
329335 readonly createdAt: string;
330336 }) {
......@@ -332,11 +338,15 @@ const make = Effect.gen(function* () {
332338 if (!thread) {
333339 return;
334340 }
341 if (input.providerOptions !== undefined) {
342 threadProviderOptions.set(input.threadId, input.providerOptions);
343 }
335344 yield* ensureSessionForThread(input.threadId, input.createdAt, {
336345 ...(input.provider !== undefined ? { provider: input.provider } : {}),
337346 ...(input.model !== undefined ? { model: input.model } : {}),
338347 ...(input.serviceTier !== undefined ? { serviceTier: input.serviceTier } : {}),
339348 ...(input.modelOptions !== undefined ? { modelOptions: input.modelOptions } : {}),
349 ...(input.providerOptions !== undefined ? { providerOptions: input.providerOptions } : {}),
340350 });
341351 const normalizedInput = toNonEmptyProviderInput(input.messageText);
342352 const normalizedAttachments = input.attachments ?? [];
......@@ -472,6 +482,7 @@ const make = Effect.gen(function* () {
472482 ...(event.payload.model !== undefined ? { model: event.payload.model } : {}),
473483 ...(event.payload.serviceTier !== undefined ? { serviceTier: event.payload.serviceTier } : {}),
474484 ...(event.payload.modelOptions !== undefined ? { modelOptions: event.payload.modelOptions } : {}),
485 ...(event.payload.providerOptions !== undefined ? { providerOptions: event.payload.providerOptions } : {}),
475486 interactionMode: event.payload.interactionMode,
476487 createdAt: event.payload.createdAt,
477488 });
......@@ -627,7 +638,10 @@ const make = Effect.gen(function* () {
627638 if (!thread?.session || thread.session.status === "stopped") {
628639 return;
629640 }
630 yield* ensureSessionForThread(event.payload.threadId, event.occurredAt);
641 const cachedProviderOptions = threadProviderOptions.get(event.payload.threadId);
642 yield* ensureSessionForThread(event.payload.threadId, event.occurredAt, {
643 ...(cachedProviderOptions !== undefined ? { providerOptions: cachedProviderOptions } : {}),
644 });
631645 return;
632646 }
633647 case "thread.turn-start-requested":
apps/server/src/orchestration/decider.ts+1
......@@ -303,6 +303,7 @@ export const decideOrchestrationCommand = Effect.fn("decideOrchestrationCommand"
303303 ...(command.model !== undefined ? { model: command.model } : {}),
304304 ...(command.serviceTier !== undefined ? { serviceTier: command.serviceTier } : {}),
305305 ...(command.modelOptions !== undefined ? { modelOptions: command.modelOptions } : {}),
306 ...(command.providerOptions !== undefined ? { providerOptions: command.providerOptions } : {}),
306307 assistantDeliveryMode: command.assistantDeliveryMode ?? DEFAULT_ASSISTANT_DELIVERY_MODE,
307308 runtimeMode:
308309 readModel.threads.find((entry) => entry.id === command.threadId)?.runtimeMode ??
apps/server/src/provider/Layers/ProviderService.ts+23-3
......@@ -86,15 +86,30 @@ function toRuntimeStatus(session: ProviderSession): "starting" | "running" | "st
8686 }
8787}
8888
89function toRuntimePayloadFromSession(session: ProviderSession): Record<string, unknown> {
89function toRuntimePayloadFromSession(
90 session: ProviderSession,
91 extra?: { readonly providerOptions?: unknown },
92): Record<string, unknown> {
9093 return {
9194 cwd: session.cwd ?? null,
9295 model: session.model ?? null,
9396 activeTurnId: session.activeTurnId ?? null,
9497 lastError: session.lastError ?? null,
98 ...(extra?.providerOptions !== undefined ? { providerOptions: extra.providerOptions } : {}),
9599 };
96100}
97101
102function readPersistedProviderOptions(
103 runtimePayload: ProviderRuntimeBinding["runtimePayload"],
104): Record<string, unknown> | undefined {
105 if (!runtimePayload || typeof runtimePayload !== "object" || Array.isArray(runtimePayload)) {
106 return undefined;
107 }
108 const raw = "providerOptions" in runtimePayload ? runtimePayload.providerOptions : undefined;
109 if (!raw || typeof raw !== "object" || Array.isArray(raw)) return undefined;
110 return raw as Record<string, unknown>;
111}
112
98113function readPersistedCwd(
99114 runtimePayload: ProviderRuntimeBinding["runtimePayload"],
100115): string | undefined {
......@@ -137,6 +152,7 @@ const makeProviderService = (options?: ProviderServiceLiveOptions) =>
137152 const upsertSessionBinding = (
138153 session: ProviderSession,
139154 threadId: ThreadId,
155 extra?: { readonly providerOptions?: unknown },
140156 ) =>
141157 directory.upsert({
142158 threadId,
......@@ -144,7 +160,7 @@ const makeProviderService = (options?: ProviderServiceLiveOptions) =>
144160 runtimeMode: session.runtimeMode,
145161 status: toRuntimeStatus(session),
146162 ...(session.resumeCursor !== undefined ? { resumeCursor: session.resumeCursor } : {}),
147 runtimePayload: toRuntimePayloadFromSession(session),
163 runtimePayload: toRuntimePayloadFromSession(session, extra),
148164 });
149165
150166 const providers = yield* registry.listProviders();
......@@ -197,11 +213,13 @@ const makeProviderService = (options?: ProviderServiceLiveOptions) =>
197213 }
198214
199215 const persistedCwd = readPersistedCwd(input.binding.runtimePayload);
216 const persistedProviderOptions = readPersistedProviderOptions(input.binding.runtimePayload);
200217
201218 const resumed = yield* adapter.startSession({
202219 threadId: input.binding.threadId,
203220 provider: input.binding.provider,
204221 ...(persistedCwd ? { cwd: persistedCwd } : {}),
222 ...(persistedProviderOptions ? { providerOptions: persistedProviderOptions } : {}),
205223 ...(hasResumeCursor ? { resumeCursor: input.binding.resumeCursor } : {}),
206224 runtimeMode: input.binding.runtimeMode ?? "full-access",
207225 });
......@@ -273,7 +291,9 @@ const makeProviderService = (options?: ProviderServiceLiveOptions) =>
273291 );
274292 }
275293
276 yield* upsertSessionBinding(session, threadId);
294 yield* upsertSessionBinding(session, threadId, {
295 ...(input.providerOptions !== undefined ? { providerOptions: input.providerOptions } : {}),
296 });
277297 yield* analytics.record("provider.session.started", {
278298 provider: session.provider,
279299 runtimeMode: input.runtimeMode,
apps/web/src/components/ChatView.tsx+26-4
......@@ -830,6 +830,17 @@ export default function ChatView({ threadId }: ChatViewProps) {
830830 };
831831 return Object.keys(codexOptions).length > 0 ? { codex: codexOptions } : undefined;
832832 }, [selectedCodexFastModeEnabled, selectedEffort, selectedProvider, supportsReasoningEffort]);
833 const providerOptionsForDispatch = useMemo(() => {
834 if (!settings.codexBinaryPath && !settings.codexHomePath) {
835 return undefined;
836 }
837 return {
838 codex: {
839 ...(settings.codexBinaryPath ? { binaryPath: settings.codexBinaryPath } : {}),
840 ...(settings.codexHomePath ? { homePath: settings.codexHomePath } : {}),
841 },
842 };
843 }, [settings.codexBinaryPath, settings.codexHomePath]);
833844 const selectedModelForPicker = selectedModel;
834845 const modelOptionsByProvider = useMemo(
835846 () => getCustomModelOptionsByProvider(settings),
......@@ -2626,6 +2637,9 @@ export default function ChatView({ threadId }: ChatViewProps) {
26262637 ...(selectedModelOptionsForDispatch
26272638 ? { modelOptions: selectedModelOptionsForDispatch }
26282639 : {}),
2640 ...(providerOptionsForDispatch
2641 ? { providerOptions: providerOptionsForDispatch }
2642 : {}),
26292643 provider: selectedProvider,
26302644 assistantDeliveryMode: settings.enableAssistantStreaming ? "streaming" : "buffered",
26312645 runtimeMode,
......@@ -2903,6 +2917,9 @@ export default function ChatView({ threadId }: ChatViewProps) {
29032917 ...(selectedModelOptionsForDispatch
29042918 ? { modelOptions: selectedModelOptionsForDispatch }
29052919 : {}),
2920 ...(providerOptionsForDispatch
2921 ? { providerOptions: providerOptionsForDispatch }
2922 : {}),
29062923 assistantDeliveryMode: settings.enableAssistantStreaming ? "streaming" : "buffered",
29072924 runtimeMode,
29082925 interactionMode: nextInteractionMode,
......@@ -2933,6 +2950,7 @@ export default function ChatView({ threadId }: ChatViewProps) {
29332950 runtimeMode,
29342951 selectedModel,
29352952 selectedModelOptionsForDispatch,
2953 providerOptionsForDispatch,
29362954 selectedProvider,
29372955 setComposerDraftInteractionMode,
29382956 setThreadError,
......@@ -2987,8 +3005,8 @@ export default function ChatView({ threadId }: ChatViewProps) {
29873005 worktreePath: activeThread.worktreePath,
29883006 createdAt,
29893007 })
2990 .then(() =>
2991 api.orchestration.dispatchCommand({
3008 .then(() => {
3009 return api.orchestration.dispatchCommand({
29923010 type: "thread.turn.start",
29933011 commandId: newCommandId(),
29943012 threadId: nextThreadId,
......@@ -3003,12 +3021,15 @@ export default function ChatView({ threadId }: ChatViewProps) {
30033021 ...(selectedModelOptionsForDispatch
30043022 ? { modelOptions: selectedModelOptionsForDispatch }
30053023 : {}),
3024 ...(providerOptionsForDispatch
3025 ? { providerOptions: providerOptionsForDispatch }
3026 : {}),
30063027 assistantDeliveryMode: settings.enableAssistantStreaming ? "streaming" : "buffered",
30073028 runtimeMode,
30083029 interactionMode: "default",
30093030 createdAt,
3010 }),
3011 )
3031 });
3032 })
30123033 .then(() => api.orchestration.getSnapshot())
30133034 .then((snapshot) => {
30143035 syncServerReadModel(snapshot);
......@@ -3052,6 +3073,7 @@ export default function ChatView({ threadId }: ChatViewProps) {
30523073 runtimeMode,
30533074 selectedModel,
30543075 selectedModelOptionsForDispatch,
3076 providerOptionsForDispatch,
30553077 selectedProvider,
30563078 settings.enableAssistantStreaming,
30573079 syncServerReadModel,
packages/contracts/src/orchestration.ts+10
......@@ -45,6 +45,13 @@ export type ProviderSandboxMode = typeof ProviderSandboxMode.Type;
4545export const ProviderServiceTier = Schema.Literals(["fast", "flex"]);
4646export type ProviderServiceTier = typeof ProviderServiceTier.Type;
4747export const DEFAULT_PROVIDER_KIND: ProviderKind = "codex";
48const CodexProviderStartOptions = Schema.Struct({
49 binaryPath: Schema.optional(TrimmedNonEmptyString),
50 homePath: Schema.optional(TrimmedNonEmptyString),
51});
52const ProviderStartOptions = Schema.Struct({
53 codex: Schema.optional(CodexProviderStartOptions),
54});
4855export const RuntimeMode = Schema.Literals(["approval-required", "full-access"]);
4956export type RuntimeMode = typeof RuntimeMode.Type;
5057export const DEFAULT_RUNTIME_MODE: RuntimeMode = "full-access";
......@@ -366,6 +373,7 @@ export const ThreadTurnStartCommand = Schema.Struct({
366373 model: Schema.optional(TrimmedNonEmptyString),
367374 serviceTier: Schema.optional(Schema.NullOr(ProviderServiceTier)),
368375 modelOptions: Schema.optional(ProviderModelOptions),
376 providerOptions: Schema.optional(ProviderStartOptions),
369377 assistantDeliveryMode: Schema.optional(AssistantDeliveryMode),
370378 runtimeMode: RuntimeMode.pipe(Schema.withDecodingDefault(() => DEFAULT_RUNTIME_MODE)),
371379 interactionMode: ProviderInteractionMode.pipe(
......@@ -388,6 +396,7 @@ const ClientThreadTurnStartCommand = Schema.Struct({
388396 model: Schema.optional(TrimmedNonEmptyString),
389397 serviceTier: Schema.optional(Schema.NullOr(ProviderServiceTier)),
390398 modelOptions: Schema.optional(ProviderModelOptions),
399 providerOptions: Schema.optional(ProviderStartOptions),
391400 assistantDeliveryMode: Schema.optional(AssistantDeliveryMode),
392401 runtimeMode: RuntimeMode,
393402 interactionMode: ProviderInteractionMode,
......@@ -668,6 +677,7 @@ export const ThreadTurnStartRequestedPayload = Schema.Struct({
668677 model: Schema.optional(TrimmedNonEmptyString),
669678 serviceTier: Schema.optional(Schema.NullOr(ProviderServiceTier)),
670679 modelOptions: Schema.optional(ProviderModelOptions),
680 providerOptions: Schema.optional(ProviderStartOptions),
671681 assistantDeliveryMode: Schema.optional(AssistantDeliveryMode),
672682 runtimeMode: RuntimeMode.pipe(Schema.withDecodingDefault(() => DEFAULT_RUNTIME_MODE)),
673683 interactionMode: ProviderInteractionMode.pipe(
packages/contracts/src/provider.ts+2-1
......@@ -53,9 +53,10 @@ const CodexProviderStartOptions = Schema.Struct({
5353 homePath: Schema.optional(TrimmedNonEmptyStringSchema),
5454});
5555
56const ProviderStartOptions = Schema.Struct({
56export const ProviderStartOptions = Schema.Struct({
5757 codex: Schema.optional(CodexProviderStartOptions),
5858});
59export type ProviderStartOptions = typeof ProviderStartOptions.Type;
5960
6061export const ProviderSessionStartInput = Schema.Struct({
6162 threadId: ThreadId,