Files
orca/src/main/codex/codex-session-backfill-fs-mocks.ts
T
Neil 015f904fca fix(codex): stop re-scanning all Codex session history on every launch (#16251) (#16593)
* fix(codex): stop re-scanning all Codex session history on every launch (#16251)

A launch deleted the backfill completion marker, and a marker could never
be written while a Codex pane was open, so every launch re-derived
"needs full scan" and walked the entire .codex/sessions tree — on Windows
with a large history that read as a hung window.

- v4 marker keeps a durable full-history baseline plus a bounded set of
  pending dates. v3 is read as a baseline, so upgrades pay no full scan.
- A launch now marks dates pending instead of deleting the marker, and a
  full pass certifies the baseline even while a pane is still running;
  the live pane's own date just stays pending.
- Pending dates are persisted, so an abnormal exit or a cross-midnight
  pane recovers a bounded window instead of a full walk.
- A date-limited pass can only extend an existing baseline, never create
  one, so it can no longer certify history it never looked at.
- Marker and index-heal target roots compare through
  normalizeRuntimePathForComparison, so Windows spellings of one
  directory stop invalidating each other.
- Both append-only ledgers stream instead of readFileSync + whole-file
  JSON.parse, keeping the main thread responsive on large histories.

* fix(codex): keep the backfill marker's full-scan demand durable

Review follow-ups on the v4 backfill marker:

- markCodexSessionBackfillMarkerPending no longer erases a persisted
  needsFullScan; the demand survives until a generation-current full walk
  retires it, and the function now reports it so the launch path folds it
  into its own in-memory flag (as @rumoii's #16252 does).
- A full pass settles the whole pending set instead of subtracting the
  empty set, so a date a full walk provably covered stops forcing an extra
  bounded pass on every startup.
- isCodexSessionBackfillDate does a real calendar check, so a corrupted
  marker cannot carry 2026/99/99. No age or future bound: the same guard
  gates rollout publication and a clock-skewed directory holds real
  sessions.
- 'scans only the current date once a baseline exists' now has a second
  date directory, so it fails on a full walk instead of passing either way.
2026-08-26 15:42:54 -07:00

118 lines
4.4 KiB
TypeScript

import type * as NodeFs from 'node:fs'
import type * as NodeFsPromises from 'node:fs/promises'
// Fault-injection doubles for the backfill tests. Kept out of the spec files so
// several suites can drive the same failure modes from one switchboard.
export const fsMockState = {
failLink: false,
failLinkTransiently: false,
failLinkPermission: false,
raceTargetIntoExistence: false,
failMarkerRm: false,
failMarkerReplacement: false,
failAuditMkdirOnce: false,
failAuditWrites: false,
failMkdirPath: null as string | null,
failDirectoryPath: null as string | null,
failLstatPath: null as string | null
}
export function resetCodexSessionBackfillFsMocks(): void {
fsMockState.failLink = false
fsMockState.failLinkTransiently = false
fsMockState.failLinkPermission = false
fsMockState.raceTargetIntoExistence = false
fsMockState.failMarkerRm = false
fsMockState.failMarkerReplacement = false
fsMockState.failAuditMkdirOnce = false
fsMockState.failAuditWrites = false
fsMockState.failMkdirPath = null
fsMockState.failDirectoryPath = null
fsMockState.failLstatPath = null
}
function errnoError(message: string, code: string): NodeJS.ErrnoException {
const error = new Error(message) as NodeJS.ErrnoException
error.code = code
return error
}
function isMarkerPath(value: unknown): boolean {
return (
String(value).includes('codex-session-backfill') &&
String(value).endsWith('backfill-complete.json')
)
}
export function createNodeFsMock(actual: typeof NodeFs): typeof NodeFs {
return {
...actual,
existsSync: (...args: Parameters<typeof actual.existsSync>) =>
args[0] === fsMockState.failLstatPath ? false : actual.existsSync(...args),
rmSync: (...args: Parameters<typeof actual.rmSync>) => {
if (fsMockState.failMarkerRm && isMarkerPath(args[0])) {
throw errnoError('EACCES: marker removal failed', 'EACCES')
}
return actual.rmSync(...args)
},
renameSync: (...args: Parameters<typeof actual.renameSync>) => {
if (fsMockState.failMarkerReplacement && isMarkerPath(args[1])) {
throw errnoError('EACCES: marker replacement failed', 'EACCES')
}
return actual.renameSync(...args)
}
}
}
export function createNodeFsPromisesMock(actual: typeof NodeFsPromises): typeof NodeFsPromises {
return {
...actual,
mkdir: (...args: Parameters<typeof actual.mkdir>) => {
if (args[0] === fsMockState.failMkdirPath) {
throw errnoError('EACCES: target directory inaccessible', 'EACCES')
}
if (fsMockState.failAuditMkdirOnce && String(args[0]).includes('codex-session-backfill')) {
fsMockState.failAuditMkdirOnce = false
throw errnoError('EACCES: transient audit directory failure', 'EACCES')
}
return actual.mkdir(...args)
},
appendFile: (...args: Parameters<typeof actual.appendFile>) => {
if (fsMockState.failAuditWrites && String(args[0]).includes('codex-session-backfill')) {
throw errnoError('ENOSPC: audit write failed', 'ENOSPC')
}
return actual.appendFile(...args)
},
lstat: (...args: Parameters<typeof actual.lstat>) => {
if (args[0] === fsMockState.failLstatPath) {
throw errnoError('EACCES: path inaccessible', 'EACCES')
}
return actual.lstat(...args)
},
link: async (...args: Parameters<typeof actual.link>) => {
if (fsMockState.raceTargetIntoExistence && String(args[0]).includes('codex-runtime-home')) {
fsMockState.raceTargetIntoExistence = false
await actual.writeFile(args[1], 'concurrent target\n', 'utf-8')
throw errnoError('EEXIST: concurrent target', 'EEXIST')
}
if (fsMockState.failLink && String(args[0]).includes('codex-runtime-home')) {
throw errnoError('EXDEV: cross-device link', 'EXDEV')
}
if (fsMockState.failLinkTransiently && String(args[0]).includes('codex-runtime-home')) {
throw errnoError('EIO: transient hardlink failure', 'EIO')
}
if (fsMockState.failLinkPermission && String(args[0]).includes('codex-runtime-home')) {
throw errnoError('EACCES: hardlink permission denied', 'EACCES')
}
return actual.link(...args)
},
opendir: (...args: Parameters<typeof actual.opendir>) => {
if (args[0] === fsMockState.failDirectoryPath) {
throw errnoError('EACCES: directory unreadable', 'EACCES')
}
return actual.opendir(...args)
}
} as typeof NodeFsPromises
}