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 3e8e7c7d..466a6598 100644 --- a/src/features/recent-projects/main/adapters/output/sources/CodexSessionFileRecentProjectsSourceAdapter.ts +++ b/src/features/recent-projects/main/adapters/output/sources/CodexSessionFileRecentProjectsSourceAdapter.ts @@ -643,7 +643,9 @@ export class CodexSessionFileRecentProjectsSourceAdapter implements RecentProjec .sort((left, right) => right.lastActivityAt - left.lastActivityAt) .slice(0, CODEX_PROJECT_CANDIDATE_LIMIT); const durationMs = Date.now() - startedAt; - if (degraded) { + // Large Codex histories often hit scan budgets while still producing useful candidates. + // Keep the detailed partial warning for user-visible empty results only. + if (degraded && snapshots.length === 0) { this.deps.logger.warn('codex session-file recent-projects source partial', { files: candidateFiles.length, visitedFiles, 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 3f34660e..ee4efee0 100644 --- a/test/features/recent-projects/main/adapters/output/CodexSessionFileRecentProjectsSourceAdapter.test.ts +++ b/test/features/recent-projects/main/adapters/output/CodexSessionFileRecentProjectsSourceAdapter.test.ts @@ -550,10 +550,65 @@ describe('CodexSessionFileRecentProjectsSourceAdapter', () => { expect(result.candidates.map((candidate) => candidate.primaryPath)).toEqual([ '/Users/test/projects/fast', ]); + expect(logger.info).toHaveBeenCalledWith( + 'codex session-file recent-projects source loaded', + expect.objectContaining({ + degraded: true, + files: 2, + timedOutReads: 1, + }) + ); + }); + + it('keeps a partial warning when no recent project candidates are available', async () => { + const codexHome = path.join(tempDir, '.codex'); + const appDataPath = path.join(tempDir, 'app-data'); + const logger = createLogger(); + const identityResolver = { + resolve: vi.fn().mockResolvedValue(null), + } as unknown as RecentProjectIdentityResolver; + const slowSessionPath = path.join( + codexHome, + 'sessions', + '2026', + '04', + '14', + 'rollout-slow.jsonl' + ); + await writeRollout( + slowSessionPath, + { + cwd: '/Users/test/projects/slow', + }, + new Date('2026-04-14T12:00:00.000Z') + ); + const originalOpen = fs.open.bind(fs); + vi.spyOn(fs, 'open').mockImplementation(async (...args) => { + if (String(args[0]) === slowSessionPath) { + await new Promise((resolve) => setTimeout(resolve, 2500)); + } + return originalOpen(...args); + }); + + const adapter = new CodexSessionFileRecentProjectsSourceAdapter({ + getActiveContext: () => ({ type: 'local', id: 'local-1' }) as never, + getLocalContext: () => ({ type: 'local', id: 'local-1' }) as never, + identityResolver, + logger, + codexHome, + appDataPath, + }); + const result = await adapter.list(); + + expect(result).toEqual({ + candidates: [], + degraded: true, + }); expect(logger.warn).toHaveBeenCalledWith( 'codex session-file recent-projects source partial', expect.objectContaining({ - files: 2, + candidates: 0, + files: 1, timedOutReads: 1, }) ); @@ -695,9 +750,10 @@ describe('CodexSessionFileRecentProjectsSourceAdapter', () => { expect(firstResult.candidates.map((candidate) => candidate.primaryPath)).toEqual([ '/Users/test/projects/alpha', ]); - expect(logger.warn).toHaveBeenCalledWith( - 'codex session-file recent-projects source partial', + expect(logger.info).toHaveBeenCalledWith( + 'codex session-file recent-projects source loaded', expect.objectContaining({ + degraded: true, files: 171, uncachedReads: 160, skippedUncached: 11, @@ -758,9 +814,10 @@ describe('CodexSessionFileRecentProjectsSourceAdapter', () => { expect(result.candidates.map((candidate) => candidate.primaryPath)).toEqual([ '/Users/test/projects/alpha', ]); - expect(logger.warn).toHaveBeenCalledWith( - 'codex session-file recent-projects source partial', + expect(logger.info).toHaveBeenCalledWith( + 'codex session-file recent-projects source loaded', expect.objectContaining({ + degraded: true, files: 500, visitedFiles: 505, droppedOlderFiles: 5,