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.
This commit is contained in:
777genius 2026-05-31 17:55:01 +03:00
parent 95e0f34d31
commit 816ff210b7
2 changed files with 65 additions and 6 deletions

View file

@ -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,

View file

@ -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,