fix(codex): bound failed session audit events

This commit is contained in:
Brennan Benson
2026-08-04 19:05:55 -07:00
parent 64dff91208
commit aca37a89fb
4 changed files with 156 additions and 13 deletions
@@ -3,6 +3,7 @@ import { lstat } from 'node:fs/promises'
import {
appendCodexSessionHealAuditRecord,
createCodexSessionBackfillAuditWriter,
createCodexSessionBackfillDiagnosticEventId,
createCodexSessionBackfillFileEventId,
readCodexSessionBackfillAuditCoverage,
recordExistingCodexSessionForHeal,
@@ -24,6 +25,18 @@ export type CodexSessionBackfillAuditPass = {
source: string,
target: string
): Promise<void>
recordDiagnostic(
record: {
action: 'copy-unsupported' | 'failed'
source: string
target: string
error?: string
linkError?: string
errorCode?: string
linkErrorCode?: string
},
sourceStat: Stats | null
): Promise<void>
finish(summary: CodexSessionBackfillSummary): Promise<void>
}
@@ -75,6 +88,18 @@ export async function createCodexSessionBackfillAuditPass(
}
}
},
async recordDiagnostic(record, sourceStat): Promise<void> {
const diagnosticEventId = createCodexSessionBackfillDiagnosticEventId({
...record,
sourceStat
})
if (coverage.diagnosticEventIds.has(diagnosticEventId)) {
return
}
if (await appendRecord({ ...record, diagnosticEventId })) {
coverage.diagnosticEventIds.add(diagnosticEventId)
}
},
async finish(summary): Promise<void> {
if (auditChanged || !coverage.hasRunSummary) {
await appendRecord({ action: 'run-summary', ...summary })
@@ -10,10 +10,12 @@ export type CodexSessionBackfillAuditWriter = (record: Record<string, unknown>)
export type CodexSessionBackfillAuditCoverage = {
fileEventIds: Set<string>
diagnosticEventIds: Set<string>
hasRunSummary: boolean
}
const HEAL_AUDIT_ACTIONS = new Set(['hardlink', 'copy', 'existing'])
const DIAGNOSTIC_AUDIT_ACTIONS = new Set(['copy-unsupported', 'failed'])
export function createCodexSessionBackfillAuditWriter(
auditLogPath: string
@@ -63,6 +65,7 @@ export async function readCodexSessionBackfillAuditCoverage(
): Promise<CodexSessionBackfillAuditCoverage> {
const coverage: CodexSessionBackfillAuditCoverage = {
fileEventIds: new Set<string>(),
diagnosticEventIds: new Set<string>(),
hasRunSummary: false
}
const input = createReadStream(auditLogPath, { encoding: 'utf-8' })
@@ -83,6 +86,13 @@ export async function readCodexSessionBackfillAuditCoverage(
) {
coverage.fileEventIds.add(record.fileEventId)
}
if (
typeof record.action === 'string' &&
DIAGNOSTIC_AUDIT_ACTIONS.has(record.action) &&
typeof record.diagnosticEventId === 'string'
) {
coverage.diagnosticEventIds.add(record.diagnosticEventId)
}
} catch {
// Torn audit tails are quarantined by the writer's leading newline.
}
@@ -107,6 +117,48 @@ export function createCodexSessionBackfillFileEventId(targetPath: string, stat:
.digest('hex')
}
export function createCodexSessionBackfillDiagnosticEventId(args: {
action: 'copy-unsupported' | 'failed'
source: string
target: string
sourceStat: Stats | null
errorCode?: string
linkErrorCode?: string
}): string {
const sourceIdentity = args.sourceStat
? [
args.sourceStat.dev,
args.sourceStat.ino,
args.sourceStat.birthtimeMs,
args.sourceStat.size,
args.sourceStat.mtimeMs,
args.sourceStat.ctimeMs
].join('\0')
: 'unavailable'
return createHash('sha256')
.update(args.action)
.update('\0')
.update(normalizeRuntimePathForComparison(args.source))
.update('\0')
.update(normalizeRuntimePathForComparison(args.target))
.update('\0')
.update(sourceIdentity)
.update('\0')
.update(args.errorCode ?? '')
.update('\0')
.update(args.linkErrorCode ?? '')
.digest('hex')
}
export function describeCodexSessionBackfillErrorCode(error: unknown): string {
const code = (error as NodeJS.ErrnoException | null)?.code
return typeof code === 'string' && code
? code
: error instanceof Error
? error.name
: typeof error
}
export async function appendCodexSessionHealAuditRecord(
writer: CodexSessionBackfillAuditWriter,
summary: CodexSessionBackfillSummary,
+56 -1
View File
@@ -110,7 +110,9 @@ vi.mock('node:fs/promises', async () => {
// Simulate a copy that fails after opening its destination, which is
// the dangerous case for resumability rather than a preflight error.
await actual.writeFile(args[1], 'partial copy\n', 'utf-8')
const error = new Error('EACCES: copy disabled for test') as NodeJS.ErrnoException
const error = new Error(
`EACCES: copy disabled for test, '${String(args[1])}'`
) as NodeJS.ErrnoException
error.code = 'EACCES'
throw error
}
@@ -173,6 +175,7 @@ type BackfillAuditRecord = {
action: string
target?: string
fileEventId?: string
diagnosticEventId?: string
}
function readBackfillAuditRecords(): BackfillAuditRecord[] {
@@ -636,9 +639,61 @@ describe('startCodexSessionBackfillInBackground', () => {
expect(first).toMatchObject({ skippedUnsupportedFilesystemFiles: 1, failedFiles: 0 })
expect(existsSync(getMarkerPath())).toBe(true)
const firstAudit = readFileSync(getAuditLogPath(), 'utf-8')
invalidateCodexSessionBackfillMarker(getMarkerPath())
const repeated = await startCodexSessionBackfillInBackground()
expect(repeated).toMatchObject({ skippedUnsupportedFilesystemFiles: 1, failedFiles: 0 })
expect(readFileSync(getAuditLogPath(), 'utf-8')).toBe(firstAudit)
expect(await startCodexSessionBackfillInBackground()).toBeNull()
})
it('keeps repeated unchanged per-file failures audit-stable', async () => {
fsMockState.failLink = true
fsMockState.failCopy = true
writeManagedSession(join('2026', '05', '26', 'rollout-a.jsonl'), '{"id":"a"}\n')
const first = await startCodexSessionBackfillInBackground()
expect(first).toMatchObject({ failedFiles: 1 })
const firstAudit = readFileSync(getAuditLogPath(), 'utf-8')
const repeated = await startCodexSessionBackfillInBackground()
expect(repeated).toMatchObject({ failedFiles: 1 })
expect(readFileSync(getAuditLogPath(), 'utf-8')).toBe(firstAudit)
})
it('records a new failure event when the source file changes', async () => {
fsMockState.failLink = true
fsMockState.failCopy = true
const relativePath = join('2026', '05', '26', 'rollout-a.jsonl')
writeManagedSession(relativePath, '{"id":"a"}\n')
await startCodexSessionBackfillInBackground()
writeManagedSession(relativePath, '{"id":"a","changed":true}\n')
await startCodexSessionBackfillInBackground()
const failedRecords = readBackfillAuditRecords().filter((record) => record.action === 'failed')
expect(failedRecords).toHaveLength(2)
expect(new Set(failedRecords.map((record) => record.diagnosticEventId)).size).toBe(2)
})
it('records a new failure event when the filesystem outcome changes', async () => {
fsMockState.failLink = true
fsMockState.failCopy = true
writeManagedSession(join('2026', '05', '26', 'rollout-a.jsonl'), '{"id":"a"}\n')
await startCodexSessionBackfillInBackground()
fsMockState.failCopy = false
fsMockState.failInstallLinkTransiently = true
await startCodexSessionBackfillInBackground()
const failedRecords = readBackfillAuditRecords().filter((record) => record.action === 'failed')
expect(failedRecords).toHaveLength(2)
expect(new Set(failedRecords.map((record) => record.diagnosticEventId)).size).toBe(2)
})
it('leaves the marker unset when any file fails so the next startup retries', async () => {
fsMockState.failLink = true
fsMockState.failCopy = true
+23 -12
View File
@@ -10,6 +10,7 @@ import {
readCodexSessionTargetStat,
type CodexSessionBackfillAuditPass
} from './codex-session-backfill-audit-pass'
import { describeCodexSessionBackfillErrorCode } from './codex-session-backfill-audit'
import {
copySessionFileWithoutOverwrite,
isAtomicNoReplaceUnsupportedError
@@ -300,21 +301,31 @@ async function backfillOneManagedSessionFile(
}
if (isAtomicNoReplaceUnsupportedError(copyError)) {
summary.skippedUnsupportedFilesystemFiles += 1
await auditPass.appendRecord({
action: 'copy-unsupported',
source: managedSessionFilePath,
target: systemSessionFilePath
})
await auditPass.recordDiagnostic(
{
action: 'copy-unsupported',
source: managedSessionFilePath,
target: systemSessionFilePath,
errorCode: describeCodexSessionBackfillErrorCode(copyError),
linkErrorCode: describeCodexSessionBackfillErrorCode(linkError)
},
await readCodexSessionTargetStat(managedSessionFilePath)
)
return
}
summary.failedFiles += 1
await auditPass.appendRecord({
action: 'failed',
source: managedSessionFilePath,
target: systemSessionFilePath,
error: describeError(copyError),
linkError: describeError(linkError)
})
await auditPass.recordDiagnostic(
{
action: 'failed',
source: managedSessionFilePath,
target: systemSessionFilePath,
error: describeError(copyError),
linkError: describeError(linkError),
errorCode: describeCodexSessionBackfillErrorCode(copyError),
linkErrorCode: describeCodexSessionBackfillErrorCode(linkError)
},
await readCodexSessionTargetStat(managedSessionFilePath)
)
}
}
}