fix(codex): preserve delayed session publication passes

This commit is contained in:
Brennan Benson
2026-08-04 19:02:49 -07:00
parent d0981ddb03
commit 64dff91208
4 changed files with 88 additions and 7 deletions
@@ -270,6 +270,12 @@ export class CodexRuntimeHomeService {
)
}
prepareHostSystemDefaultSessionMigrationPass(): void {
invalidateCodexSessionBackfillMarker(
join(getCodexSessionBackfillStateDirPath(), 'backfill-complete.json')
)
}
// Why: with the real-home flag ON, a managed HOST account runs against its own
// self-contained CODEX_HOME (codex-accounts/<id>/home) instead of the shared
// runtime mirror. Its auth.json lives there and codex refreshes it in place,
@@ -441,9 +447,7 @@ export class CodexRuntimeHomeService {
) {
return false
}
invalidateCodexSessionBackfillMarker(
join(getCodexSessionBackfillStateDirPath(), 'backfill-complete.json')
)
this.prepareHostSystemDefaultSessionMigrationPass()
return true
}
@@ -8,12 +8,14 @@ describe('createCodexSessionMigrationScheduler', () => {
it('runs after a managed-account startup switches to host system default', async () => {
let eligible = false
const prepareScheduledRun = vi.fn()
const startBackfill = vi.fn().mockResolvedValue(null)
const startIndexHeal = vi.fn().mockResolvedValue(null)
const scheduler = createCodexSessionMigrationScheduler({
isEligible: () => eligible,
isQuitting: () => false,
resolveSystemCodexHomePathOverride: () => undefined,
prepareScheduledRun,
startBackfill,
startIndexHeal
})
@@ -26,15 +28,18 @@ describe('createCodexSessionMigrationScheduler', () => {
scheduler.requestRun()
await vi.waitFor(() => expect(startIndexHeal).toHaveBeenCalledOnce())
expect(startBackfill).toHaveBeenCalledOnce()
expect(prepareScheduledRun).not.toHaveBeenCalled()
})
it('schedules a delayed rerun after a shared-home launch', async () => {
const prepareScheduledRun = vi.fn()
const startBackfill = vi.fn().mockResolvedValue(null)
const startIndexHeal = vi.fn().mockResolvedValue(null)
const scheduler = createCodexSessionMigrationScheduler({
isEligible: () => true,
isQuitting: () => false,
resolveSystemCodexHomePathOverride: () => undefined,
prepareScheduledRun,
startBackfill,
startIndexHeal,
initialDelayMs: 1_000
@@ -48,15 +53,18 @@ describe('createCodexSessionMigrationScheduler', () => {
await vi.advanceTimersByTimeAsync(1)
await vi.waitFor(() => expect(startIndexHeal).toHaveBeenCalledOnce())
expect(startBackfill).toHaveBeenCalledOnce()
expect(prepareScheduledRun).toHaveBeenCalledOnce()
})
it('delays the startup run from the latest shared-home launch', async () => {
const prepareScheduledRun = vi.fn()
const startBackfill = vi.fn().mockResolvedValue(null)
const startIndexHeal = vi.fn().mockResolvedValue(null)
const scheduler = createCodexSessionMigrationScheduler({
isEligible: () => true,
isQuitting: () => false,
resolveSystemCodexHomePathOverride: () => undefined,
prepareScheduledRun,
startBackfill,
startIndexHeal,
initialDelayMs: 1_000
@@ -72,10 +80,12 @@ describe('createCodexSessionMigrationScheduler', () => {
await vi.advanceTimersByTimeAsync(999)
await vi.waitFor(() => expect(startIndexHeal).toHaveBeenCalledOnce())
expect(startBackfill).toHaveBeenCalledOnce()
expect(prepareScheduledRun).toHaveBeenCalledOnce()
})
it('preserves a delayed launch rerun while an earlier migration is active', async () => {
let releaseFirstIndexHeal: (() => void) | undefined
const prepareScheduledRun = vi.fn()
const startBackfill = vi.fn().mockResolvedValue(null)
const startIndexHeal = vi
.fn()
@@ -90,6 +100,7 @@ describe('createCodexSessionMigrationScheduler', () => {
isEligible: () => true,
isQuitting: () => false,
resolveSystemCodexHomePathOverride: () => undefined,
prepareScheduledRun,
startBackfill,
startIndexHeal,
initialDelayMs: 1_000
@@ -101,10 +112,60 @@ describe('createCodexSessionMigrationScheduler', () => {
scheduler.scheduleRun()
await vi.advanceTimersByTimeAsync(1_000)
expect(startBackfill).toHaveBeenCalledOnce()
expect(prepareScheduledRun).not.toHaveBeenCalled()
releaseFirstIndexHeal?.()
await vi.waitFor(() => expect(startBackfill).toHaveBeenCalledTimes(2))
await vi.waitFor(() => expect(startIndexHeal).toHaveBeenCalledTimes(2))
expect(prepareScheduledRun).toHaveBeenCalledOnce()
expect(prepareScheduledRun.mock.invocationCallOrder[0]).toBeLessThan(
startBackfill.mock.invocationCallOrder[1]!
)
})
it('prepares a delayed launch pass after an earlier migration settles before the timer', async () => {
let releaseFirstBackfill: (() => void) | undefined
let markerPresent = false
const prepareScheduledRun = vi.fn(() => {
markerPresent = false
})
const startBackfill = vi
.fn()
.mockImplementationOnce(
() =>
new Promise<void>((resolve) => {
releaseFirstBackfill = () => {
markerPresent = true
resolve()
}
})
)
.mockImplementationOnce(async () => {
expect(markerPresent).toBe(false)
})
const startIndexHeal = vi.fn().mockResolvedValue(null)
const scheduler = createCodexSessionMigrationScheduler({
isEligible: () => true,
isQuitting: () => false,
resolveSystemCodexHomePathOverride: () => undefined,
prepareScheduledRun,
startBackfill,
startIndexHeal,
initialDelayMs: 1_000
})
scheduler.requestRun()
scheduler.scheduleRun()
releaseFirstBackfill?.()
await Promise.resolve()
await Promise.resolve()
await Promise.resolve()
expect(markerPresent).toBe(true)
expect(startIndexHeal).toHaveBeenCalledOnce()
await vi.advanceTimersByTimeAsync(1_000)
await vi.waitFor(() => expect(startBackfill).toHaveBeenCalledTimes(2))
expect(prepareScheduledRun).toHaveBeenCalledOnce()
})
it('coalesces concurrent run requests and stops before index heal after opt-out', async () => {
@@ -15,16 +15,25 @@ export function createCodexSessionMigrationScheduler(args: {
isEligible: () => boolean
isQuitting: () => boolean
resolveSystemCodexHomePathOverride: () => string | undefined
prepareScheduledRun?: () => void
startBackfill: MigrationRun
startIndexHeal: MigrationRun
initialDelayMs?: number
}): CodexSessionMigrationScheduler {
let scheduledTimer: ReturnType<typeof setTimeout> | null = null
let scheduledRunGeneration = 0
let pendingScheduledRunGeneration: number | null = null
let migrationTask: Promise<void> | null = null
let activeRunStopObserved = false
let rerunRequested = false
const requestRun = (rerunIfActive = false): void => {
const requestRun = (rerunIfActive = false, requestedGeneration?: number): void => {
if (requestedGeneration !== undefined) {
pendingScheduledRunGeneration = Math.max(
requestedGeneration,
pendingScheduledRunGeneration ?? requestedGeneration
)
}
if (args.isQuitting() || !args.isEligible()) {
return
}
@@ -33,6 +42,11 @@ export function createCodexSessionMigrationScheduler(args: {
rerunRequested ||= rerunIfActive || activeRunStopObserved
return
}
if (pendingScheduledRunGeneration !== null) {
pendingScheduledRunGeneration = null
// Why: an older active pass can rewrite the marker after launch invalidates it.
args.prepareScheduledRun?.()
}
activeRunStopObserved = false
rerunRequested = false
const shouldStop = (): boolean => {
@@ -69,11 +83,11 @@ export function createCodexSessionMigrationScheduler(args: {
})
}
const armScheduledRun = (): void => {
const armScheduledRun = (generation?: number): void => {
scheduledTimer = setTimeout(() => {
scheduledTimer = null
// Why: a launch can invalidate the marker while a long index-heal pass is active.
requestRun(true)
requestRun(true, generation)
}, args.initialDelayMs ?? 15_000)
}
@@ -88,7 +102,8 @@ export function createCodexSessionMigrationScheduler(args: {
if (scheduledTimer) {
clearTimeout(scheduledTimer)
}
armScheduledRun()
scheduledRunGeneration += 1
armScheduledRun(scheduledRunGeneration)
},
requestRun: () => requestRun()
}
+1
View File
@@ -2225,6 +2225,7 @@ void app.whenReady().then(async () => {
isQuitting: () => isQuitting,
resolveSystemCodexHomePathOverride: () =>
resolveHostCodexSessionSourceHome(store!.getSettings()),
prepareScheduledRun: () => codexRuntimeHome?.prepareHostSystemDefaultSessionMigrationPass(),
startBackfill: startCodexSessionBackfillInBackground,
startIndexHeal: startCodexSessionIndexHealInBackground
})