| author | |
| committer | |
| log | 6593645c8092696403f39e7d6eeebeec190385dc |
| tree | 2b228810c69cf22bbc1f43c2c97d88ad9421abd8 |
| parent | 296eb82c312b3c36237b51870b9a87249f5c6d83 |
The ProviderHealth layer blocked server startup with two sequential CLI
spawns (codex --version + codex login status), each with a 4-second
timeout, delaying startup by up to 8 seconds.
Run health checks in the background via Effect.runPromise so the layer
resolves immediately with a placeholder status. Add an onReady callback
to ProviderHealthShape so wsServer can push the resolved statuses to
connected clients once checks complete, preventing early-connecting
clients from showing "Checking..." indefinitely.3 files changed, 69 insertions(+), 5 deletions(-)
apps/server/src/provider/Layers/ProviderHealth.ts+49-2| ... | ... | @@ -312,9 +312,56 @@ export const checkCodexProviderStatus: Effect.Effect< |
| 312 | 312 | export const ProviderHealthLive = Layer.effect( |
| 313 | 313 | ProviderHealth, |
| 314 | 314 | Effect.gen(function* () { |
| 315 | const codexStatus = yield* checkCodexProviderStatus; | |
| 315 | const spawner = yield* ChildProcessSpawner.ChildProcessSpawner; | |
| 316 | let cachedStatuses: ReadonlyArray<ServerProviderStatus> = [ | |
| 317 | { | |
| 318 | provider: CODEX_PROVIDER, | |
| 319 | status: "warning", | |
| 320 | available: false, | |
| 321 | authStatus: "unknown", | |
| 322 | checkedAt: new Date().toISOString(), | |
| 323 | message: "Checking Codex CLI availability...", | |
| 324 | }, | |
| 325 | ]; | |
| 326 | ||
| 327 | let readyListeners: Array<(statuses: ReadonlyArray<ServerProviderStatus>) => void> = []; | |
| 328 | let resolved = false; | |
| 329 | ||
| 330 | const notifyReady = (statuses: ReadonlyArray<ServerProviderStatus>) => { | |
| 331 | resolved = true; | |
| 332 | cachedStatuses = statuses; | |
| 333 | for (const cb of readyListeners) cb(statuses); | |
| 334 | readyListeners = []; | |
| 335 | }; | |
| 336 | ||
| 337 | // Run health checks in the background so they don't block server startup. | |
| 338 | checkCodexProviderStatus.pipe( | |
| 339 | Effect.provideService(ChildProcessSpawner.ChildProcessSpawner, spawner), | |
| 340 | Effect.runPromise, | |
| 341 | ).then((status) => { | |
| 342 | notifyReady([status]); | |
| 343 | }).catch(() => { | |
| 344 | notifyReady([ | |
| 345 | { | |
| 346 | provider: CODEX_PROVIDER, | |
| 347 | status: "error", | |
| 348 | available: false, | |
| 349 | authStatus: "unknown", | |
| 350 | checkedAt: new Date().toISOString(), | |
| 351 | message: "Failed to check Codex CLI status.", | |
| 352 | }, | |
| 353 | ]); | |
| 354 | }); | |
| 355 | ||
| 316 | 356 | return { |
| 317 | getStatuses: Effect.succeed([codexStatus]), | |
| 357 | getStatuses: Effect.sync(() => cachedStatuses), | |
| 358 | onReady: (cb) => { | |
| 359 | if (resolved) { | |
| 360 | cb(cachedStatuses); | |
| 361 | } else { | |
| 362 | readyListeners.push(cb); | |
| 363 | } | |
| 364 | }, | |
| 318 | 365 | } satisfies ProviderHealthShape; |
| 319 | 366 | }), |
| 320 | 367 | ); |
apps/server/src/provider/Services/ProviderHealth.ts+4| ... | ... | @@ -15,6 +15,10 @@ export interface ProviderHealthShape { |
| 15 | 15 | * Read provider health statuses computed at server startup. |
| 16 | 16 | */ |
| 17 | 17 | readonly getStatuses: Effect.Effect<ReadonlyArray<ServerProviderStatus>>; |
| 18 | /** | |
| 19 | * Register a callback invoked once when background health checks complete. | |
| 20 | */ | |
| 21 | readonly onReady: (cb: (statuses: ReadonlyArray<ServerProviderStatus>) => void) => void; | |
| 18 | 22 | } |
| 19 | 23 | |
| 20 | 24 | export class ProviderHealth extends ServiceMap.Service<ProviderHealth, ProviderHealthShape>()( |
apps/server/src/wsServer.ts+16-3| ... | ... | @@ -268,7 +268,8 @@ export const createServer = Effect.fn(function* (): Effect.fn.Return< |
| 268 | 268 | ), |
| 269 | 269 | ); |
| 270 | 270 | |
| 271 | const providerStatuses = yield* providerHealth.getStatuses; | |
| 271 | // Read provider statuses lazily so background health checks are reflected. | |
| 272 | const getProviderStatuses = () => Effect.runSync(providerHealth.getStatuses); | |
| 272 | 273 | |
| 273 | 274 | const clients = yield* Ref.make(new Set<WebSocket>()); |
| 274 | 275 | const logger = createLogger("ws"); |
| ... | ... | @@ -631,11 +632,23 @@ export const createServer = Effect.fn(function* (): Effect.fn.Return< |
| 631 | 632 | channel: WS_CHANNELS.serverConfigUpdated, |
| 632 | 633 | data: { |
| 633 | 634 | issues: event.issues, |
| 634 | providers: providerStatuses, | |
| 635 | providers: getProviderStatuses(), | |
| 635 | 636 | }, |
| 636 | 637 | }), |
| 637 | 638 | ).pipe(Effect.forkIn(subscriptionsScope)); |
| 638 | 639 | |
| 640 | // Push updated provider statuses to connected clients once background health checks finish. | |
| 641 | providerHealth.onReady((statuses) => { | |
| 642 | broadcastPush({ | |
| 643 | type: "push", | |
| 644 | channel: WS_CHANNELS.serverConfigUpdated, | |
| 645 | data: { | |
| 646 | issues: [], | |
| 647 | providers: statuses, | |
| 648 | }, | |
| 649 | }).pipe(Effect.runPromise).catch(() => {}); | |
| 650 | }); | |
| 651 | ||
| 639 | 652 | yield* Scope.provide(orchestrationReactor.start, subscriptionsScope); |
| 640 | 653 | |
| 641 | 654 | let welcomeBootstrapProjectId: ProjectId | undefined; |
| ... | ... | @@ -883,7 +896,7 @@ export const createServer = Effect.fn(function* (): Effect.fn.Return< |
| 883 | 896 | keybindingsConfigPath, |
| 884 | 897 | keybindings: keybindingsConfig.keybindings, |
| 885 | 898 | issues: keybindingsConfig.issues, |
| 886 | providers: providerStatuses, | |
| 899 | providers: getProviderStatuses(), | |
| 887 | 900 | availableEditors, |
| 888 | 901 | }; |
| 889 | 902 |