mirror of
https://github.com/stablyai/orca.git
synced 2026-10-18 00:03:20 +00:00
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:
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user