| 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,9 +312,56 @@ export const checkCodexProviderStatus: Effect.Effect< |
| 312 | export const ProviderHealthLive = Layer.effect( | 312 | export const ProviderHealthLive = Layer.effect( |
| 313 | ProviderHealth, | 313 | ProviderHealth, |
| 314 | Effect.gen(function* () { | 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 | return { | 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 | } satisfies ProviderHealthShape; | 365 | } satisfies ProviderHealthShape; |
| 319 | }), | 366 | }), |
| 320 | ); | 367 | ); |
apps/server/src/provider/Services/ProviderHealth.ts+4| ... | @@ -15,6 +15,10 @@ export interface ProviderHealthShape { | ... | @@ -15,6 +15,10 @@ export interface ProviderHealthShape { |
| 15 | * Read provider health statuses computed at server startup. | 15 | * Read provider health statuses computed at server startup. |
| 16 | */ | 16 | */ |
| 17 | readonly getStatuses: Effect.Effect<ReadonlyArray<ServerProviderStatus>>; | 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 | export class ProviderHealth extends ServiceMap.Service<ProviderHealth, ProviderHealthShape>()( | 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,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 | const clients = yield* Ref.make(new Set<WebSocket>()); | 274 | const clients = yield* Ref.make(new Set<WebSocket>()); |
| 274 | const logger = createLogger("ws"); | 275 | const logger = createLogger("ws"); |
| ... | @@ -631,11 +632,23 @@ export const createServer = Effect.fn(function* (): Effect.fn.Return< | ... | @@ -631,11 +632,23 @@ export const createServer = Effect.fn(function* (): Effect.fn.Return< |
| 631 | channel: WS_CHANNELS.serverConfigUpdated, | 632 | channel: WS_CHANNELS.serverConfigUpdated, |
| 632 | data: { | 633 | data: { |
| 633 | issues: event.issues, | 634 | issues: event.issues, |
| 634 | providers: providerStatuses, | 635 | providers: getProviderStatuses(), |
| 635 | }, | 636 | }, |
| 636 | }), | 637 | }), |
| 637 | ).pipe(Effect.forkIn(subscriptionsScope)); | 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 | yield* Scope.provide(orchestrationReactor.start, subscriptionsScope); | 652 | yield* Scope.provide(orchestrationReactor.start, subscriptionsScope); |
| 640 | 653 | ||
| 641 | let welcomeBootstrapProjectId: ProjectId | undefined; | 654 | let welcomeBootstrapProjectId: ProjectId | undefined; |
| ... | @@ -883,7 +896,7 @@ export const createServer = Effect.fn(function* (): Effect.fn.Return< | ... | @@ -883,7 +896,7 @@ export const createServer = Effect.fn(function* (): Effect.fn.Return< |
| 883 | keybindingsConfigPath, | 896 | keybindingsConfigPath, |
| 884 | keybindings: keybindingsConfig.keybindings, | 897 | keybindings: keybindingsConfig.keybindings, |
| 885 | issues: keybindingsConfig.issues, | 898 | issues: keybindingsConfig.issues, |
| 886 | providers: providerStatuses, | 899 | providers: getProviderStatuses(), |
| 887 | availableEditors, | 900 | availableEditors, |
| 888 | }; | 901 | }; |
| 889 | 902 |