| author | |
| committer | |
| log | a7ec0179fb42b12ab7e1fd4c2cabd508c8f8de67 |
| tree | 5d97745b5e18e9b6b39ff6ea2de9fa640dcf5897 |
| parent | c97c6b7836ce888b24a157de8eb4aea761028979 |
| signature | Signed by PGP key B5690EEEBB952194 |
* 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 #4867 files changed, 79 insertions(+), 9 deletions(-)
apps/server/scripts/cli.ts+2| ... | ... | @@ -132,6 +132,7 @@ const buildCmd = Command.make( |
| 132 | 132 | cwd: serverDir, |
| 133 | 133 | stdout: config.verbose ? "inherit" : "ignore", |
| 134 | 134 | stderr: "inherit", |
| 135 | shell: process.platform === "win32", | |
| 135 | 136 | })`bun tsdown`, |
| 136 | 137 | ); |
| 137 | 138 | |
| ... | ... | @@ -225,6 +226,7 @@ const publishCmd = Command.make( |
| 225 | 226 | cwd: serverDir, |
| 226 | 227 | stdout: config.verbose ? "inherit" : "ignore", |
| 227 | 228 | stderr: "inherit", |
| 229 | shell: process.platform === "win32", | |
| 228 | 230 | }), |
| 229 | 231 | ); |
| 230 | 232 | }), |
apps/server/src/orchestration/Layers/ProviderCommandReactor.ts+15-1| ... | ... | @@ -6,6 +6,7 @@ import { |
| 6 | 6 | type ProviderModelOptions, |
| 7 | 7 | type ProviderKind, |
| 8 | 8 | type ProviderServiceTier, |
| 9 | type ProviderStartOptions, | |
| 9 | 10 | type OrchestrationSession, |
| 10 | 11 | ThreadId, |
| 11 | 12 | type ProviderSession, |
| ... | ... | @@ -144,6 +145,8 @@ const make = Effect.gen(function* () { |
| 144 | 145 | ), |
| 145 | 146 | ); |
| 146 | 147 | |
| 148 | const threadProviderOptions = new Map<string, ProviderStartOptions>(); | |
| 149 | ||
| 147 | 150 | const appendProviderFailureActivity = (input: { |
| 148 | 151 | readonly threadId: ThreadId; |
| 149 | 152 | readonly kind: |
| ... | ... | @@ -203,6 +206,7 @@ const make = Effect.gen(function* () { |
| 203 | 206 | readonly model?: string; |
| 204 | 207 | readonly modelOptions?: ProviderModelOptions; |
| 205 | 208 | readonly serviceTier?: ProviderServiceTier | null; |
| 209 | readonly providerOptions?: ProviderStartOptions; | |
| 206 | 210 | }, |
| 207 | 211 | ) { |
| 208 | 212 | const readModel = yield* orchestrationEngine.getReadModel(); |
| ... | ... | @@ -239,6 +243,7 @@ const make = Effect.gen(function* () { |
| 239 | 243 | ...(desiredModel ? { model: desiredModel } : {}), |
| 240 | 244 | ...(options?.serviceTier !== undefined ? { serviceTier: options.serviceTier } : {}), |
| 241 | 245 | ...(options?.modelOptions !== undefined ? { modelOptions: options.modelOptions } : {}), |
| 246 | ...(options?.providerOptions !== undefined ? { providerOptions: options.providerOptions } : {}), | |
| 242 | 247 | ...(input?.resumeCursor !== undefined ? { resumeCursor: input.resumeCursor } : {}), |
| 243 | 248 | runtimeMode: desiredRuntimeMode, |
| 244 | 249 | }); |
| ... | ... | @@ -325,6 +330,7 @@ const make = Effect.gen(function* () { |
| 325 | 330 | readonly model?: string; |
| 326 | 331 | readonly serviceTier?: ProviderServiceTier | null; |
| 327 | 332 | readonly modelOptions?: ProviderModelOptions; |
| 333 | readonly providerOptions?: ProviderStartOptions; | |
| 328 | 334 | readonly interactionMode?: "default" | "plan"; |
| 329 | 335 | readonly createdAt: string; |
| 330 | 336 | }) { |
| ... | ... | @@ -332,11 +338,15 @@ const make = Effect.gen(function* () { |
| 332 | 338 | if (!thread) { |
| 333 | 339 | return; |
| 334 | 340 | } |
| 341 | if (input.providerOptions !== undefined) { | |
| 342 | threadProviderOptions.set(input.threadId, input.providerOptions); | |
| 343 | } | |
| 335 | 344 | yield* ensureSessionForThread(input.threadId, input.createdAt, { |
| 336 | 345 | ...(input.provider !== undefined ? { provider: input.provider } : {}), |
| 337 | 346 | ...(input.model !== undefined ? { model: input.model } : {}), |
| 338 | 347 | ...(input.serviceTier !== undefined ? { serviceTier: input.serviceTier } : {}), |
| 339 | 348 | ...(input.modelOptions !== undefined ? { modelOptions: input.modelOptions } : {}), |
| 349 | ...(input.providerOptions !== undefined ? { providerOptions: input.providerOptions } : {}), | |
| 340 | 350 | }); |
| 341 | 351 | const normalizedInput = toNonEmptyProviderInput(input.messageText); |
| 342 | 352 | const normalizedAttachments = input.attachments ?? []; |
| ... | ... | @@ -472,6 +482,7 @@ const make = Effect.gen(function* () { |
| 472 | 482 | ...(event.payload.model !== undefined ? { model: event.payload.model } : {}), |
| 473 | 483 | ...(event.payload.serviceTier !== undefined ? { serviceTier: event.payload.serviceTier } : {}), |
| 474 | 484 | ...(event.payload.modelOptions !== undefined ? { modelOptions: event.payload.modelOptions } : {}), |
| 485 | ...(event.payload.providerOptions !== undefined ? { providerOptions: event.payload.providerOptions } : {}), | |
| 475 | 486 | interactionMode: event.payload.interactionMode, |
| 476 | 487 | createdAt: event.payload.createdAt, |
| 477 | 488 | }); |
| ... | ... | @@ -627,7 +638,10 @@ const make = Effect.gen(function* () { |
| 627 | 638 | if (!thread?.session || thread.session.status === "stopped") { |
| 628 | 639 | return; |
| 629 | 640 | } |
| 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 | }); | |
| 631 | 645 | return; |
| 632 | 646 | } |
| 633 | 647 | case "thread.turn-start-requested": |
apps/server/src/orchestration/decider.ts+1| ... | ... | @@ -303,6 +303,7 @@ export const decideOrchestrationCommand = Effect.fn("decideOrchestrationCommand" |
| 303 | 303 | ...(command.model !== undefined ? { model: command.model } : {}), |
| 304 | 304 | ...(command.serviceTier !== undefined ? { serviceTier: command.serviceTier } : {}), |
| 305 | 305 | ...(command.modelOptions !== undefined ? { modelOptions: command.modelOptions } : {}), |
| 306 | ...(command.providerOptions !== undefined ? { providerOptions: command.providerOptions } : {}), | |
| 306 | 307 | assistantDeliveryMode: command.assistantDeliveryMode ?? DEFAULT_ASSISTANT_DELIVERY_MODE, |
| 307 | 308 | runtimeMode: |
| 308 | 309 | 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 |
| 86 | 86 | } |
| 87 | 87 | } |
| 88 | 88 | |
| 89 | function toRuntimePayloadFromSession(session: ProviderSession): Record<string, unknown> { | |
| 89 | function toRuntimePayloadFromSession( | |
| 90 | session: ProviderSession, | |
| 91 | extra?: { readonly providerOptions?: unknown }, | |
| 92 | ): Record<string, unknown> { | |
| 90 | 93 | return { |
| 91 | 94 | cwd: session.cwd ?? null, |
| 92 | 95 | model: session.model ?? null, |
| 93 | 96 | activeTurnId: session.activeTurnId ?? null, |
| 94 | 97 | lastError: session.lastError ?? null, |
| 98 | ...(extra?.providerOptions !== undefined ? { providerOptions: extra.providerOptions } : {}), | |
| 95 | 99 | }; |
| 96 | 100 | } |
| 97 | 101 | |
| 102 | function 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 | ||
| 98 | 113 | function readPersistedCwd( |
| 99 | 114 | runtimePayload: ProviderRuntimeBinding["runtimePayload"], |
| 100 | 115 | ): string | undefined { |
| ... | ... | @@ -137,6 +152,7 @@ const makeProviderService = (options?: ProviderServiceLiveOptions) => |
| 137 | 152 | const upsertSessionBinding = ( |
| 138 | 153 | session: ProviderSession, |
| 139 | 154 | threadId: ThreadId, |
| 155 | extra?: { readonly providerOptions?: unknown }, | |
| 140 | 156 | ) => |
| 141 | 157 | directory.upsert({ |
| 142 | 158 | threadId, |
| ... | ... | @@ -144,7 +160,7 @@ const makeProviderService = (options?: ProviderServiceLiveOptions) => |
| 144 | 160 | runtimeMode: session.runtimeMode, |
| 145 | 161 | status: toRuntimeStatus(session), |
| 146 | 162 | ...(session.resumeCursor !== undefined ? { resumeCursor: session.resumeCursor } : {}), |
| 147 | runtimePayload: toRuntimePayloadFromSession(session), | |
| 163 | runtimePayload: toRuntimePayloadFromSession(session, extra), | |
| 148 | 164 | }); |
| 149 | 165 | |
| 150 | 166 | const providers = yield* registry.listProviders(); |
| ... | ... | @@ -197,11 +213,13 @@ const makeProviderService = (options?: ProviderServiceLiveOptions) => |
| 197 | 213 | } |
| 198 | 214 | |
| 199 | 215 | const persistedCwd = readPersistedCwd(input.binding.runtimePayload); |
| 216 | const persistedProviderOptions = readPersistedProviderOptions(input.binding.runtimePayload); | |
| 200 | 217 | |
| 201 | 218 | const resumed = yield* adapter.startSession({ |
| 202 | 219 | threadId: input.binding.threadId, |
| 203 | 220 | provider: input.binding.provider, |
| 204 | 221 | ...(persistedCwd ? { cwd: persistedCwd } : {}), |
| 222 | ...(persistedProviderOptions ? { providerOptions: persistedProviderOptions } : {}), | |
| 205 | 223 | ...(hasResumeCursor ? { resumeCursor: input.binding.resumeCursor } : {}), |
| 206 | 224 | runtimeMode: input.binding.runtimeMode ?? "full-access", |
| 207 | 225 | }); |
| ... | ... | @@ -273,7 +291,9 @@ const makeProviderService = (options?: ProviderServiceLiveOptions) => |
| 273 | 291 | ); |
| 274 | 292 | } |
| 275 | 293 | |
| 276 | yield* upsertSessionBinding(session, threadId); | |
| 294 | yield* upsertSessionBinding(session, threadId, { | |
| 295 | ...(input.providerOptions !== undefined ? { providerOptions: input.providerOptions } : {}), | |
| 296 | }); | |
| 277 | 297 | yield* analytics.record("provider.session.started", { |
| 278 | 298 | provider: session.provider, |
| 279 | 299 | runtimeMode: input.runtimeMode, |
apps/web/src/components/ChatView.tsx+26-4| ... | ... | @@ -830,6 +830,17 @@ export default function ChatView({ threadId }: ChatViewProps) { |
| 830 | 830 | }; |
| 831 | 831 | return Object.keys(codexOptions).length > 0 ? { codex: codexOptions } : undefined; |
| 832 | 832 | }, [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]); | |
| 833 | 844 | const selectedModelForPicker = selectedModel; |
| 834 | 845 | const modelOptionsByProvider = useMemo( |
| 835 | 846 | () => getCustomModelOptionsByProvider(settings), |
| ... | ... | @@ -2626,6 +2637,9 @@ export default function ChatView({ threadId }: ChatViewProps) { |
| 2626 | 2637 | ...(selectedModelOptionsForDispatch |
| 2627 | 2638 | ? { modelOptions: selectedModelOptionsForDispatch } |
| 2628 | 2639 | : {}), |
| 2640 | ...(providerOptionsForDispatch | |
| 2641 | ? { providerOptions: providerOptionsForDispatch } | |
| 2642 | : {}), | |
| 2629 | 2643 | provider: selectedProvider, |
| 2630 | 2644 | assistantDeliveryMode: settings.enableAssistantStreaming ? "streaming" : "buffered", |
| 2631 | 2645 | runtimeMode, |
| ... | ... | @@ -2903,6 +2917,9 @@ export default function ChatView({ threadId }: ChatViewProps) { |
| 2903 | 2917 | ...(selectedModelOptionsForDispatch |
| 2904 | 2918 | ? { modelOptions: selectedModelOptionsForDispatch } |
| 2905 | 2919 | : {}), |
| 2920 | ...(providerOptionsForDispatch | |
| 2921 | ? { providerOptions: providerOptionsForDispatch } | |
| 2922 | : {}), | |
| 2906 | 2923 | assistantDeliveryMode: settings.enableAssistantStreaming ? "streaming" : "buffered", |
| 2907 | 2924 | runtimeMode, |
| 2908 | 2925 | interactionMode: nextInteractionMode, |
| ... | ... | @@ -2933,6 +2950,7 @@ export default function ChatView({ threadId }: ChatViewProps) { |
| 2933 | 2950 | runtimeMode, |
| 2934 | 2951 | selectedModel, |
| 2935 | 2952 | selectedModelOptionsForDispatch, |
| 2953 | providerOptionsForDispatch, | |
| 2936 | 2954 | selectedProvider, |
| 2937 | 2955 | setComposerDraftInteractionMode, |
| 2938 | 2956 | setThreadError, |
| ... | ... | @@ -2987,8 +3005,8 @@ export default function ChatView({ threadId }: ChatViewProps) { |
| 2987 | 3005 | worktreePath: activeThread.worktreePath, |
| 2988 | 3006 | createdAt, |
| 2989 | 3007 | }) |
| 2990 | .then(() => | |
| 2991 | api.orchestration.dispatchCommand({ | |
| 3008 | .then(() => { | |
| 3009 | return api.orchestration.dispatchCommand({ | |
| 2992 | 3010 | type: "thread.turn.start", |
| 2993 | 3011 | commandId: newCommandId(), |
| 2994 | 3012 | threadId: nextThreadId, |
| ... | ... | @@ -3003,12 +3021,15 @@ export default function ChatView({ threadId }: ChatViewProps) { |
| 3003 | 3021 | ...(selectedModelOptionsForDispatch |
| 3004 | 3022 | ? { modelOptions: selectedModelOptionsForDispatch } |
| 3005 | 3023 | : {}), |
| 3024 | ...(providerOptionsForDispatch | |
| 3025 | ? { providerOptions: providerOptionsForDispatch } | |
| 3026 | : {}), | |
| 3006 | 3027 | assistantDeliveryMode: settings.enableAssistantStreaming ? "streaming" : "buffered", |
| 3007 | 3028 | runtimeMode, |
| 3008 | 3029 | interactionMode: "default", |
| 3009 | 3030 | createdAt, |
| 3010 | }), | |
| 3011 | ) | |
| 3031 | }); | |
| 3032 | }) | |
| 3012 | 3033 | .then(() => api.orchestration.getSnapshot()) |
| 3013 | 3034 | .then((snapshot) => { |
| 3014 | 3035 | syncServerReadModel(snapshot); |
| ... | ... | @@ -3052,6 +3073,7 @@ export default function ChatView({ threadId }: ChatViewProps) { |
| 3052 | 3073 | runtimeMode, |
| 3053 | 3074 | selectedModel, |
| 3054 | 3075 | selectedModelOptionsForDispatch, |
| 3076 | providerOptionsForDispatch, | |
| 3055 | 3077 | selectedProvider, |
| 3056 | 3078 | settings.enableAssistantStreaming, |
| 3057 | 3079 | syncServerReadModel, |
packages/contracts/src/orchestration.ts+10| ... | ... | @@ -45,6 +45,13 @@ export type ProviderSandboxMode = typeof ProviderSandboxMode.Type; |
| 45 | 45 | export const ProviderServiceTier = Schema.Literals(["fast", "flex"]); |
| 46 | 46 | export type ProviderServiceTier = typeof ProviderServiceTier.Type; |
| 47 | 47 | export const DEFAULT_PROVIDER_KIND: ProviderKind = "codex"; |
| 48 | const CodexProviderStartOptions = Schema.Struct({ | |
| 49 | binaryPath: Schema.optional(TrimmedNonEmptyString), | |
| 50 | homePath: Schema.optional(TrimmedNonEmptyString), | |
| 51 | }); | |
| 52 | const ProviderStartOptions = Schema.Struct({ | |
| 53 | codex: Schema.optional(CodexProviderStartOptions), | |
| 54 | }); | |
| 48 | 55 | export const RuntimeMode = Schema.Literals(["approval-required", "full-access"]); |
| 49 | 56 | export type RuntimeMode = typeof RuntimeMode.Type; |
| 50 | 57 | export const DEFAULT_RUNTIME_MODE: RuntimeMode = "full-access"; |
| ... | ... | @@ -366,6 +373,7 @@ export const ThreadTurnStartCommand = Schema.Struct({ |
| 366 | 373 | model: Schema.optional(TrimmedNonEmptyString), |
| 367 | 374 | serviceTier: Schema.optional(Schema.NullOr(ProviderServiceTier)), |
| 368 | 375 | modelOptions: Schema.optional(ProviderModelOptions), |
| 376 | providerOptions: Schema.optional(ProviderStartOptions), | |
| 369 | 377 | assistantDeliveryMode: Schema.optional(AssistantDeliveryMode), |
| 370 | 378 | runtimeMode: RuntimeMode.pipe(Schema.withDecodingDefault(() => DEFAULT_RUNTIME_MODE)), |
| 371 | 379 | interactionMode: ProviderInteractionMode.pipe( |
| ... | ... | @@ -388,6 +396,7 @@ const ClientThreadTurnStartCommand = Schema.Struct({ |
| 388 | 396 | model: Schema.optional(TrimmedNonEmptyString), |
| 389 | 397 | serviceTier: Schema.optional(Schema.NullOr(ProviderServiceTier)), |
| 390 | 398 | modelOptions: Schema.optional(ProviderModelOptions), |
| 399 | providerOptions: Schema.optional(ProviderStartOptions), | |
| 391 | 400 | assistantDeliveryMode: Schema.optional(AssistantDeliveryMode), |
| 392 | 401 | runtimeMode: RuntimeMode, |
| 393 | 402 | interactionMode: ProviderInteractionMode, |
| ... | ... | @@ -668,6 +677,7 @@ export const ThreadTurnStartRequestedPayload = Schema.Struct({ |
| 668 | 677 | model: Schema.optional(TrimmedNonEmptyString), |
| 669 | 678 | serviceTier: Schema.optional(Schema.NullOr(ProviderServiceTier)), |
| 670 | 679 | modelOptions: Schema.optional(ProviderModelOptions), |
| 680 | providerOptions: Schema.optional(ProviderStartOptions), | |
| 671 | 681 | assistantDeliveryMode: Schema.optional(AssistantDeliveryMode), |
| 672 | 682 | runtimeMode: RuntimeMode.pipe(Schema.withDecodingDefault(() => DEFAULT_RUNTIME_MODE)), |
| 673 | 683 | interactionMode: ProviderInteractionMode.pipe( |
packages/contracts/src/provider.ts+2-1| ... | ... | @@ -53,9 +53,10 @@ const CodexProviderStartOptions = Schema.Struct({ |
| 53 | 53 | homePath: Schema.optional(TrimmedNonEmptyStringSchema), |
| 54 | 54 | }); |
| 55 | 55 | |
| 56 | const ProviderStartOptions = Schema.Struct({ | |
| 56 | export const ProviderStartOptions = Schema.Struct({ | |
| 57 | 57 | codex: Schema.optional(CodexProviderStartOptions), |
| 58 | 58 | }); |
| 59 | export type ProviderStartOptions = typeof ProviderStartOptions.Type; | |
| 59 | 60 | |
| 60 | 61 | export const ProviderSessionStartInput = Schema.Struct({ |
| 61 | 62 | threadId: ThreadId, |