diff --git a/src/renderer/src/components/workspace-cleanup/use-workspace-cleanup-git-evidence.test.tsx b/src/renderer/src/components/workspace-cleanup/use-workspace-cleanup-git-evidence.test.tsx index 66bb6445aa2..979c4d7c23d 100644 --- a/src/renderer/src/components/workspace-cleanup/use-workspace-cleanup-git-evidence.test.tsx +++ b/src/renderer/src/components/workspace-cleanup/use-workspace-cleanup-git-evidence.test.tsx @@ -38,7 +38,7 @@ describe('useWorkspaceCleanupGitEvidence', () => { }) }) - it('counts queued rows and isolates a restarted pass from stale settlements', async () => { + it('bounds restarted passes and isolates them from stale settlements', async () => { const candidates = ['a', 'b', 'c', 'd', 'e', 'f'].map(deferredCandidate) const view = renderHook( ({ enabled }: { enabled: boolean }) => @@ -53,7 +53,8 @@ describe('useWorkspaceCleanupGitEvidence', () => { view.rerender({ enabled: false }) await waitFor(() => expect(view.result.current.pendingWorktreeIds.size).toBe(0)) view.rerender({ enabled: true }) - await waitFor(() => expect(holders.scan).toHaveBeenCalledTimes(8)) + await waitFor(() => expect(view.result.current.pendingWorktreeIds.size).toBe(6)) + expect(holders.scan).toHaveBeenCalledTimes(4) expect(view.result.current.pendingWorktreeIds.size).toBe(6) await act(async () => { @@ -63,6 +64,7 @@ describe('useWorkspaceCleanupGitEvidence', () => { errors: [] }) }) + await waitFor(() => expect(holders.scan).toHaveBeenCalledTimes(5)) expect(view.result.current.pendingWorktreeIds.size).toBe(6) expect(view.result.current.evidenceByWorktreeId.size).toBe(0) @@ -87,7 +89,7 @@ describe('useWorkspaceCleanupGitEvidence', () => { await waitFor(() => expect(holders.scan).toHaveBeenCalledTimes(1)) view.rerender({ scannedAt: 2 }) - await waitFor(() => expect(holders.scan).toHaveBeenCalledTimes(2)) + expect(holders.scan).toHaveBeenCalledTimes(1) await act(async () => { pending[0]?.resolve({ @@ -96,6 +98,7 @@ describe('useWorkspaceCleanupGitEvidence', () => { errors: [] }) }) + await waitFor(() => expect(holders.scan).toHaveBeenCalledTimes(2)) expect(view.result.current.evidenceByWorktreeId.size).toBe(0) expect(view.result.current.pendingWorktreeIds).toEqual(new Set(['a'])) diff --git a/src/renderer/src/components/workspace-cleanup/use-workspace-cleanup-git-evidence.ts b/src/renderer/src/components/workspace-cleanup/use-workspace-cleanup-git-evidence.ts index 05177cea921..8ca7e538f05 100644 --- a/src/renderer/src/components/workspace-cleanup/use-workspace-cleanup-git-evidence.ts +++ b/src/renderer/src/components/workspace-cleanup/use-workspace-cleanup-git-evidence.ts @@ -44,6 +44,8 @@ export function useWorkspaceCleanupGitEvidence({ const queueRef = useRef([]) const queuedRef = useRef(new Set()) const inFlightRef = useRef(new Set()) + // Why: restarted filters must share one physical RPC/subprocess cap. + const activeRequestWorktreeIdsRef = useRef(new Set()) const totalRef = useRef(0) const enabledRef = useRef(enabled) const scannedAtRef = useRef(scannedAt) @@ -66,15 +68,19 @@ export function useWorkspaceCleanupGitEvidence({ const pump = useCallback(() => { const generation = generationRef.current while ( - inFlightRef.current.size < WORKSPACE_CLEANUP_GIT_EVIDENCE_CONCURRENCY && + activeRequestWorktreeIdsRef.current.size < WORKSPACE_CLEANUP_GIT_EVIDENCE_CONCURRENCY && queueRef.current.length > 0 ) { - const worktreeId = queueRef.current.shift() - if (worktreeId === undefined) { + const queueIndex = queueRef.current.findIndex( + (worktreeId) => !activeRequestWorktreeIdsRef.current.has(worktreeId) + ) + if (queueIndex < 0) { break } + const [worktreeId] = queueRef.current.splice(queueIndex, 1) queuedRef.current.delete(worktreeId) inFlightRef.current.add(worktreeId) + activeRequestWorktreeIdsRef.current.add(worktreeId) attemptedRef.current.add(worktreeId) void scanWorkspaceCleanup({ worktreeId }) .then((result) => { @@ -94,11 +100,10 @@ export function useWorkspaceCleanupGitEvidence({ // attemptedRef keeps it from retrying in a loop. }) .finally(() => { - if (generation !== generationRef.current) { - return + activeRequestWorktreeIdsRef.current.delete(worktreeId) + if (generation === generationRef.current) { + inFlightRef.current.delete(worktreeId) } - inFlightRef.current.delete(worktreeId) - publish() pump() }) }