From f5efa857ca498bd20cd9b8e390a805915bf5e276 Mon Sep 17 00:00:00 2001 From: Shirone Date: Sat, 24 Jan 2026 22:05:29 +0100 Subject: [PATCH 1/3] fix: Prevent GitHub API rate limiting from frequent worktree PR fetching MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes #685 This commit addresses the GitHub API rate limit issue caused by excessive worktree PR status fetching. ## Changes ### Server-side PR caching (list.ts) - Added `GitHubPRCacheEntry` interface and `githubPRCache` Map - Implemented 2-minute TTL cache for GitHub PR data - Modified `fetchGitHubPRs()` to check cache before making API calls - Added `forceRefresh` parameter to bypass cache when explicitly requested - Cache is properly cleared when force refresh is triggered ### Frontend polling reduction (worktree-panel.tsx) - Increased worktree polling interval from 5 seconds to 30 seconds - Reduces polling frequency by 6x while keeping UI reasonably fresh - Updated comment to reflect new polling strategy ### Type improvements (use-worktrees.ts) - Fixed `fetchWorktrees` callback signature to accept `silent` option - Returns proper type for removed worktrees detection ## Impact - Combined ~12x reduction in GitHub API calls - 2-minute cache prevents repeated API hits during normal operation - 30-second polling balances responsiveness with API conservation - Force refresh option allows users to manually update when needed 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 --- .../server/src/routes/worktree/routes/list.ts | 33 +++++++++++++++++-- .../worktree-panel/hooks/use-worktrees.ts | 20 +++++++---- .../worktree-panel/worktree-panel.tsx | 6 ++-- 3 files changed, 47 insertions(+), 12 deletions(-) diff --git a/apps/server/src/routes/worktree/routes/list.ts b/apps/server/src/routes/worktree/routes/list.ts index f0d9c030..8482f62c 100644 --- a/apps/server/src/routes/worktree/routes/list.ts +++ b/apps/server/src/routes/worktree/routes/list.ts @@ -39,8 +39,15 @@ interface GitHubRemoteCacheEntry { checkedAt: number; } +interface GitHubPRCacheEntry { + prs: Map; + fetchedAt: number; +} + const githubRemoteCache = new Map(); +const githubPRCache = new Map(); const GITHUB_REMOTE_CACHE_TTL_MS = 5 * 60 * 1000; // 5 minutes +const GITHUB_PR_CACHE_TTL_MS = 2 * 60 * 1000; // 2 minutes - avoid hitting GitHub on every poll interface WorktreeInfo { path: string; @@ -180,9 +187,24 @@ async function getGitHubRemoteStatus(projectPath: string): Promise> { +async function fetchGitHubPRs( + projectPath: string, + forceRefresh = false +): Promise> { + const now = Date.now(); + + if (!forceRefresh) { + const cached = githubPRCache.get(projectPath); + if (cached && now - cached.fetchedAt < GITHUB_PR_CACHE_TTL_MS) { + return cached.prs; + } + } else { + githubPRCache.delete(projectPath); + } + const prMap = new Map(); try { @@ -225,6 +247,11 @@ async function fetchGitHubPRs(projectPath: string): Promise(); for (const worktree of worktrees) { diff --git a/apps/ui/src/components/views/board-view/worktree-panel/hooks/use-worktrees.ts b/apps/ui/src/components/views/board-view/worktree-panel/hooks/use-worktrees.ts index 6a3276ec..ab3a87d0 100644 --- a/apps/ui/src/components/views/board-view/worktree-panel/hooks/use-worktrees.ts +++ b/apps/ui/src/components/views/board-view/worktree-panel/hooks/use-worktrees.ts @@ -95,12 +95,20 @@ export function useWorktrees({ ); // fetchWorktrees for backward compatibility - now just triggers a refetch - const fetchWorktrees = useCallback(async () => { - await queryClient.invalidateQueries({ - queryKey: queryKeys.worktrees.all(projectPath), - }); - return refetch(); - }, [projectPath, queryClient, refetch]); + // The silent option is accepted but not used (React Query handles loading states) + // Returns removed worktrees array if any were detected, undefined otherwise + const fetchWorktrees = useCallback( + async (_options?: { + silent?: boolean; + }): Promise | undefined> => { + await queryClient.invalidateQueries({ + queryKey: queryKeys.worktrees.all(projectPath), + }); + const result = await refetch(); + return result.data?.removedWorktrees; + }, + [projectPath, queryClient, refetch] + ); const currentWorktreePath = currentWorktree?.path ?? null; const selectedWorktree = currentWorktreePath diff --git a/apps/ui/src/components/views/board-view/worktree-panel/worktree-panel.tsx b/apps/ui/src/components/views/board-view/worktree-panel/worktree-panel.tsx index b1e800fe..6d376ea5 100644 --- a/apps/ui/src/components/views/board-view/worktree-panel/worktree-panel.tsx +++ b/apps/ui/src/components/views/board-view/worktree-panel/worktree-panel.tsx @@ -383,13 +383,13 @@ export function WorktreePanel({ const isMobile = useIsMobile(); - // Periodic interval check (5 seconds) to detect branch changes on disk - // Reduced from 1s to 5s to minimize GPU/CPU usage from frequent re-renders + // Periodic interval check (30 seconds) to detect branch changes on disk + // Reduced polling to lessen repeated worktree list calls while keeping UI reasonably fresh const intervalRef = useRef(null); useEffect(() => { intervalRef.current = setInterval(() => { fetchWorktrees({ silent: true }); - }, 5000); + }, 30000); return () => { if (intervalRef.current) { From b5143f4b005802098a93b67707c1407c7ee8bf58 Mon Sep 17 00:00:00 2001 From: Shirone Date: Sat, 24 Jan 2026 22:27:58 +0100 Subject: [PATCH 2/3] fix: Return stale cache on GitHub PR fetch failure to prevent repeated API calls MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Address PR #688 review feedback: previously the cache was deleted before fetch, causing repeated API calls if the fetch failed. Now the cache entry is preserved and stale data is returned on failure, preventing unnecessary API calls during GitHub API flakiness or temporary outages. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- .../server/src/routes/worktree/routes/list.ts | 20 +++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/apps/server/src/routes/worktree/routes/list.ts b/apps/server/src/routes/worktree/routes/list.ts index 8482f62c..bb9e5d8f 100644 --- a/apps/server/src/routes/worktree/routes/list.ts +++ b/apps/server/src/routes/worktree/routes/list.ts @@ -195,14 +195,11 @@ async function fetchGitHubPRs( forceRefresh = false ): Promise> { const now = Date.now(); + const cached = githubPRCache.get(projectPath); - if (!forceRefresh) { - const cached = githubPRCache.get(projectPath); - if (cached && now - cached.fetchedAt < GITHUB_PR_CACHE_TTL_MS) { - return cached.prs; - } - } else { - githubPRCache.delete(projectPath); + // Return cached result if valid and not forcing refresh + if (!forceRefresh && cached && now - cached.fetchedAt < GITHUB_PR_CACHE_TTL_MS) { + return cached.prs; } const prMap = new Map(); @@ -248,12 +245,19 @@ async function fetchGitHubPRs( }); } + // Only update cache on successful fetch githubPRCache.set(projectPath, { prs: prMap, fetchedAt: Date.now(), }); } catch (error) { - // Silently fail - PR detection is optional + // On fetch failure, return stale cached data if available to avoid + // repeated API calls during GitHub API flakiness or temporary outages + if (cached) { + logger.warn(`Failed to fetch GitHub PRs, returning stale cache: ${getErrorMessage(error)}`); + return cached.prs; + } + // No cache available, log warning and return empty map logger.warn(`Failed to fetch GitHub PRs: ${getErrorMessage(error)}`); } From 8dd6ab2161797f6118fb6715346ab3b0e3aba60a Mon Sep 17 00:00:00 2001 From: Shirone Date: Sat, 24 Jan 2026 22:38:50 +0100 Subject: [PATCH 3/3] fix: Extend cache TTL on GitHub PR fetch failure to prevent retry storms MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Address PR #688 review feedback from CodeRabbit: When a GitHub PR fetch fails and we return stale cached data, also update the fetchedAt timestamp. This prevents the original TTL from expiring and causing every subsequent poll to retry the failing request, which would still hammer GitHub during API outages. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- apps/server/src/routes/worktree/routes/list.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/apps/server/src/routes/worktree/routes/list.ts b/apps/server/src/routes/worktree/routes/list.ts index bb9e5d8f..0f8021f1 100644 --- a/apps/server/src/routes/worktree/routes/list.ts +++ b/apps/server/src/routes/worktree/routes/list.ts @@ -255,6 +255,8 @@ async function fetchGitHubPRs( // repeated API calls during GitHub API flakiness or temporary outages if (cached) { logger.warn(`Failed to fetch GitHub PRs, returning stale cache: ${getErrorMessage(error)}`); + // Extend cache TTL to avoid repeated retries during outages + githubPRCache.set(projectPath, { prs: cached.prs, fetchedAt: Date.now() }); return cached.prs; } // No cache available, log warning and return empty map