diff --git a/src/features/recent-projects/main/adapters/output/sources/CodexSessionFileRecentProjectsSourceAdapter.ts b/src/features/recent-projects/main/adapters/output/sources/CodexSessionFileRecentProjectsSourceAdapter.ts index 691fcd59..3e8e7c7d 100644 --- a/src/features/recent-projects/main/adapters/output/sources/CodexSessionFileRecentProjectsSourceAdapter.ts +++ b/src/features/recent-projects/main/adapters/output/sources/CodexSessionFileRecentProjectsSourceAdapter.ts @@ -345,32 +345,50 @@ async function listRecentJsonlFiles( if (!hasBudget()) { return; } - let entries; + let directoryHandle; try { - entries = await fs.readdir(directory, { withFileTypes: true, encoding: 'utf8' }); + directoryHandle = await fs.opendir(directory, { encoding: 'utf8' }); } catch { return; } directoriesVisited += 1; - const filePaths: string[] = []; + const fileBatch: string[] = []; const childDirectories: string[] = []; + const flushFileBatch = async (): Promise => { + if (!fileBatch.length) { + return; + } + const batch = fileBatch.splice(0, fileBatch.length); + await collectFileStats(batch); + }; - for (const entry of entries) { - const entryPath = path.join(directory, entry.name); - if (entry.isDirectory()) { - if (depth < maxDepth) { - childDirectories.push(entryPath); + try { + for await (const entry of directoryHandle) { + if (!hasBudget()) { + return; } - continue; - } - if (entry.isFile() && entry.name.endsWith('.jsonl')) { - filePaths.push(entryPath); + const entryPath = path.join(directory, entry.name); + if (entry.isDirectory()) { + if (depth < maxDepth) { + childDirectories.push(entryPath); + } + continue; + } + + if (entry.isFile() && entry.name.endsWith('.jsonl')) { + fileBatch.push(entryPath); + if (fileBatch.length >= CODEX_SESSION_FILE_DISCOVERY_STAT_BATCH_SIZE) { + await flushFileBatch(); + } + } } + } catch { + return; } - await collectFileStats(filePaths); + await flushFileBatch(); for (const childDirectory of childDirectories) { if (!hasBudget()) { diff --git a/test/features/recent-projects/main/adapters/output/CodexSessionFileRecentProjectsSourceAdapter.test.ts b/test/features/recent-projects/main/adapters/output/CodexSessionFileRecentProjectsSourceAdapter.test.ts index ff4eacf7..3f34660e 100644 --- a/test/features/recent-projects/main/adapters/output/CodexSessionFileRecentProjectsSourceAdapter.test.ts +++ b/test/features/recent-projects/main/adapters/output/CodexSessionFileRecentProjectsSourceAdapter.test.ts @@ -742,6 +742,7 @@ describe('CodexSessionFileRecentProjectsSourceAdapter', () => { ) ) ); + const readdirSpy = vi.spyOn(fs, 'readdir'); const adapter = new CodexSessionFileRecentProjectsSourceAdapter({ getActiveContext: () => ({ type: 'local', id: 'local-1' }) as never, @@ -767,6 +768,7 @@ describe('CodexSessionFileRecentProjectsSourceAdapter', () => { skippedUncached: 340, }) ); + expect(readdirSpy).not.toHaveBeenCalled(); }); it('skips non-interactive and ephemeral sessions', async () => {