mirror of
https://github.com/stablyai/orca.git
synced 2026-11-01 17:47:40 +00:00
fix(codex): owe a full history scan after launches while the source is in WSL (#35703)
A launch made while the Codex history source is a \\wsl path records no rollout dates, so after switching back to a plain path the next migration pass only scanned the old pending dates and missed those sessions. Such a launch now sets the local backfill marker's full-scan flag (Orca user data, no 9P access), and the next eligible pass walks all history. Also checks the resolved source home (setting, else the system home) when deciding whether the history source is in WSL.
This commit is contained in:
@@ -29,12 +29,7 @@ const bridges = vi.hoisted(() => ({
|
||||
wsl: vi.fn(async () => {})
|
||||
}))
|
||||
|
||||
// Every lane that links Codex sessions between homes, for one launch each.
|
||||
async function launchWith(
|
||||
share: boolean | undefined,
|
||||
lane: 'real-home' | 'per-account' | 'shared-mirror',
|
||||
codexSessionSourceHome?: { host: string }
|
||||
): Promise<void> {
|
||||
function mockBridges(): void {
|
||||
vi.doMock('../codex/codex-account-session-bridge', async (importOriginal) => ({
|
||||
...(await importOriginal<object>()),
|
||||
startCodexAccountSessionBridgeInBackground: bridges.account
|
||||
@@ -43,6 +38,15 @@ async function launchWith(
|
||||
...(await importOriginal<object>()),
|
||||
startSystemCodexSessionBridgeInBackground: bridges.system
|
||||
}))
|
||||
}
|
||||
|
||||
// Every lane that links Codex sessions between homes, for one launch each.
|
||||
async function launchWith(
|
||||
share: boolean | undefined,
|
||||
lane: 'real-home' | 'per-account' | 'shared-mirror',
|
||||
codexSessionSourceHome?: { host: string }
|
||||
): Promise<void> {
|
||||
mockBridges()
|
||||
const managedHomePath = createManagedAuth(
|
||||
testState.userDataDir,
|
||||
'account-1',
|
||||
@@ -214,4 +218,83 @@ describe('the Codex history-sharing opt-out (#24511)', () => {
|
||||
vi.unstubAllEnvs()
|
||||
}
|
||||
)
|
||||
|
||||
it('a launch while the source is \\\\wsl makes the next plain-source pass a full scan', async () => {
|
||||
vi.stubEnv('HOME', testState.fakeHomeDir)
|
||||
const plain = { host: join(testState.fakeHomeDir, 'custom-codex') }
|
||||
const store = createStore(
|
||||
createSettings({ realHomeRoutable: true, codexSessionSourceHome: plain })
|
||||
)
|
||||
const { CodexRuntimeHomeService } = await import('./runtime-home-service')
|
||||
const { resolveCodexSessionBackfillPaths } = await import('../codex/codex-session-backfill')
|
||||
const { writeCodexSessionBackfillMarker, captureCodexSessionBackfillMarkerGeneration } =
|
||||
await import('../codex/codex-session-backfill-marker')
|
||||
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the harness store implements the getSettings/updateSettings surface this service reads.
|
||||
const service = new CodexRuntimeHomeService(store as never)
|
||||
const paths = resolveCodexSessionBackfillPaths(plain.host)
|
||||
writeCodexSessionBackfillMarker(
|
||||
paths.markerPath,
|
||||
paths.systemSessionsRoot,
|
||||
{
|
||||
stopped: false,
|
||||
scannedFiles: 3,
|
||||
linkedFiles: 3,
|
||||
copiedFiles: 0,
|
||||
skippedExistingFiles: 0,
|
||||
skippedUnexpectedFiles: 0,
|
||||
skippedSymlinkFiles: 0,
|
||||
skippedUnsupportedFilesystemFiles: 0,
|
||||
failedDirectories: 0,
|
||||
failedFiles: 0,
|
||||
failedHealAuditRecords: 0
|
||||
},
|
||||
captureCodexSessionBackfillMarkerGeneration(),
|
||||
{ coverage: 'full', coveredScanDates: [] }
|
||||
)
|
||||
// Certified plain-source history only owes a bounded pass.
|
||||
expect(service.prepareHostSystemDefaultSessionMigrationPass()).toBe(false)
|
||||
service.finishHostSystemDefaultSessionMigrationPass()
|
||||
|
||||
store.updateSettings({ codexSessionSourceHome: WSL_SOURCE })
|
||||
expect(service.beginHostSystemDefaultSessionMigrationLaunch(null, { reattached: true })).toBe(
|
||||
null
|
||||
)
|
||||
store.updateSettings({ codexSessionSourceHome: plain })
|
||||
|
||||
expect(service.prepareHostSystemDefaultSessionMigrationPass()).toBe(true)
|
||||
vi.unstubAllEnvs()
|
||||
})
|
||||
|
||||
it('a \\\\wsl CODEX_HOME never becomes the shared-mirror source', async () => {
|
||||
const store = createStore(createSettings({ codexManagedAccounts: [] }))
|
||||
// After createSettings, which sets the harness CODEX_HOME.
|
||||
vi.stubEnv('CODEX_HOME', WSL_SOURCE.host)
|
||||
mockBridges()
|
||||
try {
|
||||
const { CodexRuntimeHomeService } = await import('./runtime-home-service')
|
||||
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the harness store implements the getSettings/updateSettings surface this service reads.
|
||||
new CodexRuntimeHomeService(store as never).prepareForCodexLaunch()
|
||||
expect(bridges.system).toHaveBeenCalledWith({}, undefined)
|
||||
expect(bridges.system.mock.calls.flat()).not.toContain(WSL_SOURCE.host)
|
||||
} finally {
|
||||
vi.unstubAllEnvs()
|
||||
}
|
||||
})
|
||||
|
||||
it('a system home that is itself a \\\\wsl path blocks migration and backfill', async () => {
|
||||
vi.stubEnv('HOME', testState.fakeHomeDir)
|
||||
const store = createStore(createSettings({ realHomeRoutable: true }))
|
||||
const { CodexRuntimeHomeService } = await import('./runtime-home-service')
|
||||
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the harness store implements the getSettings/updateSettings surface this service reads.
|
||||
const service = new CodexRuntimeHomeService(store as never)
|
||||
expect(service.isHostSystemDefaultSessionMigrationEligible()).toBe(true)
|
||||
const realHome = testState.fakeHomeDir
|
||||
testState.fakeHomeDir = '\\\\wsl.localhost\\Ubuntu\\home\\me'
|
||||
try {
|
||||
expect(service.isHostSystemDefaultSessionMigrationEligible()).toBe(false)
|
||||
} finally {
|
||||
testState.fakeHomeDir = realHome
|
||||
vi.unstubAllEnvs()
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
@@ -145,8 +145,9 @@ export abstract class CodexRuntimeHomeLaunch extends CodexRuntimeHomeRouting {
|
||||
codexHomePath: string | null,
|
||||
options: { reattached?: boolean; launchEnv?: NodeJS.ProcessEnv } = {}
|
||||
): boolean | null {
|
||||
// A WSL source still reaches invalidation, which records the full scan its launch will owe.
|
||||
if (
|
||||
!this.isHostSystemDefaultSessionMigrationEligible() ||
|
||||
!this.isHostSystemDefaultSessionMigrationRouteSelected() ||
|
||||
(!codexHomePath && !options.reattached) ||
|
||||
(codexHomePath &&
|
||||
normalizeRuntimePathForComparison(codexHomePath) !==
|
||||
@@ -161,10 +162,15 @@ export abstract class CodexRuntimeHomeLaunch extends CodexRuntimeHomeRouting {
|
||||
}
|
||||
|
||||
isHostSystemDefaultSessionMigrationEligible(): boolean {
|
||||
return (
|
||||
this.isHostSystemDefaultSessionMigrationRouteSelected() && !this.isHostHistorySourceInWsl()
|
||||
)
|
||||
}
|
||||
|
||||
private isHostSystemDefaultSessionMigrationRouteSelected(): boolean {
|
||||
return (
|
||||
normalizeCodexRuntimeSelection(this.store.getSettings()).host === null &&
|
||||
!hasCustomCodexHomeOverrideForLaunch() &&
|
||||
!this.isHostHistorySourceInWsl()
|
||||
!hasCustomCodexHomeOverrideForLaunch()
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -22,7 +22,10 @@ import {
|
||||
} from './host-codex-managed-home-ownership'
|
||||
import { hasCustomCodexHomeOverrideForLaunch } from '../codex/codex-real-home-path'
|
||||
import { resolveCodexSessionBackfillPaths } from '../codex/codex-session-backfill'
|
||||
import { hasCompletedCodexSessionBackfillMarker } from '../codex/codex-session-backfill-marker'
|
||||
import {
|
||||
hasCompletedCodexSessionBackfillMarker,
|
||||
markCodexSessionBackfillMarkerNeedsFullScan
|
||||
} from '../codex/codex-session-backfill-marker'
|
||||
import { getDefaultWslDistro } from '../wsl'
|
||||
import { startWslCodexSessionBridgeInBackground } from '../codex/wsl-codex-session-bridge'
|
||||
import type { CodexAccountSelectionTarget } from './runtime-selection'
|
||||
@@ -147,8 +150,10 @@ export abstract class CodexRuntimeHomeManagedHome extends CodexRuntimeHomeSync {
|
||||
|
||||
// Reading a distro's history over 9P would boot it (E9); the WSL lane owns those homes.
|
||||
protected isHostHistorySourceInWsl(): boolean {
|
||||
const sourceHome = resolveHostCodexSessionSourceHome(this.store.getSettings())
|
||||
return Boolean(sourceHome && parseWslUncPath(sourceHome))
|
||||
// The home the lanes actually read: the setting, else the system home they fall back to.
|
||||
const sourceHome =
|
||||
resolveHostCodexSessionSourceHome(this.store.getSettings()) ?? getSystemCodexHomePath()
|
||||
return parseWslUncPath(sourceHome) !== null
|
||||
}
|
||||
|
||||
protected getSelfContainedSessionBridgeSourceHomes(): string[] {
|
||||
@@ -242,11 +247,15 @@ export abstract class CodexRuntimeHomeManagedHome extends CodexRuntimeHomeSync {
|
||||
const settings = this.store.getSettings()
|
||||
if (
|
||||
normalizeCodexRuntimeSelection(settings).host !== null ||
|
||||
hasCustomCodexHomeOverrideForLaunch(launchEnv) ||
|
||||
this.isHostHistorySourceInWsl()
|
||||
hasCustomCodexHomeOverrideForLaunch(launchEnv)
|
||||
) {
|
||||
return null
|
||||
}
|
||||
if (this.isHostHistorySourceInWsl()) {
|
||||
// This launch's dates go unrecorded, so a later plain source must walk all history.
|
||||
markCodexSessionBackfillMarkerNeedsFullScan(resolveCodexSessionBackfillPaths().markerPath)
|
||||
return null
|
||||
}
|
||||
if (!this.hostSystemDefaultSessionMigrationPending) {
|
||||
const paths = resolveCodexSessionBackfillPaths(
|
||||
resolveHostCodexSessionSourceHome(this.store.getSettings())
|
||||
|
||||
@@ -5,6 +5,7 @@ import { join } from 'node:path'
|
||||
import {
|
||||
captureCodexSessionBackfillMarkerGeneration,
|
||||
hasCompletedCodexSessionBackfillMarker,
|
||||
markCodexSessionBackfillMarkerNeedsFullScan,
|
||||
markCodexSessionBackfillMarkerPending,
|
||||
readCodexSessionBackfillBaseline,
|
||||
writeCodexSessionBackfillMarker
|
||||
@@ -244,4 +245,24 @@ describe('codex session backfill marker', () => {
|
||||
expect(readMarker()).toMatchObject({ coverage: 'bounded', pendingScanDates: [TODAY] })
|
||||
expect(hasCompletedCodexSessionBackfillMarker(markerPath, WINDOWS_ROOT)).toBe(false)
|
||||
})
|
||||
|
||||
it('a full-scan demand outlives later launches and only a full pass clears it', () => {
|
||||
writeFullBaseline(WINDOWS_ROOT)
|
||||
markCodexSessionBackfillMarkerNeedsFullScan(markerPath)
|
||||
|
||||
expect(readCodexSessionBackfillBaseline(markerPath, WINDOWS_ROOT)).toBeNull()
|
||||
expect(markCodexSessionBackfillMarkerPending(markerPath, WINDOWS_ROOT, [LAUNCH_DATE])).toBe(
|
||||
true
|
||||
)
|
||||
writeFullBaseline(WINDOWS_ROOT)
|
||||
expect(readCodexSessionBackfillBaseline(markerPath, WINDOWS_ROOT)).toEqual({
|
||||
pendingScanDates: []
|
||||
})
|
||||
})
|
||||
|
||||
it('a full-scan demand without a marker writes nothing', () => {
|
||||
markCodexSessionBackfillMarkerNeedsFullScan(markerPath)
|
||||
|
||||
expect(() => readMarker()).toThrow()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -153,6 +153,23 @@ export function markCodexSessionBackfillMarkerPending(
|
||||
return pending.needsFullScan
|
||||
}
|
||||
|
||||
/** Owe a full walk for launches whose rollout dates could not be recorded. */
|
||||
export function markCodexSessionBackfillMarkerNeedsFullScan(markerPath: string): void {
|
||||
// Prevent older in-flight passes from clearing this demand.
|
||||
markerInvalidationGeneration += 1
|
||||
const record = readMarkerRecord(markerPath)
|
||||
// No marker already means no baseline, so the next pass walks everything.
|
||||
if (!record || record.needsFullScan) {
|
||||
return
|
||||
}
|
||||
try {
|
||||
writeMarkerRecord(markerPath, { ...record.raw, pendingScanDates: [], needsFullScan: true })
|
||||
} catch (error) {
|
||||
console.warn('[codex-session-backfill] Failed to record full-scan demand:', error)
|
||||
rmSync(markerPath, { force: true })
|
||||
}
|
||||
}
|
||||
|
||||
type CodexSessionBackfillMarkerRecord = {
|
||||
raw: Record<string, unknown>
|
||||
systemSessionsRoot: string
|
||||
|
||||
Reference in New Issue
Block a user