fix(codex): publish Windows system-default sessions

This commit is contained in:
Brennan Benson
2026-08-04 18:08:19 -07:00
parent 39c3c58d55
commit 0f73b814a0
9 changed files with 154 additions and 32 deletions
@@ -1758,9 +1758,12 @@ describe('CodexRuntimeHomeService', () => {
const store = createStore(createSettings())
const { CodexRuntimeHomeService } = await import('./runtime-home-service')
const service = new CodexRuntimeHomeService(store as never)
const scheduleSessionMigration = vi.fn()
service.setHostSystemDefaultSessionMigrationScheduler(scheduleSessionMigration)
expect(service.prepareForCodexLaunch()).toBe(getRuntimeCodexHomePath())
expect(existsSync(markerPath)).toBe(false)
expect(scheduleSessionMigration).toHaveBeenCalledOnce()
expect(service.prepareForRateLimitFetch()).toBe(getRuntimeCodexHomePath())
expect(service.getHostCodexHomePathsForSessionDiscovery()).toEqual([getRuntimeCodexHomePath()])
expect(existsSync(getRuntimeCodexHomePath())).toBe(true)
@@ -1792,12 +1795,15 @@ describe('CodexRuntimeHomeService', () => {
expect(existsSync(markerPath)).toBe(false)
service.setRealHomeLaneGate(() => true)
const perSpawnCustomHome = join(testState.fakeHomeDir, 'per-spawn-custom-codex-home')
const scheduleSessionMigration = vi.fn()
service.setHostSystemDefaultSessionMigrationScheduler(scheduleSessionMigration)
writeFileSync(markerPath, '{}\n', 'utf-8')
expect(service.isHostSystemDefaultRealHome({ CODEX_HOME: perSpawnCustomHome })).toBe(false)
expect(service.prepareForCodexLaunch(undefined, { CODEX_HOME: perSpawnCustomHome })).toBe(
getRuntimeCodexHomePath()
)
expect(existsSync(markerPath)).toBe(true)
expect(scheduleSessionMigration).not.toHaveBeenCalled()
if (process.platform !== 'win32') {
// Why: shell startup CODEX_HOME discovery is a POSIX-shell lane; Windows
// must not invoke an ambient WSL bash while evaluating this contract.
@@ -2506,10 +2512,14 @@ describe('CodexRuntimeHomeService', () => {
)
const { CodexRuntimeHomeService } = await import('./runtime-home-service')
const service = new CodexRuntimeHomeService(store as never)
const scheduleSessionMigration = vi.fn()
service.setHostSystemDefaultSessionMigrationScheduler(scheduleSessionMigration)
// Flag ON + host managed account = the account's own home is CODEX_HOME.
expect(service.isHostSystemDefaultRealHome()).toBe(false)
expect(service.isHostSystemDefaultSessionMigrationEligible()).toBe(false)
expect(service.prepareForCodexLaunch()).toBe(managedHomePath)
expect(scheduleSessionMigration).not.toHaveBeenCalled()
// The per-account home keeps its own auth in place; the shared mirror's
// auth.json is never hot-swapped, so two accounts cannot race one file.
expect(readFileSync(join(managedHomePath, 'auth.json'), 'utf-8')).toBe(
+29 -10
View File
@@ -182,6 +182,7 @@ export class CodexRuntimeHomeService {
private sharedAuthRefreshBlockedByManagedTransition = false
// Why: transient auth.json read/parse failures must not deselect an account.
private readonly credentialAbsenceGrace = new CodexCredentialAbsenceGrace()
private scheduleHostSystemDefaultSessionMigration: () => void = () => {}
constructor(private readonly store: Store) {
this.safeRecoverInterruptedRuntimeAuthOperation()
@@ -242,7 +243,8 @@ export class CodexRuntimeHomeService {
this.reconcileLegacySharedHomeForRetainedPanes()
return null
}
this.invalidateBackfillAfterManagedSystemDefaultLaunch(launchEnv)
const shouldScheduleSessionMigration =
this.invalidateBackfillAfterManagedSystemDefaultLaunch(launchEnv)
this.syncForCurrentSelection(target, launchEnv)
syncSystemCodexResourcesIntoManagedHome()
syncSystemConfigIntoManagedCodexHome()
@@ -251,9 +253,23 @@ export class CodexRuntimeHomeService {
{},
resolveHostCodexSessionSourceHome(this.store.getSettings())
)
if (shouldScheduleSessionMigration) {
this.scheduleHostSystemDefaultSessionMigration()
}
return this.getRuntimeHomePath()
}
setHostSystemDefaultSessionMigrationScheduler(schedule: () => void): void {
this.scheduleHostSystemDefaultSessionMigration = schedule
}
isHostSystemDefaultSessionMigrationEligible(): boolean {
return (
normalizeCodexRuntimeSelection(this.store.getSettings()).host === null &&
!hasCustomCodexHomeOverrideForLaunch()
)
}
// 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,
@@ -415,17 +431,20 @@ export class CodexRuntimeHomeService {
this.lastHostAccountUsedSelfContainedHome = false
}
private invalidateBackfillAfterManagedSystemDefaultLaunch(launchEnv?: NodeJS.ProcessEnv): void {
private invalidateBackfillAfterManagedSystemDefaultLaunch(
launchEnv?: NodeJS.ProcessEnv
): boolean {
const settings = this.store.getSettings()
if (normalizeCodexRuntimeSelection(settings).host !== null) {
return
}
const realHomeSelected = this.isHostSystemDefaultRealHomeSelected(launchEnv)
if (realHomeSelected || !isCodexSystemDefaultRealHomeEnabled()) {
invalidateCodexSessionBackfillMarker(
join(getCodexSessionBackfillStateDirPath(), 'backfill-complete.json')
)
if (
normalizeCodexRuntimeSelection(settings).host !== null ||
hasCustomCodexHomeOverrideForLaunch(launchEnv)
) {
return false
}
invalidateCodexSessionBackfillMarker(
join(getCodexSessionBackfillStateDirPath(), 'backfill-complete.json')
)
return true
}
private startWslSessionBridgeForLaunch(
@@ -32,6 +32,7 @@ describe('Windows System Default Codex home ownership', () => {
SHELL: 'powershell.exe'
})
).toBe(false)
expect(service.isHostSystemDefaultSessionMigrationEligible()).toBe(true)
})
})
@@ -1,5 +1,5 @@
import { mkdirSync, readFileSync, rmSync } from 'node:fs'
import { dirname } from 'node:path'
import { mkdirSync, readFileSync, readdirSync, rmSync } from 'node:fs'
import { dirname, join } from 'node:path'
import { writeFileAtomically } from '../codex-accounts/fs-utils'
import type { CodexSessionBackfillSummary } from './codex-session-backfill-types'
@@ -9,25 +9,70 @@ const CODEX_SESSION_BACKFILL_MARKER_VERSION = 3
export function hasCompletedCodexSessionBackfillMarker(
markerPath: string,
systemSessionsRoot: string
systemSessionsRoot: string,
managedSessionsRoot: string
): boolean {
try {
const parsed: unknown = JSON.parse(readFileSync(markerPath, 'utf-8'))
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
return false
}
const marker = parsed as { version?: unknown; systemSessionsRoot?: unknown }
const marker = parsed as {
version?: unknown
systemSessionsRoot?: unknown
summary?: { scannedFiles?: unknown }
}
// Why: changing the configured real Codex home must backfill the new
// target instead of honoring a marker written for a different history.
return (
const markerMatchesTarget =
marker.version === CODEX_SESSION_BACKFILL_MARKER_VERSION &&
marker.systemSessionsRoot === systemSessionsRoot
)
if (!markerMatchesTarget) {
return false
}
// Why: an empty source can become populated after an early migration run.
return marker.summary?.scannedFiles !== 0 || !managedSessionsRootHasRollout(managedSessionsRoot)
} catch {
return false
}
}
function managedSessionsRootHasRollout(managedSessionsRoot: string): boolean {
try {
for (const year of readdirSync(managedSessionsRoot, { withFileTypes: true })) {
if (!year.isDirectory() || !/^\d{4}$/.test(year.name)) {
continue
}
const yearPath = join(managedSessionsRoot, year.name)
for (const month of readdirSync(yearPath, { withFileTypes: true })) {
if (!month.isDirectory() || !/^\d{2}$/.test(month.name)) {
continue
}
const monthPath = join(yearPath, month.name)
for (const day of readdirSync(monthPath, { withFileTypes: true })) {
if (!day.isDirectory() || !/^\d{2}$/.test(day.name)) {
continue
}
const dayPath = join(monthPath, day.name)
if (
readdirSync(dayPath, { withFileTypes: true }).some(
(entry) => entry.isFile() && /^rollout-.+\.jsonl$/.test(entry.name)
)
) {
return true
}
}
}
}
return false
} catch (error) {
if ((error as NodeJS.ErrnoException | null)?.code === 'ENOENT') {
return false
}
throw error
}
}
export function writeCodexSessionBackfillMarker(
markerPath: string,
systemSessionsRoot: string,
@@ -496,6 +496,22 @@ describe('startCodexSessionBackfillInBackground', () => {
).toBe(false)
})
it('self-heals a zero-file marker when managed rollouts appear later', async () => {
const empty = await startCodexSessionBackfillInBackground()
expect(empty).toMatchObject({ scannedFiles: 0, linkedFiles: 0 })
expect(JSON.parse(readFileSync(getMarkerPath(), 'utf-8'))).toMatchObject({
summary: { scannedFiles: 0 }
})
writeManagedSession(join('2026', '07', '28', 'rollout-later.jsonl'), '{"id":"later"}\n')
const healed = await startCodexSessionBackfillInBackground()
expect(healed).toMatchObject({ scannedFiles: 1, linkedFiles: 1 })
expect(
existsSync(join(getSystemSessionsRoot(), '2026', '07', '28', 'rollout-later.jsonl'))
).toBe(true)
})
it('recovers an installed rollout after the completion marker write fails', async () => {
writeManagedSession(join('2026', '05', '26', 'rollout-a.jsonl'), '{"id":"a"}\n')
mkdirSync(getMarkerPath(), { recursive: true })
+7 -1
View File
@@ -87,7 +87,13 @@ async function runCodexSessionBackfillOncePerHost(
systemCodexHomePathOverride?: string
): Promise<CodexSessionBackfillSummary | null> {
const paths = resolveCodexSessionBackfillPaths(systemCodexHomePathOverride)
if (hasCompletedCodexSessionBackfillMarker(paths.markerPath, paths.systemSessionsRoot)) {
if (
hasCompletedCodexSessionBackfillMarker(
paths.markerPath,
paths.systemSessionsRoot,
paths.managedSessionsRoot
)
) {
return null
}
const summary = await backfillManagedCodexSessionsIntoSystemHome(paths, options)
@@ -28,6 +28,28 @@ describe('createCodexSessionMigrationScheduler', () => {
expect(startBackfill).toHaveBeenCalledOnce()
})
it('schedules a delayed rerun after a shared-home launch', async () => {
const startBackfill = vi.fn().mockResolvedValue(null)
const startIndexHeal = vi.fn().mockResolvedValue(null)
const scheduler = createCodexSessionMigrationScheduler({
isEligible: () => true,
isQuitting: () => false,
resolveSystemCodexHomePathOverride: () => undefined,
startBackfill,
startIndexHeal,
initialDelayMs: 1_000
})
scheduler.scheduleRun()
scheduler.scheduleRun()
await vi.advanceTimersByTimeAsync(999)
expect(startBackfill).not.toHaveBeenCalled()
await vi.advanceTimersByTimeAsync(1)
await vi.waitFor(() => expect(startIndexHeal).toHaveBeenCalledOnce())
expect(startBackfill).toHaveBeenCalledOnce()
})
it('coalesces concurrent run requests and stops before index heal after opt-out', async () => {
let eligible = true
let releaseBackfill: (() => void) | undefined
@@ -7,6 +7,7 @@ type MigrationRun = (
export type CodexSessionMigrationScheduler = {
scheduleInitialRun(): void
scheduleRun(): void
requestRun(): void
}
@@ -18,7 +19,7 @@ export function createCodexSessionMigrationScheduler(args: {
startIndexHeal: MigrationRun
initialDelayMs?: number
}): CodexSessionMigrationScheduler {
let initialTimer: ReturnType<typeof setTimeout> | null = null
let scheduledTimer: ReturnType<typeof setTimeout> | null = null
let migrationTask: Promise<void> | null = null
let activeRunStopObserved = false
let rerunRequested = false
@@ -68,16 +69,19 @@ export function createCodexSessionMigrationScheduler(args: {
})
}
const scheduleRun = (): void => {
if (scheduledTimer) {
return
}
scheduledTimer = setTimeout(() => {
scheduledTimer = null
requestRun()
}, args.initialDelayMs ?? 15_000)
}
return {
scheduleInitialRun(): void {
if (initialTimer) {
return
}
initialTimer = setTimeout(() => {
initialTimer = null
requestRun()
}, args.initialDelayMs ?? 15_000)
},
scheduleInitialRun: scheduleRun,
scheduleRun,
requestRun
}
}
+4 -5
View File
@@ -2221,20 +2221,19 @@ void app.whenReady().then(async () => {
isAgentStatusHooksEnabled(store?.getSettings())
)
const codexSessionMigration = createCodexSessionMigrationScheduler({
isEligible: () => codexRuntimeHome?.isHostSystemDefaultRealHome() === true,
isEligible: () => codexRuntimeHome?.isHostSystemDefaultSessionMigrationEligible() === true,
isQuitting: () => isQuitting,
resolveSystemCodexHomePathOverride: () =>
resolveHostCodexSessionSourceHome(store!.getSettings()),
startBackfill: startCodexSessionBackfillInBackground,
startIndexHeal: startCodexSessionIndexHealInBackground
})
codexRuntimeHome.setHostSystemDefaultSessionMigrationScheduler(codexSessionMigration.scheduleRun)
codexAccounts = new CodexAccountService(store, rateLimits, codexRuntimeHome, {
onHostSystemDefaultSelected: codexSessionMigration.requestRun
})
// Why: one-time per-host backfill makes historical Orca-managed Codex
// sessions visible to the user's own resume picker and app history (#4444,
// #8612). Deferred so startup and first PTY spawns never compete with the
// sessions tree walk.
// Why: migrate historical shared-home sessions after startup; compatibility
// launches re-arm the non-destructive pass for new rollouts (#4444, #8612, #12480).
codexSessionMigration.scheduleInitialRun()
claudeRuntimeAuth = new ClaudeRuntimeAuthService(store)
claudeAccounts = new ClaudeAccountService(store, rateLimits, claudeRuntimeAuth)