fix(recent-projects): skip oversized codex session cache
This commit is contained in:
parent
8c86def84d
commit
031e5eda2f
2 changed files with 66 additions and 0 deletions
|
|
@ -30,6 +30,7 @@ const CODEX_SESSION_FILE_CACHE_RELATIVE_PATH = path.join(
|
||||||
'recent-projects',
|
'recent-projects',
|
||||||
'codex-session-files-index.json'
|
'codex-session-files-index.json'
|
||||||
);
|
);
|
||||||
|
const CODEX_SESSION_FILE_CACHE_MAX_BYTES = 4 * 1024 * 1024;
|
||||||
|
|
||||||
interface CodexSessionFileEntry {
|
interface CodexSessionFileEntry {
|
||||||
filePath: string;
|
filePath: string;
|
||||||
|
|
@ -663,6 +664,16 @@ export class CodexSessionFileRecentProjectsSourceAdapter implements RecentProjec
|
||||||
|
|
||||||
async #readCacheSafe(): Promise<CodexSessionFileCacheFile> {
|
async #readCacheSafe(): Promise<CodexSessionFileCacheFile> {
|
||||||
try {
|
try {
|
||||||
|
const stats = await fs.stat(this.#cachePath);
|
||||||
|
if (stats.size > CODEX_SESSION_FILE_CACHE_MAX_BYTES) {
|
||||||
|
this.deps.logger.warn('codex session-file recent-projects cache skipped - too large', {
|
||||||
|
cachePath: this.#cachePath,
|
||||||
|
bytes: stats.size,
|
||||||
|
maxBytes: CODEX_SESSION_FILE_CACHE_MAX_BYTES,
|
||||||
|
});
|
||||||
|
return emptyCache();
|
||||||
|
}
|
||||||
|
|
||||||
const raw = await fs.readFile(this.#cachePath, 'utf8');
|
const raw = await fs.readFile(this.#cachePath, 'utf8');
|
||||||
const parsed = JSON.parse(raw) as Partial<CodexSessionFileCacheFile>;
|
const parsed = JSON.parse(raw) as Partial<CodexSessionFileCacheFile>;
|
||||||
if (
|
if (
|
||||||
|
|
|
||||||
|
|
@ -596,6 +596,61 @@ describe('CodexSessionFileRecentProjectsSourceAdapter', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('skips an oversized legacy session-file cache before reading it', 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;
|
||||||
|
await writeRollout(
|
||||||
|
path.join(codexHome, 'sessions', '2026', '04', '14', 'rollout-alpha.jsonl'),
|
||||||
|
{
|
||||||
|
cwd: '/Users/test/projects/alpha',
|
||||||
|
},
|
||||||
|
new Date('2026-04-14T12:00:00.000Z')
|
||||||
|
);
|
||||||
|
const cachePath = getSessionFileCachePath(appDataPath);
|
||||||
|
await fs.mkdir(path.dirname(cachePath), { recursive: true });
|
||||||
|
await fs.writeFile(cachePath, 'x', 'utf8');
|
||||||
|
await fs.truncate(cachePath, 4 * 1024 * 1024 + 1);
|
||||||
|
const readFileSpy = vi.spyOn(fs, 'readFile');
|
||||||
|
|
||||||
|
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(readFileSpy).not.toHaveBeenCalledWith(cachePath, 'utf8');
|
||||||
|
expect(result).toEqual({
|
||||||
|
candidates: [
|
||||||
|
expect.objectContaining({
|
||||||
|
primaryPath: '/Users/test/projects/alpha',
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
degraded: false,
|
||||||
|
});
|
||||||
|
expect(logger.warn).toHaveBeenCalledWith(
|
||||||
|
'codex session-file recent-projects cache skipped - too large',
|
||||||
|
expect.objectContaining({
|
||||||
|
bytes: 4 * 1024 * 1024 + 1,
|
||||||
|
maxBytes: 4 * 1024 * 1024,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
await expect(fs.stat(cachePath)).resolves.toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
size: expect.any(Number),
|
||||||
|
})
|
||||||
|
);
|
||||||
|
expect((await fs.stat(cachePath)).size).toBeLessThan(4 * 1024 * 1024);
|
||||||
|
});
|
||||||
|
|
||||||
it('returns a degraded partial result under the uncached read cap and completes on the next cached pass', async () => {
|
it('returns a degraded partial result under the uncached read cap and completes on the next cached pass', async () => {
|
||||||
const codexHome = path.join(tempDir, '.codex');
|
const codexHome = path.join(tempDir, '.codex');
|
||||||
const appDataPath = path.join(tempDir, 'app-data');
|
const appDataPath = path.join(tempDir, 'app-data');
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue