writeCacheEntry() in lib/codex-sessions.ts does a read-modify-write with no atomic rename:
function writeCacheEntry(sessionId: string, path: string): void {
try {
mkdirSync(dirname(CACHE_PATH), { recursive: true });
const cache = readCache();
cache[sessionId] = path;
writeFileSync(CACHE_PATH, JSON.stringify(cache), "utf-8"); // not atomic
} catch {
// Cache is best-effort
}
}
The dashboard fans out Codex session lookups in parallel on the projects page, so these writes can interleave.
Why it matters
A lost entry isn't the problem — that's just a cache miss, and the next lookup rescans and repopulates it.
The problem is a torn file. If a write is interrupted partway, readCache() hits invalid JSON, throws, and the entire cache map is lost — not one entry. That turns a harmless race into a full cache wipe.
Where to fix
lib/codex-sessions.ts:
- Write to a process-unique temp path —
${CACHE_PATH}.${process.pid}.tmp
renameSync() it into place (atomic on the same filesystem)
- On failure, unlink the temp file so nothing is left behind
Keep the whole thing best-effort: the cache is an optimisation and must never throw into the caller.
Done when
- a test proves the cache file is written correctly and no
.tmp files survive
- existing
lib/codex-sessions tests still pass
_getCacheFilePath() is already exported from that module for exactly this kind of test.
Small and self-contained. Supersedes #276, which was closed while the fix was still unmerged, so it never landed.
writeCacheEntry()inlib/codex-sessions.tsdoes a read-modify-write with no atomic rename:The dashboard fans out Codex session lookups in parallel on the projects page, so these writes can interleave.
Why it matters
A lost entry isn't the problem — that's just a cache miss, and the next lookup rescans and repopulates it.
The problem is a torn file. If a write is interrupted partway,
readCache()hits invalid JSON, throws, and the entire cache map is lost — not one entry. That turns a harmless race into a full cache wipe.Where to fix
lib/codex-sessions.ts:${CACHE_PATH}.${process.pid}.tmprenameSync()it into place (atomic on the same filesystem)Keep the whole thing best-effort: the cache is an optimisation and must never throw into the caller.
Done when
.tmpfiles survivelib/codex-sessionstests still pass_getCacheFilePath()is already exported from that module for exactly this kind of test.Small and self-contained. Supersedes #276, which was closed while the fix was still unmerged, so it never landed.