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