From e4458b82228cd4563194654db376ca6a729cfcb4 Mon Sep 17 00:00:00 2001 From: Stefan de Vogelaere Date: Tue, 13 Jan 2026 00:47:51 +0100 Subject: [PATCH 1/2] fix(codex): prevent infinite loop when fetching models on settings screen When Codex is not connected/authenticated, the /api/codex/models endpoint returns 503. The fetchCodexModels function had no cooldown after failures, causing infinite retries when navigating to the Settings screen. Added codexModelsLastFailedAt state to track failed fetch attempts and skip retries for 30 seconds after a failure. This prevents the infinite loop while still allowing periodic retry attempts. Co-Authored-By: Claude Opus 4.5 --- apps/ui/src/store/app-store.ts | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/apps/ui/src/store/app-store.ts b/apps/ui/src/store/app-store.ts index ca415ed6..85d7b782 100644 --- a/apps/ui/src/store/app-store.ts +++ b/apps/ui/src/store/app-store.ts @@ -689,6 +689,7 @@ export interface AppState { codexModelsLoading: boolean; codexModelsError: string | null; codexModelsLastFetched: number | null; + codexModelsLastFailedAt: number | null; // Pipeline Configuration (per-project, keyed by project path) pipelineConfigByProject: Record; @@ -1286,6 +1287,7 @@ const initialState: AppState = { codexModelsLoading: false, codexModelsError: null, codexModelsLastFetched: null, + codexModelsLastFailedAt: null, pipelineConfigByProject: {}, worktreePanelVisibleByProject: {}, showInitScriptIndicatorByProject: {}, @@ -3113,11 +3115,21 @@ export const useAppStore = create()((set, get) => ({ // Codex Models actions fetchCodexModels: async (forceRefresh = false) => { - const { codexModelsLastFetched, codexModelsLoading } = get(); + const { codexModelsLastFetched, codexModelsLoading, codexModelsLastFailedAt } = get(); // Skip if already loading if (codexModelsLoading) return; + // Skip if recently failed (< 30 seconds ago) and not forcing refresh + const FAILURE_COOLDOWN = 30000; // 30 seconds + if ( + !forceRefresh && + codexModelsLastFailedAt && + Date.now() - codexModelsLastFailedAt < FAILURE_COOLDOWN + ) { + return; + } + // Skip if recently fetched (< 5 minutes ago) and not forcing refresh if (!forceRefresh && codexModelsLastFetched && Date.now() - codexModelsLastFetched < 300000) { return; @@ -3142,12 +3154,14 @@ export const useAppStore = create()((set, get) => ({ codexModelsLastFetched: Date.now(), codexModelsLoading: false, codexModelsError: null, + codexModelsLastFailedAt: null, // Clear failure on success }); } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Unknown error'; set({ codexModelsError: errorMessage, codexModelsLoading: false, + codexModelsLastFailedAt: Date.now(), // Record failure time for cooldown }); } }, From 18d82b1bb18ee9506b778c1d6a53cbfd8ac9f73c Mon Sep 17 00:00:00 2001 From: Stefan de Vogelaere Date: Tue, 13 Jan 2026 00:53:39 +0100 Subject: [PATCH 2/2] refactor: improve time constant readability - Rename FAILURE_COOLDOWN to FAILURE_COOLDOWN_MS with explicit calculation - Add SUCCESS_CACHE_MS constant to replace magic number 300000 - Use multiplication (30 * 1000, 5 * 60 * 1000) to make units explicit Co-Authored-By: Claude Opus 4.5 --- apps/ui/src/store/app-store.ts | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/apps/ui/src/store/app-store.ts b/apps/ui/src/store/app-store.ts index 85d7b782..36aec5ed 100644 --- a/apps/ui/src/store/app-store.ts +++ b/apps/ui/src/store/app-store.ts @@ -3115,23 +3115,29 @@ export const useAppStore = create()((set, get) => ({ // Codex Models actions fetchCodexModels: async (forceRefresh = false) => { + const FAILURE_COOLDOWN_MS = 30 * 1000; // 30 seconds + const SUCCESS_CACHE_MS = 5 * 60 * 1000; // 5 minutes + const { codexModelsLastFetched, codexModelsLoading, codexModelsLastFailedAt } = get(); // Skip if already loading if (codexModelsLoading) return; - // Skip if recently failed (< 30 seconds ago) and not forcing refresh - const FAILURE_COOLDOWN = 30000; // 30 seconds + // Skip if recently failed and not forcing refresh if ( !forceRefresh && codexModelsLastFailedAt && - Date.now() - codexModelsLastFailedAt < FAILURE_COOLDOWN + Date.now() - codexModelsLastFailedAt < FAILURE_COOLDOWN_MS ) { return; } - // Skip if recently fetched (< 5 minutes ago) and not forcing refresh - if (!forceRefresh && codexModelsLastFetched && Date.now() - codexModelsLastFetched < 300000) { + // Skip if recently fetched successfully and not forcing refresh + if ( + !forceRefresh && + codexModelsLastFetched && + Date.now() - codexModelsLastFetched < SUCCESS_CACHE_MS + ) { return; }