From 72cb9427885685fe91089c5da534a17d204dc03d Mon Sep 17 00:00:00 2001 From: gsxdsm Date: Sat, 21 Feb 2026 23:58:09 -0800 Subject: [PATCH] Fix Codex CLI timeout handling and improve CI workflows (#797) * Changes from fix/codex-cli-timeout * test: Clarify timeout values and multipliers in codex-provider tests * refactor: Rename useWorktreesEnabled to worktreesEnabled for clarity --- apps/server/src/providers/codex-provider.ts | 3 +- .../unit/providers/codex-provider.test.ts | 12 ++++--- .../board-view/hooks/use-board-actions.ts | 36 ++++++++++--------- .../board-view/hooks/use-board-drag-drop.ts | 7 +++- .../board-view/hooks/use-board-persistence.ts | 6 +++- 5 files changed, 39 insertions(+), 25 deletions(-) diff --git a/apps/server/src/providers/codex-provider.ts b/apps/server/src/providers/codex-provider.ts index 10146591..1359d37a 100644 --- a/apps/server/src/providers/codex-provider.ts +++ b/apps/server/src/providers/codex-provider.ts @@ -33,7 +33,6 @@ import { supportsReasoningEffort, validateBareModelId, calculateReasoningTimeout, - DEFAULT_TIMEOUT_MS, type CodexApprovalPolicy, type CodexSandboxMode, type CodexAuthStatus, @@ -98,7 +97,7 @@ const TEXT_ENCODING = 'utf-8'; * * @see calculateReasoningTimeout from @automaker/types */ -const CODEX_CLI_TIMEOUT_MS = DEFAULT_TIMEOUT_MS; +const CODEX_CLI_TIMEOUT_MS = 120000; // 2 minutes — matches CLI provider base timeout const CODEX_FEATURE_GENERATION_BASE_TIMEOUT_MS = 300000; // 5 minutes for feature generation const SYSTEM_PROMPT_SEPARATOR = '\n\n'; const CODEX_INSTRUCTIONS_DIR = '.codex'; diff --git a/apps/server/tests/unit/providers/codex-provider.test.ts b/apps/server/tests/unit/providers/codex-provider.test.ts index 03cd5591..0121fd17 100644 --- a/apps/server/tests/unit/providers/codex-provider.test.ts +++ b/apps/server/tests/unit/providers/codex-provider.test.ts @@ -320,8 +320,10 @@ describe('codex-provider.ts', () => { ); const call = vi.mocked(spawnJSONLProcess).mock.calls[0][0]; - // High reasoning effort should have 3x the default timeout (90000ms) - expect(call.timeout).toBe(DEFAULT_TIMEOUT_MS * REASONING_TIMEOUT_MULTIPLIERS.high); + // High reasoning effort should have 3x the CLI base timeout (120000ms) + // CODEX_CLI_TIMEOUT_MS = 120000, multiplier for 'high' = 3.0 → 360000ms + const CODEX_CLI_TIMEOUT_MS = 120000; + expect(call.timeout).toBe(CODEX_CLI_TIMEOUT_MS * REASONING_TIMEOUT_MULTIPLIERS.high); }); it('passes extended timeout for xhigh reasoning effort', async () => { @@ -357,8 +359,10 @@ describe('codex-provider.ts', () => { ); const call = vi.mocked(spawnJSONLProcess).mock.calls[0][0]; - // No reasoning effort should use the default timeout - expect(call.timeout).toBe(DEFAULT_TIMEOUT_MS); + // No reasoning effort should use the CLI base timeout (2 minutes) + // CODEX_CLI_TIMEOUT_MS = 120000ms, no multiplier applied + const CODEX_CLI_TIMEOUT_MS = 120000; + expect(call.timeout).toBe(CODEX_CLI_TIMEOUT_MS); }); }); diff --git a/apps/ui/src/components/views/board-view/hooks/use-board-actions.ts b/apps/ui/src/components/views/board-view/hooks/use-board-actions.ts index b04983be..8c87b7da 100644 --- a/apps/ui/src/components/views/board-view/hooks/use-board-actions.ts +++ b/apps/ui/src/components/views/board-view/hooks/use-board-actions.ts @@ -84,17 +84,19 @@ export function useBoardActions({ onWorktreeAutoSelect, currentWorktreeBranch, }: UseBoardActionsProps) { - const { - addFeature, - updateFeature, - removeFeature, - moveFeature, - useWorktrees, - enableDependencyBlocking, - skipVerificationInAutoMode, - isPrimaryWorktreeBranch, - getPrimaryWorktreeBranch, - } = useAppStore(); + // IMPORTANT: Use individual selectors instead of bare useAppStore() to prevent + // subscribing to the entire store. Bare useAppStore() causes the host component + // (BoardView) to re-render on EVERY store change, which cascades through effects + // and triggers React error #185 (maximum update depth exceeded). + const addFeature = useAppStore((s) => s.addFeature); + const updateFeature = useAppStore((s) => s.updateFeature); + const removeFeature = useAppStore((s) => s.removeFeature); + const moveFeature = useAppStore((s) => s.moveFeature); + const worktreesEnabled = useAppStore((s) => s.useWorktrees); + const enableDependencyBlocking = useAppStore((s) => s.enableDependencyBlocking); + const skipVerificationInAutoMode = useAppStore((s) => s.skipVerificationInAutoMode); + const isPrimaryWorktreeBranch = useAppStore((s) => s.isPrimaryWorktreeBranch); + const getPrimaryWorktreeBranch = useAppStore((s) => s.getPrimaryWorktreeBranch); const autoMode = useAutoMode(); // React Query mutations for feature operations @@ -549,7 +551,7 @@ export function useBoardActions({ const result = await api.autoMode.runFeature( currentProject.path, feature.id, - useWorktrees + worktreesEnabled // No worktreePath - server derives from feature.branchName ); @@ -560,7 +562,7 @@ export function useBoardActions({ throw new Error(result.error || 'Failed to start feature'); } }, - [currentProject, useWorktrees] + [currentProject, worktreesEnabled] ); const handleStartImplementation = useCallback( @@ -693,9 +695,9 @@ export function useBoardActions({ logger.error('No current project'); return; } - resumeFeatureMutation.mutate({ featureId: feature.id, useWorktrees }); + resumeFeatureMutation.mutate({ featureId: feature.id, useWorktrees: worktreesEnabled }); }, - [currentProject, resumeFeatureMutation, useWorktrees] + [currentProject, resumeFeatureMutation, worktreesEnabled] ); const handleManualVerify = useCallback( @@ -780,7 +782,7 @@ export function useBoardActions({ followUpFeature.id, followUpPrompt, imagePaths, - useWorktrees + worktreesEnabled ); if (!result.success) { @@ -818,7 +820,7 @@ export function useBoardActions({ setFollowUpPrompt, setFollowUpImagePaths, setFollowUpPreviewMap, - useWorktrees, + worktreesEnabled, ]); const handleCommitFeature = useCallback( diff --git a/apps/ui/src/components/views/board-view/hooks/use-board-drag-drop.ts b/apps/ui/src/components/views/board-view/hooks/use-board-drag-drop.ts index b313c762..4b98806d 100644 --- a/apps/ui/src/components/views/board-view/hooks/use-board-drag-drop.ts +++ b/apps/ui/src/components/views/board-view/hooks/use-board-drag-drop.ts @@ -33,7 +33,12 @@ export function useBoardDragDrop({ const [pendingDependencyLink, setPendingDependencyLink] = useState( null ); - const { moveFeature, updateFeature } = useAppStore(); + // IMPORTANT: Use individual selectors instead of bare useAppStore() to prevent + // subscribing to the entire store. Bare useAppStore() causes the host component + // (BoardView) to re-render on EVERY store change, which cascades through effects + // and triggers React error #185 (maximum update depth exceeded). + const moveFeature = useAppStore((s) => s.moveFeature); + const updateFeature = useAppStore((s) => s.updateFeature); const autoMode = useAutoMode(); // Note: getOrCreateWorktreeForFeature removed - worktrees are now created server-side diff --git a/apps/ui/src/components/views/board-view/hooks/use-board-persistence.ts b/apps/ui/src/components/views/board-view/hooks/use-board-persistence.ts index 3bbe0a15..e5b896b3 100644 --- a/apps/ui/src/components/views/board-view/hooks/use-board-persistence.ts +++ b/apps/ui/src/components/views/board-view/hooks/use-board-persistence.ts @@ -14,7 +14,11 @@ interface UseBoardPersistenceProps { } export function useBoardPersistence({ currentProject }: UseBoardPersistenceProps) { - const { updateFeature } = useAppStore(); + // IMPORTANT: Use individual selector instead of bare useAppStore() to prevent + // subscribing to the entire store. Bare useAppStore() causes the host component + // (BoardView) to re-render on EVERY store change, which cascades through effects + // and triggers React error #185 (maximum update depth exceeded). + const updateFeature = useAppStore((s) => s.updateFeature); const queryClient = useQueryClient(); // Persist feature update to API (replaces saveFeatures)