From 64dff91208d94aa3d6c529c09b2f7a2f7dde93ef Mon Sep 17 00:00:00 2001 From: Brennan Benson <79079362+brennanb2025@users.noreply.github.com> Date: Tue, 4 Aug 2026 19:02:49 -0700 Subject: [PATCH] fix(codex): preserve delayed session publication passes --- .../codex-accounts/runtime-home-service.ts | 10 ++- .../codex-session-migration-scheduler.test.ts | 61 +++++++++++++++++++ .../codex-session-migration-scheduler.ts | 23 +++++-- src/main/index.ts | 1 + 4 files changed, 88 insertions(+), 7 deletions(-) diff --git a/src/main/codex-accounts/runtime-home-service.ts b/src/main/codex-accounts/runtime-home-service.ts index 0d6d2dada41..a458f81954b 100644 --- a/src/main/codex-accounts/runtime-home-service.ts +++ b/src/main/codex-accounts/runtime-home-service.ts @@ -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//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 } diff --git a/src/main/codex/codex-session-migration-scheduler.test.ts b/src/main/codex/codex-session-migration-scheduler.test.ts index 357fc46b9ca..d25b349095f 100644 --- a/src/main/codex/codex-session-migration-scheduler.test.ts +++ b/src/main/codex/codex-session-migration-scheduler.test.ts @@ -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((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 () => { diff --git a/src/main/codex/codex-session-migration-scheduler.ts b/src/main/codex/codex-session-migration-scheduler.ts index b23744c1db2..bcb94e7e4cd 100644 --- a/src/main/codex/codex-session-migration-scheduler.ts +++ b/src/main/codex/codex-session-migration-scheduler.ts @@ -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 | null = null + let scheduledRunGeneration = 0 + let pendingScheduledRunGeneration: number | null = null let migrationTask: Promise | 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() } diff --git a/src/main/index.ts b/src/main/index.ts index 2412493021c..9493c449fac 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -2225,6 +2225,7 @@ void app.whenReady().then(async () => { isQuitting: () => isQuitting, resolveSystemCodexHomePathOverride: () => resolveHostCodexSessionSourceHome(store!.getSettings()), + prepareScheduledRun: () => codexRuntimeHome?.prepareHostSystemDefaultSessionMigrationPass(), startBackfill: startCodexSessionBackfillInBackground, startIndexHeal: startCodexSessionIndexHealInBackground })