fix(codex): keep managed rollouts discoverable across real-home opt-out

WI-4 lossless migration/rollback validation for pre-E shared-mirror managed
accounts. Session discovery gated the per-account home scan on the real-home
flag, so opting back out (flag OFF) hid every rollout an account accumulated
while the flag was ON — the data stayed on disk but vanished from the AI Vault
until the flag flipped back on.

Scan a managed host home whenever it holds a sessions/ tree, independent of the
flag; a never-enabled install keeps its homes credential-only so opt-out stays
byte-identical to today. Forward migration was already lossless (the shared
mirror is always scanned) and the opt-out credential read-back already refuses
to overwrite a fresher per-account token; add tests locking all three
invariants. Sandboxed tests only; ~/.codex is never touched.
This commit is contained in:
Brennan Benson
2026-07-17 13:01:34 -07:00
parent 433356adcf
commit ac99e5f2b2
2 changed files with 180 additions and 8 deletions
@@ -1309,6 +1309,165 @@ describe('CodexRuntimeHomeService', () => {
expect(readFileSync(runtimeAuthPath, 'utf-8')).toBe(account1Auth)
})
it('keeps pre-E shared-mirror sessions discoverable alongside per-account rollouts (flag ON)', async () => {
// Pre-E history lives in the shared runtime mirror; after upgrading to
// per-account homes (flag ON) new rollouts land in the account's own home.
const sharedSessionsDir = join(getRuntimeCodexHomePath(), 'sessions', '2026', '07', '16')
mkdirSync(sharedSessionsDir, { recursive: true })
writeFileSync(join(sharedSessionsDir, 'rollout-pre-e.jsonl'), '{"record":"pre-e"}\n', 'utf-8')
const home1 = createManagedAuth(
testState.userDataDir,
'account-1',
createCodexAuthJson('one@example.com', 'acct-1', 'one')
)
const perAccountSessionsDir = join(home1, 'sessions', '2026', '07', '17')
mkdirSync(perAccountSessionsDir, { recursive: true })
writeFileSync(
join(perAccountSessionsDir, 'rollout-e-era.jsonl'),
'{"record":"e-era"}\n',
'utf-8'
)
const store = createStore(
createSettings({
codexSystemDefaultRealHomeEnabled: true,
codexManagedAccounts: [
{
id: 'account-1',
email: 'one@example.com',
managedHomePath: home1,
providerAccountId: 'acct-1',
workspaceLabel: null,
workspaceAccountId: 'acct-1',
createdAt: 1,
updatedAt: 1,
lastAuthenticatedAt: 1
}
],
activeCodexManagedAccountId: 'account-1',
activeCodexManagedAccountIdsByRuntime: { host: 'account-1', wsl: {} }
})
)
const { CodexRuntimeHomeService } = await import('./runtime-home-service')
const service = new CodexRuntimeHomeService(store as never)
// Migrating to per-account homes must not strand the pre-E shared-mirror
// history: both roots surface in discovery so no session is lost.
const discovery = service.getHostCodexHomePathsForSessionDiscovery()
expect(discovery).toContain(getRuntimeCodexHomePath())
expect(discovery).toContain(home1)
})
it('surfaces per-account rollouts for session discovery after opting out (flag OFF)', async () => {
// The account ran with the flag ON and accumulated rollouts in its own home,
// then the user opted back out (flag OFF, byte-identical launch path).
const home1 = createManagedAuth(
testState.userDataDir,
'account-1',
createCodexAuthJson('one@example.com', 'acct-1', 'one')
)
const rolloutDir = join(home1, 'sessions', '2026', '07', '17')
mkdirSync(rolloutDir, { recursive: true })
writeFileSync(join(rolloutDir, 'rollout-e-era.jsonl'), '{"record":"e-era"}\n', 'utf-8')
const store = createStore(
createSettings({
codexSystemDefaultRealHomeEnabled: false,
codexManagedAccounts: [
{
id: 'account-1',
email: 'one@example.com',
managedHomePath: home1,
providerAccountId: 'acct-1',
workspaceLabel: null,
workspaceAccountId: 'acct-1',
createdAt: 1,
updatedAt: 1,
lastAuthenticatedAt: 1
}
],
activeCodexManagedAccountId: 'account-1',
activeCodexManagedAccountIdsByRuntime: { host: 'account-1', wsl: {} }
})
)
const { CodexRuntimeHomeService } = await import('./runtime-home-service')
const service = new CodexRuntimeHomeService(store as never)
// Opting out must not hide rollouts that already live in the per-account home.
expect(service.getHostCodexHomePathsForSessionDiscovery()).toContain(home1)
})
it('does not scan an opted-out never-enabled account home that holds no rollouts', async () => {
// A pure never-enabled install keeps its per-account homes credential-only, so
// opt-out discovery must stay byte-identical to today (mirror home only).
const home1 = createManagedAuth(
testState.userDataDir,
'account-1',
createCodexAuthJson('one@example.com', 'acct-1', 'one')
)
const store = createStore(
createSettings({
codexSystemDefaultRealHomeEnabled: false,
codexManagedAccounts: [
{
id: 'account-1',
email: 'one@example.com',
managedHomePath: home1,
providerAccountId: 'acct-1',
workspaceLabel: null,
workspaceAccountId: 'acct-1',
createdAt: 1,
updatedAt: 1,
lastAuthenticatedAt: 1
}
],
activeCodexManagedAccountId: 'account-1',
activeCodexManagedAccountIdsByRuntime: { host: 'account-1', wsl: {} }
})
)
const { CodexRuntimeHomeService } = await import('./runtime-home-service')
const service = new CodexRuntimeHomeService(store as never)
expect(service.getHostCodexHomePathsForSessionDiscovery()).not.toContain(home1)
})
it('preserves a fresh per-account credential and propagates it to the shared mirror on opt-out', async () => {
// Flag ON refreshed the account's token in its own home; the shared mirror's
// auth is frozen from before the ON period. Opting out (flag OFF) must not let
// the stale mirror token clobber the fresher per-account credential.
const freshAuth = createCodexAuthJson('one@example.com', 'acct-1', 'fresh', 4_000)
const staleAuth = createCodexAuthJson('one@example.com', 'acct-1', 'stale', 1_000)
const home1 = createManagedAuth(testState.userDataDir, 'account-1', freshAuth)
// A stale mirror auth left over from before the account ran with the flag ON.
writeFileSync(getRuntimeCodexAuthPath(), staleAuth, 'utf-8')
const store = createStore(
createSettings({
codexSystemDefaultRealHomeEnabled: false,
codexManagedAccounts: [
{
id: 'account-1',
email: 'one@example.com',
managedHomePath: home1,
providerAccountId: 'acct-1',
workspaceLabel: null,
workspaceAccountId: 'acct-1',
createdAt: 1,
updatedAt: 1,
lastAuthenticatedAt: 1
}
],
activeCodexManagedAccountId: 'account-1',
activeCodexManagedAccountIdsByRuntime: { host: 'account-1', wsl: {} }
})
)
const { CodexRuntimeHomeService } = await import('./runtime-home-service')
// Constructor runs the startup opt-out sync (read-back + hot-swap).
new CodexRuntimeHomeService(store as never)
// The fresher per-account credential is preserved (never overwritten by the
// stale mirror read-back) and becomes the mirror's auth for the opted-out launch.
expect(readFileSync(join(home1, 'auth.json'), 'utf-8')).toBe(freshAuth)
expect(readFileSync(getRuntimeCodexAuthPath(), 'utf-8')).toBe(freshAuth)
})
it('uses the same host CODEX_HOME after switching managed Codex accounts', async () => {
const runtimeAuthPath = getRuntimeCodexAuthPath()
const account1Auth = createCodexAuthJson('one@example.com', 'acct-1', 'one')
@@ -220,14 +220,26 @@ export class CodexRuntimeHomeService {
return account
}
private getSelfContainedManagedHostAccountHomes(): string[] {
// Why: session discovery must surface a managed account's own rollouts wherever
// they physically live. Flag ON makes every host managed home a live CODEX_HOME,
// so scan them all. Flag OFF (opt-out/rollback) hands launches back to the shared
// mirror, but a home that already accumulated rollouts while the flag was ON must
// still surface them — otherwise opting out silently hides history that is safe on
// disk. Gate the flag-OFF case on a sessions/ tree so a never-enabled install stays
// byte-identical to today (its per-account homes hold only auth, no rollouts).
private getManagedHostAccountHomesForSessionDiscovery(): string[] {
const settings = this.store.getSettings()
if (!isCodexSystemDefaultRealHomeEnabled(settings)) {
return []
const flagEnabled = isCodexSystemDefaultRealHomeEnabled(settings)
const homes: string[] = []
for (const account of settings.codexManagedAccounts) {
if (this.getWslManagedHomePath(account)) {
continue
}
if (flagEnabled || existsSync(join(account.managedHomePath, 'sessions'))) {
homes.push(account.managedHomePath)
}
}
return settings.codexManagedAccounts
.filter((account) => !this.getWslManagedHomePath(account))
.map((account) => account.managedHomePath)
return homes
}
private prepareSelfContainedManagedHomeForLaunch(account: CodexManagedAccount): string | null {
@@ -350,8 +362,9 @@ export class CodexRuntimeHomeService {
}
// Why: flag ON routes each managed host account to its own self-contained
// home, so its rollouts live there rather than in the shared mirror. Scan
// every such home so account-scoped sessions still surface in the AI Vault.
for (const perAccountHome of this.getSelfContainedManagedHostAccountHomes()) {
// every such home — plus any that retained rollouts across an opt-out — so
// account-scoped sessions still surface in the AI Vault.
for (const perAccountHome of this.getManagedHostAccountHomesForSessionDiscovery()) {
homes.push(perAccountHome)
}
return homes.filter((home, index) => homes.indexOf(home) === index)