fix(codex): rank the shared runtime mirror above the path-sorted homes

The mirror winning the rescan is not inert: it is the only input that
satisfies prepareLegacySharedCodexSessionResume's guard, which migrates the
rollout into ~/.codex and returns useRealCodexHome. Letting 'codex-accounts'
outrank 'codex-runtime-home' on path order alone therefore did more than
reorder tier 3 — it silently disabled that migration, so a system-default
selection resumed under an arbitrary account's credentials and, once the hook
stamped a transcript path there, stayed pinned to it.

The duplication this depends on already exists on main: the one-shot
migrateLegacySessions copies each per-account rollout into the mirror and
leaves the original, so both homes really do hold the id.

Give the mirror its own tier between the system home and the path-sorted
remainder, restoring the pre-ranking mirror-before-accounts order while
keeping the account-vs-account determinism this PR exists to add.

Also pin the system-home tier, which no test covered: its fixture
'/Users/…' already won the tier-3 byte order ('U' < 'u'), so the tier could
be deleted with the suite green.
This commit is contained in:
Brennan Benson
2026-07-26 17:00:20 -07:00
parent 5bde2d7734
commit 009eb196cd
3 changed files with 65 additions and 16 deletions
@@ -13,7 +13,8 @@ const tempRoots: string[] = []
// Why: the ranking inputs are required by design; cases below that never reach the rescan stay neutral.
const withoutHomeRanking = {
getSelectedAccountCodexHome: (): string | null => null,
systemCodexHomePath: null
systemCodexHomePath: null,
sharedRuntimeCodexHomePath: null
}
afterEach(() => {
@@ -274,6 +275,7 @@ describe('findTrustedCodexSessionResume legacy-rescan home ranking', () => {
trustedCodexHomes,
getSelectedAccountCodexHome: () => accountBHome,
systemCodexHomePath: systemHome,
sharedRuntimeCodexHomePath: sharedMirror,
listSessionFiles: listRolloutInEveryHome
})
).resolves.toEqual({ homePath: accountBHome, transcriptPath: rolloutIn(accountBHome) })
@@ -288,15 +290,36 @@ describe('findTrustedCodexSessionResume legacy-rescan home ranking', () => {
trustedCodexHomes: [sharedMirror, accountAHome, systemHome],
getSelectedAccountCodexHome: () => null,
systemCodexHomePath: systemHome,
sharedRuntimeCodexHomePath: sharedMirror,
listSessionFiles: listRolloutInEveryHome
})
).resolves.toEqual({ homePath: systemHome, transcriptPath: rolloutIn(systemHome) })
})
// Why: `/Users/…` already wins the tier-3 byte order, so the case above cannot tell the
// system-home tier apart from the path tie-break. Pin it with a home that sorts last.
it('ranks the real system home above the others even when its path sorts last', async () => {
const lateSortingSystemHome = join('/var', 'lib', 'orca', '.codex')
await expect(
findTrustedCodexSessionResume({
sessionId,
transcriptPath: undefined,
trustedCodexHomes: [sharedMirror, accountAHome, lateSortingSystemHome],
getSelectedAccountCodexHome: () => null,
systemCodexHomePath: lateSortingSystemHome,
sharedRuntimeCodexHomePath: sharedMirror,
listSessionFiles: listRolloutInEveryHome
})
).resolves.toEqual({
homePath: lateSortingSystemHome,
transcriptPath: rolloutIn(lateSortingSystemHome)
})
})
it('orders the remaining homes by path so insertion order never decides', async () => {
for (const trustedCodexHomes of [
[accountBHome, sharedMirror, accountAHome],
[accountAHome, accountBHome, sharedMirror]
[accountBHome, accountAHome],
[accountAHome, accountBHome]
]) {
await expect(
findTrustedCodexSessionResume({
@@ -305,25 +328,30 @@ describe('findTrustedCodexSessionResume legacy-rescan home ranking', () => {
trustedCodexHomes,
getSelectedAccountCodexHome: () => null,
systemCodexHomePath: systemHome,
sharedRuntimeCodexHomePath: sharedMirror,
listSessionFiles: listRolloutInEveryHome
})
).resolves.toEqual({ homePath: accountAHome, transcriptPath: rolloutIn(accountAHome) })
}
})
it('prefers a per-account home over the shared mirror when the system home lacks the id', async () => {
// Why: 'codex-accounts' sorts before 'codex-runtime-home', so tier 3 flips the pre-ranking
// winner here. Deliberate: the mirror is hot-swapped and names no account, the account home does.
// Why: the mirror is the only home whose win migrates the rollout into ~/.codex
// (prepareLegacySharedCodexSessionResume), which is how a system-default selection resumes on the
// real home. 'codex-accounts' sorts before 'codex-runtime-home', so path order alone would hand
// that selection to an arbitrary account instead. One-shot legacy migration already copies
// per-account rollouts into the mirror, so both really do hold the id.
it('prefers the shared mirror over a per-account home when the system home lacks the id', async () => {
await expect(
findTrustedCodexSessionResume({
sessionId,
transcriptPath: undefined,
trustedCodexHomes: [systemHome, sharedMirror, accountAHome],
trustedCodexHomes: [systemHome, accountAHome, sharedMirror],
getSelectedAccountCodexHome: () => null,
systemCodexHomePath: systemHome,
sharedRuntimeCodexHomePath: sharedMirror,
listSessionFiles: listRolloutIn(sharedMirror, accountAHome)
})
).resolves.toEqual({ homePath: accountAHome, transcriptPath: rolloutIn(accountAHome) })
).resolves.toEqual({ homePath: sharedMirror, transcriptPath: rolloutIn(sharedMirror) })
})
it('ranks Windows homes case-insensitively and keeps the caller path spelling', async () => {
@@ -345,6 +373,7 @@ describe('findTrustedCodexSessionResume legacy-rescan home ranking', () => {
// Why: settings and discovery can disagree on drive/segment case; the selection must still match.
getSelectedAccountCodexHome: () => windowsAccountBHome.toLowerCase(),
systemCodexHomePath: windowsSystemHome,
sharedRuntimeCodexHomePath: null,
listSessionFiles
})
).resolves.toEqual({
@@ -361,6 +390,7 @@ describe('findTrustedCodexSessionResume legacy-rescan home ranking', () => {
trustedCodexHomes: [systemHome, sharedMirror, accountAHome, accountBHome],
getSelectedAccountCodexHome: () => accountAHome,
systemCodexHomePath: systemHome,
sharedRuntimeCodexHomePath: sharedMirror,
listSessionFiles: listRolloutIn(accountBHome)
})
).resolves.toEqual({ homePath: accountBHome, transcriptPath: rolloutIn(accountBHome) })
@@ -378,6 +408,7 @@ describe('findTrustedCodexSessionResume legacy-rescan home ranking', () => {
trustedCodexHomes: [systemHome, accountAHome, accountBHome],
getSelectedAccountCodexHome: () => accountAHome,
systemCodexHomePath: systemHome,
sharedRuntimeCodexHomePath: sharedMirror,
fileIsRegular: () => false,
listSessionFiles
})
+21 -6
View File
@@ -104,7 +104,14 @@ export function claimsCodexRolloutLayout(transcriptPath: string | undefined): bo
* directly. Everything else is ordered by normalized path so no winner ever
* depends on the order accounts happen to sit in settings.
*
* Both inputs are required, not optional: a caller that forgot one would
* The shared runtime mirror ranks above the remaining homes because winning is
* not inert for it: it is the only home that triggers the legacy migration into
* the real system home (prepareLegacySharedCodexSessionResume), which is how a
* system-default selection resumes on ~/.codex instead of some account's home.
* Letting a per-account home outrank it by mere path order would silently route
* that selection to an account the user did not pick.
*
* All inputs are required, not optional: a caller that forgot one would
* silently degrade to pure path order, which is the accident this exists to
* remove. The selection arrives as a thunk because resolving it stats the
* account's ownership marker, and the far more common provenance-present
@@ -114,16 +121,23 @@ function rankTrustedCodexHomesForRescan(args: {
trustedCodexHomes: readonly string[]
getSelectedAccountCodexHome: () => string | null
systemCodexHomePath: string | null
sharedRuntimeCodexHomePath: string | null
}): string[] {
const selected = args.getSelectedAccountCodexHome()?.trim()
const system = args.systemCodexHomePath?.trim()
const selectedComparison = selected ? normalizeRuntimePathForComparison(selected) : null
const systemComparison = system ? normalizeRuntimePathForComparison(system) : null
const toComparisonHome = (value: string | null | undefined): string | null => {
const trimmed = value?.trim()
return trimmed ? normalizeRuntimePathForComparison(trimmed) : null
}
const selectedComparison = toComparisonHome(args.getSelectedAccountCodexHome())
const systemComparison = toComparisonHome(args.systemCodexHomePath)
const sharedRuntimeComparison = toComparisonHome(args.sharedRuntimeCodexHomePath)
const rankOf = (comparisonHome: string): number => {
if (selectedComparison && comparisonHome === selectedComparison) {
return 0
}
return systemComparison && comparisonHome === systemComparison ? 1 : 2
if (systemComparison && comparisonHome === systemComparison) {
return 1
}
return sharedRuntimeComparison && comparisonHome === sharedRuntimeComparison ? 2 : 3
}
return args.trustedCodexHomes
.map((homePath) => ({ homePath, comparisonHome: normalizeRuntimePathForComparison(homePath) }))
@@ -147,6 +161,7 @@ export async function findTrustedCodexSessionResume(args: {
trustedCodexHomes: readonly string[]
getSelectedAccountCodexHome: () => string | null
systemCodexHomePath: string | null
sharedRuntimeCodexHomePath: string | null
fileIsRegular?: (filePath: string) => boolean
listSessionFiles?: (sessionsRoot: string) => AsyncIterable<string>
}): Promise<{ homePath: string; transcriptPath: string } | null> {
+5 -2
View File
@@ -176,7 +176,7 @@ import {
claimsCodexRolloutLayout,
findTrustedCodexSessionResume
} from './codex/codex-session-resume-home'
import { getSystemCodexHomePath } from './codex/codex-home-paths'
import { getOrcaManagedCodexHomePath, getSystemCodexHomePath } from './codex/codex-home-paths'
import { normalizeRuntimePathForComparison } from '../shared/cross-platform-path'
import type { AgentProviderSessionMetadata } from '../shared/agent-session-resume'
import { getDefaultWslDistro } from './wsl'
@@ -857,7 +857,10 @@ async function prepareCodexSessionResumeForLaunch(args: {
// rank it by the current selection so settings insertion order can never decide the account.
// Lazy: only the legacy branch ranks, so a provenance-present resume never stats the marker.
getSelectedAccountCodexHome: () => codexRuntimeHome!.getSelectedHostAccountCodexHomePath(),
systemCodexHomePath: systemHomePath
systemCodexHomePath: systemHomePath,
// Why: the mirror winning is what triggers the migration into ~/.codex below, so it must
// outrank the path-sorted account homes or a system-default selection resumes as an account.
sharedRuntimeCodexHomePath: getOrcaManagedCodexHomePath()
})
if (!sessionSource) {
// Why: an unverifiable Codex rollout still blocks; only paths that never claimed Codex