feat(codex): backfill managed-home sessions into the real Codex home once per host

Orca-launched Codex sessions currently land only in the Orca-managed
runtime home, so the user's own `codex resume` picker and app history
never see them (#4444, #8612). Backfill the managed sessions tree into
the real ~/.codex/sessions/YYYY/MM/DD layout once per host:

- hardlink first (one physical rollout log), copy as the cross-volume
  fallback; existing target files are always skipped, nothing in either
  home is deleted or moved
- idempotent; per-file failures leave the completion marker unset so the
  next startup retries cheaply
- JSONL audit log of every link/copy/failure under
  <userData>/codex-session-backfill/
- honors the custom Codex session source home override, mirroring the
  existing system->managed bridge

WSL managed homes are distro-local and need an in-distro variant; that
is a follow-up.
This commit is contained in:
Brennan Benson
2026-07-14 19:42:07 -07:00
parent d9f7fd08f7
commit 228ad45c8d
4 changed files with 577 additions and 0 deletions
+4
View File
@@ -38,6 +38,10 @@ export function getOrcaManagedCodexHomePath(): string {
return managedHomePath
}
export function getCodexSessionBackfillStateDirPath(): string {
return join(getOrcaUserDataPath(), 'codex-session-backfill')
}
function getOrcaUserDataPath(): string {
if (process.env.ORCA_USER_DATA_PATH) {
return process.env.ORCA_USER_DATA_PATH
@@ -0,0 +1,297 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import {
existsSync,
lstatSync,
mkdirSync,
mkdtempSync,
readFileSync,
rmSync,
symlinkSync,
writeFileSync
} from 'node:fs'
import type * as NodeFs from 'node:fs'
import type * as NodeOs from 'node:os'
import { tmpdir } from 'node:os'
import { dirname, join } from 'node:path'
const { homedirMock } = vi.hoisted(() => ({
homedirMock: vi.fn<() => string>()
}))
const { fsMockState } = vi.hoisted(() => ({
fsMockState: {
failLink: false,
failCopy: false
}
}))
vi.mock('node:fs', async () => {
const actual = await vi.importActual<typeof NodeFs>('node:fs')
return {
...actual,
linkSync: (...args: Parameters<typeof actual.linkSync>) => {
if (fsMockState.failLink) {
const error = new Error('EXDEV: cross-device link') as NodeJS.ErrnoException
error.code = 'EXDEV'
throw error
}
return actual.linkSync(...args)
},
copyFileSync: (...args: Parameters<typeof actual.copyFileSync>) => {
if (fsMockState.failCopy) {
const error = new Error('EACCES: copy disabled for test') as NodeJS.ErrnoException
error.code = 'EACCES'
throw error
}
return actual.copyFileSync(...args)
}
}
})
vi.mock('node:os', async () => {
const actual = await vi.importActual<typeof NodeOs>('node:os')
return {
...actual,
homedir: homedirMock
}
})
import {
backfillManagedCodexSessionsIntoSystemHome,
resolveCodexSessionBackfillPaths,
startCodexSessionBackfillInBackground
} from './codex-session-backfill'
let fakeHomeDir: string
let userDataDir: string
let previousUserDataPath: string | undefined
function getSystemSessionsRoot(): string {
return join(fakeHomeDir, '.codex', 'sessions')
}
function getManagedSessionsRoot(): string {
return join(userDataDir, 'codex-runtime-home', 'home', 'sessions')
}
function getMarkerPath(): string {
return join(userDataDir, 'codex-session-backfill', 'backfill-complete.json')
}
function getAuditLogPath(): string {
return join(userDataDir, 'codex-session-backfill', 'audit.jsonl')
}
function writeManagedSession(relativePath: string, contents: string): string {
const filePath = join(getManagedSessionsRoot(), relativePath)
mkdirSync(dirname(filePath), { recursive: true })
writeFileSync(filePath, contents, 'utf-8')
return filePath
}
function readAuditActions(): string[] {
return readFileSync(getAuditLogPath(), 'utf-8')
.split('\n')
.filter(Boolean)
.map((line) => (JSON.parse(line) as { action: string }).action)
}
beforeEach(() => {
fsMockState.failLink = false
fsMockState.failCopy = false
fakeHomeDir = mkdtempSync(join(tmpdir(), 'orca-codex-backfill-home-'))
userDataDir = mkdtempSync(join(tmpdir(), 'orca-codex-backfill-user-data-'))
previousUserDataPath = process.env.ORCA_USER_DATA_PATH
process.env.ORCA_USER_DATA_PATH = userDataDir
homedirMock.mockReturnValue(fakeHomeDir)
})
afterEach(() => {
rmSync(fakeHomeDir, { recursive: true, force: true })
rmSync(userDataDir, { recursive: true, force: true })
if (previousUserDataPath === undefined) {
delete process.env.ORCA_USER_DATA_PATH
} else {
process.env.ORCA_USER_DATA_PATH = previousUserDataPath
}
vi.clearAllMocks()
})
describe('backfillManagedCodexSessionsIntoSystemHome', () => {
it('hardlinks managed rollout files into the real home preserving layout', async () => {
const managedPath = writeManagedSession(
join('2026', '05', '26', 'rollout-a.jsonl'),
'{"type":"session_meta","id":"a"}\n'
)
writeManagedSession(join('2026', '06', '01', 'rollout-b.jsonl'), '{"id":"b"}\n')
writeFileSync(join(getManagedSessionsRoot(), '2026', '05', '26', 'notes.txt'), 'skip me\n')
const summary = await backfillManagedCodexSessionsIntoSystemHome(
resolveCodexSessionBackfillPaths()
)
expect(summary).toMatchObject({ scannedFiles: 2, linkedFiles: 2, failedFiles: 0 })
const targetPath = join(getSystemSessionsRoot(), '2026', '05', '26', 'rollout-a.jsonl')
expect(lstatSync(targetPath).ino).toBe(lstatSync(managedPath).ino)
expect(existsSync(join(getSystemSessionsRoot(), '2026', '06', '01', 'rollout-b.jsonl'))).toBe(
true
)
expect(existsSync(join(getSystemSessionsRoot(), '2026', '05', '26', 'notes.txt'))).toBe(false)
expect(readAuditActions()).toEqual(['hardlink', 'hardlink', 'run-summary'])
})
it('never overwrites an existing target file, even with different contents', async () => {
writeManagedSession(join('2026', '05', '26', 'rollout-a.jsonl'), 'managed contents\n')
const collidingPath = join(getSystemSessionsRoot(), '2026', '05', '26', 'rollout-a.jsonl')
mkdirSync(dirname(collidingPath), { recursive: true })
writeFileSync(collidingPath, 'user contents\n', 'utf-8')
const summary = await backfillManagedCodexSessionsIntoSystemHome(
resolveCodexSessionBackfillPaths()
)
expect(summary).toMatchObject({ scannedFiles: 1, linkedFiles: 0, skippedExistingFiles: 1 })
expect(readFileSync(collidingPath, 'utf-8')).toBe('user contents\n')
expect(readAuditActions()).toEqual(['run-summary'])
})
it('treats a broken symlink at the target as taken', async () => {
writeManagedSession(join('2026', '05', '26', 'rollout-a.jsonl'), 'managed contents\n')
const collidingPath = join(getSystemSessionsRoot(), '2026', '05', '26', 'rollout-a.jsonl')
mkdirSync(dirname(collidingPath), { recursive: true })
try {
symlinkSync(join(fakeHomeDir, 'missing-target.jsonl'), collidingPath)
} catch {
// Windows without symlink privilege cannot set up this fixture.
return
}
const summary = await backfillManagedCodexSessionsIntoSystemHome(
resolveCodexSessionBackfillPaths()
)
expect(summary).toMatchObject({ linkedFiles: 0, copiedFiles: 0, skippedExistingFiles: 1 })
expect(lstatSync(collidingPath).isSymbolicLink()).toBe(true)
})
it('does not backfill symlinked managed session files', async () => {
const realSource = join(fakeHomeDir, 'outside.jsonl')
writeFileSync(realSource, 'outside contents\n', 'utf-8')
const managedLinkPath = join(getManagedSessionsRoot(), '2026', '05', '26', 'rollout-a.jsonl')
mkdirSync(dirname(managedLinkPath), { recursive: true })
try {
symlinkSync(realSource, managedLinkPath)
} catch {
return
}
const summary = await backfillManagedCodexSessionsIntoSystemHome(
resolveCodexSessionBackfillPaths()
)
// Why: the session walker skips symlink dirents, so bridge-created links
// (which point back into the user's own home) never reach the copier.
expect(summary).toMatchObject({ linkedFiles: 0, copiedFiles: 0, failedFiles: 0 })
expect(existsSync(join(getSystemSessionsRoot(), '2026', '05', '26', 'rollout-a.jsonl'))).toBe(
false
)
})
it('is idempotent: a second run links nothing new and changes nothing', async () => {
writeManagedSession(join('2026', '05', '26', 'rollout-a.jsonl'), '{"id":"a"}\n')
const paths = resolveCodexSessionBackfillPaths()
const first = await backfillManagedCodexSessionsIntoSystemHome(paths)
const second = await backfillManagedCodexSessionsIntoSystemHome(paths)
expect(first).toMatchObject({ linkedFiles: 1 })
expect(second).toMatchObject({ linkedFiles: 0, copiedFiles: 0, skippedExistingFiles: 1 })
})
it('falls back to copy when hardlinking fails across volumes', async () => {
fsMockState.failLink = true
const managedPath = writeManagedSession(
join('2026', '05', '26', 'rollout-a.jsonl'),
'{"id":"a"}\n'
)
const summary = await backfillManagedCodexSessionsIntoSystemHome(
resolveCodexSessionBackfillPaths()
)
expect(summary).toMatchObject({ linkedFiles: 0, copiedFiles: 1, failedFiles: 0 })
const targetPath = join(getSystemSessionsRoot(), '2026', '05', '26', 'rollout-a.jsonl')
expect(readFileSync(targetPath, 'utf-8')).toBe(readFileSync(managedPath, 'utf-8'))
expect(lstatSync(targetPath).ino).not.toBe(lstatSync(managedPath).ino)
expect(readAuditActions()).toEqual(['copy', 'run-summary'])
})
it('records per-file failures without aborting the run', async () => {
fsMockState.failLink = true
fsMockState.failCopy = true
writeManagedSession(join('2026', '05', '26', 'rollout-a.jsonl'), '{"id":"a"}\n')
const summary = await backfillManagedCodexSessionsIntoSystemHome(
resolveCodexSessionBackfillPaths()
)
expect(summary).toMatchObject({ failedFiles: 1, linkedFiles: 0, copiedFiles: 0 })
expect(readAuditActions()).toEqual(['failed', 'run-summary'])
})
it('does not create the real sessions tree when there is nothing to backfill', async () => {
const summary = await backfillManagedCodexSessionsIntoSystemHome(
resolveCodexSessionBackfillPaths()
)
expect(summary).toMatchObject({ scannedFiles: 0 })
expect(existsSync(getSystemSessionsRoot())).toBe(false)
})
})
describe('startCodexSessionBackfillInBackground', () => {
it('writes a completion marker and skips the walk on later runs', async () => {
writeManagedSession(join('2026', '05', '26', 'rollout-a.jsonl'), '{"id":"a"}\n')
const first = await startCodexSessionBackfillInBackground()
expect(first).toMatchObject({ linkedFiles: 1, failedFiles: 0 })
expect(existsSync(getMarkerPath())).toBe(true)
// A file appearing after the marker must not be backfilled again.
writeManagedSession(join('2026', '07', '01', 'rollout-later.jsonl'), '{"id":"later"}\n')
const second = await startCodexSessionBackfillInBackground()
expect(second).toBeNull()
expect(
existsSync(join(getSystemSessionsRoot(), '2026', '07', '01', 'rollout-later.jsonl'))
).toBe(false)
})
it('leaves the marker unset when any file fails so the next startup retries', async () => {
fsMockState.failLink = true
fsMockState.failCopy = true
writeManagedSession(join('2026', '05', '26', 'rollout-a.jsonl'), '{"id":"a"}\n')
const first = await startCodexSessionBackfillInBackground()
expect(first).toMatchObject({ failedFiles: 1 })
expect(existsSync(getMarkerPath())).toBe(false)
fsMockState.failLink = false
fsMockState.failCopy = false
const second = await startCodexSessionBackfillInBackground()
expect(second).toMatchObject({ linkedFiles: 1, failedFiles: 0 })
expect(existsSync(getMarkerPath())).toBe(true)
})
it('honors a custom system Codex home override', async () => {
const customHome = join(fakeHomeDir, 'custom-codex-home')
writeManagedSession(join('2026', '05', '26', 'rollout-a.jsonl'), '{"id":"a"}\n')
const summary = await startCodexSessionBackfillInBackground({}, customHome)
expect(summary).toMatchObject({ linkedFiles: 1 })
expect(existsSync(join(customHome, 'sessions', '2026', '05', '26', 'rollout-a.jsonl'))).toBe(
true
)
expect(existsSync(getSystemSessionsRoot())).toBe(false)
})
})
+264
View File
@@ -0,0 +1,264 @@
import {
appendFileSync,
constants,
copyFileSync,
existsSync,
linkSync,
lstatSync,
mkdirSync,
readFileSync
} from 'node:fs'
import { dirname, join, relative } from 'node:path'
import { writeFileAtomically } from '../codex-accounts/fs-utils'
import {
getCodexSessionBackfillStateDirPath,
getOrcaManagedCodexHomePath,
getSystemCodexHomePath
} from './codex-home-paths'
import { listCodexSessionJsonlFilesIncrementally } from './codex-session-file-listing'
import type { CodexSessionBridgeIncrementalOptions } from './codex-session-file-listing'
// Why: bump to re-run the backfill for every host after a layout or semantics
// change; the run itself stays skip-existing so re-runs never overwrite.
const CODEX_SESSION_BACKFILL_MARKER_VERSION = 1
export type CodexSessionBackfillSummary = {
scannedFiles: number
linkedFiles: number
copiedFiles: number
skippedExistingFiles: number
skippedSymlinkFiles: number
failedFiles: number
}
export type CodexSessionBackfillPaths = {
managedSessionsRoot: string
systemSessionsRoot: string
auditLogPath: string
markerPath: string
}
let backgroundBackfillTask: Promise<CodexSessionBackfillSummary | null> | null = null
/**
* Resolves the production source/target/state paths for the session backfill.
*
* `systemCodexHomePathOverride` mirrors the session bridge: users who run
* Codex with a custom CODEX_HOME need their history placed where their own
* `codex resume` actually looks.
*/
export function resolveCodexSessionBackfillPaths(
systemCodexHomePathOverride?: string
): CodexSessionBackfillPaths {
const stateDir = getCodexSessionBackfillStateDirPath()
return {
managedSessionsRoot: join(getOrcaManagedCodexHomePath(), 'sessions'),
systemSessionsRoot: join(systemCodexHomePathOverride || getSystemCodexHomePath(), 'sessions'),
auditLogPath: join(stateDir, 'audit.jsonl'),
markerPath: join(stateDir, 'backfill-complete.json')
}
}
/**
* Starts the once-per-host background backfill of managed-home session files
* into the user's real Codex home.
*
* Concurrent callers share one in-flight task; a completed-marker host resolves
* to null without walking the sessions tree.
*/
export function startCodexSessionBackfillInBackground(
options: CodexSessionBridgeIncrementalOptions = {},
systemCodexHomePathOverride?: string
): Promise<CodexSessionBackfillSummary | null> {
if (backgroundBackfillTask) {
return backgroundBackfillTask
}
const task = runCodexSessionBackfillOncePerHost(options, systemCodexHomePathOverride).catch(
(error: unknown) => {
console.warn('[codex-session-backfill] Background session backfill failed:', error)
return null
}
)
backgroundBackfillTask = task
void task.finally(() => {
if (backgroundBackfillTask === task) {
backgroundBackfillTask = null
}
})
return task
}
async function runCodexSessionBackfillOncePerHost(
options: CodexSessionBridgeIncrementalOptions,
systemCodexHomePathOverride?: string
): Promise<CodexSessionBackfillSummary | null> {
const paths = resolveCodexSessionBackfillPaths(systemCodexHomePathOverride)
if (hasCompletedBackfillMarker(paths.markerPath)) {
return null
}
const summary = await backfillManagedCodexSessionsIntoSystemHome(paths, options)
// Why: per-file failures (locked or unreadable files) leave the marker unset
// so the next startup retries; skip-existing keeps those retries cheap.
if (summary.failedFiles === 0) {
writeBackfillMarker(paths.markerPath, summary)
}
return summary
}
/**
* Backfills managed-home session rollout files into the real Codex home.
*
* Non-destructive by contract: existing target files are always skipped, and
* nothing in either home is deleted or moved. Hardlink first so resume sees
* one physical JSONL log; copy is the cross-volume fallback.
*/
export async function backfillManagedCodexSessionsIntoSystemHome(
paths: CodexSessionBackfillPaths,
options: CodexSessionBridgeIncrementalOptions = {}
): Promise<CodexSessionBackfillSummary> {
const summary: CodexSessionBackfillSummary = {
scannedFiles: 0,
linkedFiles: 0,
copiedFiles: 0,
skippedExistingFiles: 0,
skippedSymlinkFiles: 0,
failedFiles: 0
}
if (existsSync(paths.managedSessionsRoot)) {
for await (const managedSessionFilePath of listCodexSessionJsonlFilesIncrementally(
paths.managedSessionsRoot,
options
)) {
summary.scannedFiles += 1
backfillOneManagedSessionFile(paths, managedSessionFilePath, summary)
}
}
appendAuditRecord(paths.auditLogPath, { action: 'run-summary', ...summary })
return summary
}
function backfillOneManagedSessionFile(
paths: CodexSessionBackfillPaths,
managedSessionFilePath: string,
summary: CodexSessionBackfillSummary
): void {
if (isSymbolicLink(managedSessionFilePath)) {
// Why: bridge-created symlinks already point at a file in the user's own
// home; materializing them here could duplicate a foreign tree.
summary.skippedSymlinkFiles += 1
return
}
const relativePath = relative(paths.managedSessionsRoot, managedSessionFilePath)
const systemSessionFilePath = join(paths.systemSessionsRoot, relativePath)
if (pathEntryExists(systemSessionFilePath)) {
summary.skippedExistingFiles += 1
return
}
try {
mkdirSync(dirname(systemSessionFilePath), { recursive: true })
linkSync(managedSessionFilePath, systemSessionFilePath)
summary.linkedFiles += 1
appendAuditRecord(paths.auditLogPath, {
action: 'hardlink',
source: managedSessionFilePath,
target: systemSessionFilePath
})
} catch (linkError) {
if (isExistsError(linkError)) {
summary.skippedExistingFiles += 1
return
}
try {
// Why: hardlinks fail across volumes; COPYFILE_EXCL keeps the
// never-overwrite contract even if the target appeared mid-run.
copyFileSync(managedSessionFilePath, systemSessionFilePath, constants.COPYFILE_EXCL)
summary.copiedFiles += 1
appendAuditRecord(paths.auditLogPath, {
action: 'copy',
source: managedSessionFilePath,
target: systemSessionFilePath
})
} catch (copyError) {
if (isExistsError(copyError)) {
summary.skippedExistingFiles += 1
return
}
summary.failedFiles += 1
appendAuditRecord(paths.auditLogPath, {
action: 'failed',
source: managedSessionFilePath,
target: systemSessionFilePath,
error: describeError(copyError),
linkError: describeError(linkError)
})
}
}
}
function isSymbolicLink(filePath: string): boolean {
try {
return lstatSync(filePath).isSymbolicLink()
} catch {
return false
}
}
/** Existence via lstat so a broken symlink at the target still counts as taken. */
function pathEntryExists(entryPath: string): boolean {
try {
lstatSync(entryPath)
return true
} catch {
return false
}
}
function isExistsError(error: unknown): boolean {
return (error as NodeJS.ErrnoException | null)?.code === 'EEXIST'
}
function describeError(error: unknown): string {
return error instanceof Error ? error.message : String(error)
}
function appendAuditRecord(auditLogPath: string, record: Record<string, unknown>): void {
try {
mkdirSync(dirname(auditLogPath), { recursive: true })
appendFileSync(
auditLogPath,
`${JSON.stringify({ at: new Date().toISOString(), ...record })}\n`,
{
encoding: 'utf-8'
}
)
} catch (error) {
// Why: the audit trail is diagnostics; losing a line must not fail the
// backfill or leave a half-linked tree unrecorded in the summary counts.
console.warn('[codex-session-backfill] Failed to append audit record:', error)
}
}
function hasCompletedBackfillMarker(markerPath: string): boolean {
try {
const parsed: unknown = JSON.parse(readFileSync(markerPath, 'utf-8'))
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
return false
}
return (parsed as { version?: unknown }).version === CODEX_SESSION_BACKFILL_MARKER_VERSION
} catch {
return false
}
}
function writeBackfillMarker(markerPath: string, summary: CodexSessionBackfillSummary): void {
mkdirSync(dirname(markerPath), { recursive: true })
writeFileAtomically(
markerPath,
`${JSON.stringify(
{ version: CODEX_SESSION_BACKFILL_MARKER_VERSION, completedAt: Date.now(), summary },
null,
2
)}\n`
)
}
+12
View File
@@ -129,6 +129,8 @@ import {
} from './codex-accounts/runtime-selection'
import { normalizeClaudeRuntimeSelection } from './claude-accounts/runtime-selection'
import { codexHookService } from './codex/hook-service'
import { startCodexSessionBackfillInBackground } from './codex/codex-session-backfill'
import { resolveHostCodexSessionSourceHome } from './codex/codex-session-source-home'
import { getDefaultWslDistro } from './wsl'
import { ClaudeAccountService } from './claude-accounts/service'
import { ClaudeRuntimeAuthService } from './claude-accounts/runtime-auth-service'
@@ -1749,6 +1751,16 @@ app.whenReady().then(async () => {
rateLimits = new RateLimitService()
codexRuntimeHome = new CodexRuntimeHomeService(store)
codexAccounts = new CodexAccountService(store, rateLimits, codexRuntimeHome)
// Why: one-time per-host backfill makes historical Orca-managed Codex
// sessions visible to the user's own resume picker and app history (#4444,
// #8612). Deferred so startup and first PTY spawns never compete with the
// sessions tree walk.
setTimeout(() => {
void startCodexSessionBackfillInBackground(
{},
resolveHostCodexSessionSourceHome(store!.getSettings())
)
}, 15_000)
claudeRuntimeAuth = new ClaudeRuntimeAuthService(store)
claudeAccounts = new ClaudeAccountService(store, rateLimits, claudeRuntimeAuth)
rateLimits.setCodexHomePathResolver((target) =>