mirror of
https://github.com/stablyai/orca.git
synced 2026-10-28 00:02:54 +00:00
fix(codex): bound failed session audit events
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user