From 816ff210b759fff117afb1d91d2398dc40241fd4 Mon Sep 17 00:00:00 2001 From: 777genius Date: Sun, 31 May 2026 17:55:01 +0300 Subject: [PATCH] fix(recent-projects): keep codex partial warning only for empty results Large Codex histories routinely hit the scan budget while still returning useful project candidates, so the detailed "partial" warning was firing on healthy degraded runs. Only warn when a degraded scan yields zero candidates; otherwise the run is logged at info level with the degraded flag. --- ...xSessionFileRecentProjectsSourceAdapter.ts | 4 +- ...ionFileRecentProjectsSourceAdapter.test.ts | 67 +++++++++++++++++-- 2 files changed, 65 insertions(+), 6 deletions(-) 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,