Bridge WSL Codex sessions into runtime home (#7477)

This commit is contained in:
Jinwoo Hong 2026-07-06 15:11:48 -07:00 committed by GitHub
parent 6aed5c1122
commit 377bde351c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 513 additions and 2 deletions

View File

@ -484,6 +484,134 @@ describe('CodexRuntimeHomeService', () => {
}
})
it('starts WSL session bridging after materializing the WSL launch home', async () => {
const originalPlatform = Object.getOwnPropertyDescriptor(process, 'platform')
Object.defineProperty(process, 'platform', { configurable: true, value: 'win32' })
const startWslCodexSessionBridgeInBackground = vi.fn(() => Promise.resolve())
vi.doMock('../codex/wsl-codex-session-bridge', () => ({
startWslCodexSessionBridgeInBackground
}))
const wslHome = join(testState.userDataDir, 'wsl-home')
vi.doMock('../wsl', () => ({
getDefaultWslDistro: () => 'Ubuntu',
getWslHome: () => wslHome
}))
const store = createStore(
createSettings({
activeCodexManagedAccountId: null,
activeCodexManagedAccountIdsByRuntime: { host: null, wsl: { Ubuntu: null } }
})
)
try {
const { CodexRuntimeHomeService } = await import('./runtime-home-service')
const service = new CodexRuntimeHomeService(store as never)
const wslRuntimeHomePath = join(
wslHome,
'.local',
'share',
'orca',
'codex-runtime-home',
'home'
)
expect(service.prepareForCodexLaunch({ runtime: 'wsl', wslDistro: 'Ubuntu' })).toBe(
wslRuntimeHomePath
)
expect(startWslCodexSessionBridgeInBackground).toHaveBeenCalledTimes(1)
expect(startWslCodexSessionBridgeInBackground).toHaveBeenCalledWith({
distro: 'Ubuntu',
systemCodexHomePath: join(wslHome, '.codex'),
managedCodexHomePath: wslRuntimeHomePath
})
} finally {
vi.doUnmock('../codex/wsl-codex-session-bridge')
vi.doUnmock('../wsl')
if (originalPlatform) {
Object.defineProperty(process, 'platform', originalPlatform)
}
}
})
it('starts WSL session bridging for the distro used by the materialized runtime home', async () => {
const originalPlatform = Object.getOwnPropertyDescriptor(process, 'platform')
Object.defineProperty(process, 'platform', { configurable: true, value: 'win32' })
const startWslCodexSessionBridgeInBackground = vi.fn(() => Promise.resolve())
vi.doMock('../codex/wsl-codex-session-bridge', () => ({
startWslCodexSessionBridgeInBackground
}))
const wslHome = join(testState.userDataDir, 'debian-wsl-home')
const wslRuntimeHomePath = join(
wslHome,
'.local',
'share',
'orca',
'codex-runtime-home',
'home'
)
vi.doMock('../wsl', () => ({
getDefaultWslDistro: () => null,
getWslHome: (distro: string) => (distro === 'Debian' ? wslHome : null)
}))
vi.doMock('../../shared/wsl-paths', () => ({
parseWslUncPath: (candidate: string) =>
candidate === wslRuntimeHomePath
? {
distro: 'Debian',
linuxPath: '/home/alice/.local/share/orca/codex-runtime-home/home'
}
: null
}))
const managedHomePath = createManagedAuth(
testState.userDataDir,
'debian-account',
'{"account":"debian"}\n'
)
const store = createStore(
createSettings({
codexManagedAccounts: [
{
id: 'debian-account',
email: 'debian@example.com',
managedHomePath,
managedHomeRuntime: 'wsl',
wslDistro: 'Debian',
wslLinuxHomePath: '/home/alice/.local/share/orca/codex-accounts/debian/home',
providerAccountId: null,
workspaceLabel: null,
workspaceAccountId: null,
createdAt: 1,
updatedAt: 1,
lastAuthenticatedAt: 1
}
],
activeCodexManagedAccountId: null,
activeCodexManagedAccountIdsByRuntime: { host: null, wsl: { Debian: 'debian-account' } }
})
)
try {
const { CodexRuntimeHomeService } = await import('./runtime-home-service')
const service = new CodexRuntimeHomeService(store as never)
expect(service.prepareForCodexLaunch({ runtime: 'wsl', wslDistro: null })).toBe(
wslRuntimeHomePath
)
expect(startWslCodexSessionBridgeInBackground).toHaveBeenCalledWith({
distro: 'Debian',
systemCodexHomePath: join(wslHome, '.codex'),
managedCodexHomePath: wslRuntimeHomePath
})
} finally {
vi.doUnmock('../codex/wsl-codex-session-bridge')
vi.doUnmock('../wsl')
vi.doUnmock('../../shared/wsl-paths')
if (originalPlatform) {
Object.defineProperty(process, 'platform', originalPlatform)
}
}
})
it('restores the system-default snapshot when no managed account is selected', async () => {
const runtimeAuthPath = getRuntimeCodexAuthPath()
writeFileSync(getSystemCodexAuthPath(), '{"account":"system"}\n', 'utf-8')

View File

@ -40,6 +40,7 @@ import {
syncSystemCodexResourcesIntoManagedHome
} from '../codex/codex-home-paths'
import { startSystemCodexSessionBridgeInBackground } from '../codex/codex-session-bridge'
import { startWslCodexSessionBridgeInBackground } from '../codex/wsl-codex-session-bridge'
import {
prepareSystemConfigForFreshRuntimeMirror,
syncSystemConfigIntoManagedCodexHome
@ -134,10 +135,11 @@ export class CodexRuntimeHomeService {
prepareForCodexLaunch(target?: CodexAccountSelectionTarget): string | null {
if (target?.runtime === 'wsl') {
const wslTarget = this.resolveWslDefaultTarget(target)
return (
const runtimeHomePath =
this.syncWslRuntimeForCurrentSelection(wslTarget) ??
this.getWslSystemCodexHomePath(wslTarget)
)
this.startWslSessionBridgeForLaunch(wslTarget, runtimeHomePath)
return runtimeHomePath
}
this.syncForCurrentSelection()
syncSystemCodexResourcesIntoManagedHome()
@ -148,6 +150,34 @@ export class CodexRuntimeHomeService {
return this.getRuntimeHomePath()
}
private startWslSessionBridgeForLaunch(
target: CodexAccountSelectionTarget,
runtimeHomePath: string | null
): void {
if (process.platform !== 'win32' || !runtimeHomePath) {
return
}
const runtimeHomeWsl = parseWslUncPath(runtimeHomePath)
const distro = target.wslDistro?.trim() || runtimeHomeWsl?.distro || getDefaultWslDistro()
if (!distro) {
return
}
const systemCodexHomePath = this.getWslSystemCodexHomePath({
runtime: 'wsl',
wslDistro: distro
})
if (!systemCodexHomePath || systemCodexHomePath === runtimeHomePath) {
return
}
// Why: WSL history must be hardlinked inside the distro; host-side links
// cannot bridge Windows and WSL filesystems in a resume-visible way.
void startWslCodexSessionBridgeInBackground({
distro,
systemCodexHomePath,
managedCodexHomePath: runtimeHomePath
})
}
getHostRuntimeHomePath(): string {
return this.getRuntimeHomePath()
}

View File

@ -0,0 +1,168 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
import type { ChildProcess } from 'node:child_process'
const { execFileMock } = vi.hoisted(() => ({
execFileMock: vi.fn()
}))
vi.mock('node:child_process', () => ({
execFile: execFileMock
}))
import {
buildWslCodexSessionBridgeShellCommand,
resolveWslCodexSessionBridgeLinuxPaths,
startWslCodexSessionBridgeInBackground,
syncWslCodexSessionsIntoManagedHome
} from './wsl-codex-session-bridge'
function mockExecFileSuccess(stdout = '{"scannedFiles":2,"linkedFiles":1}\n'): void {
execFileMock.mockImplementation(
(
_command: string,
_args: string[],
_options: unknown,
callback: (error: Error | null, stdout: string, stderr: string) => void
): ChildProcess => {
callback(null, stdout, '')
return {} as ChildProcess
}
)
}
beforeEach(() => {
execFileMock.mockReset()
})
describe('syncWslCodexSessionsIntoManagedHome', () => {
it('runs a WSL hardlink bridge from the WSL system sessions into the managed home', async () => {
mockExecFileSuccess()
const summary = await syncWslCodexSessionsIntoManagedHome({
distro: 'Ubuntu',
systemCodexHomePath: '\\\\wsl.localhost\\Ubuntu\\home\\alice\\.codex',
managedCodexHomePath:
'\\\\wsl.localhost\\Ubuntu\\home\\alice\\.local\\share\\orca\\codex-runtime-home\\home'
})
expect(summary).toEqual({ scannedFiles: 2, linkedFiles: 1 })
expect(execFileMock).toHaveBeenCalledTimes(1)
const firstCall = execFileMock.mock.calls[0]
expect(firstCall).toBeDefined()
const [command, args, options] = firstCall as [
string,
string[],
{ timeout?: number; windowsHide?: boolean }
]
expect(command).toBe('wsl.exe')
expect(args.slice(0, 5)).toEqual(['-d', 'Ubuntu', '--', 'bash', '-lc'])
expect(args).toHaveLength(6)
expect(options.timeout).toBe(30_000)
expect(options.windowsHide).toBe(true)
const shellCommand = args[5]
expect(shellCommand).toContain("source_sessions_root='/home/alice/.codex/sessions'")
expect(shellCommand).toContain(
"managed_sessions_root='/home/alice/.local/share/orca/codex-runtime-home/home/sessions'"
)
expect(shellCommand).toContain(`find "\\$source_sessions_root" -type f -name '*.jsonl' -print0`)
expect(shellCommand).toContain('ln -- "\\$source_file" "\\$target_file"')
expect(shellCommand).toContain('if [ -e "\\$target_file" ] || [ -L "\\$target_file" ]; then')
expect(shellCommand).not.toContain('ln -s')
expect(shellCommand).not.toContain('cp ')
expect(shellCommand).not.toContain('sqlite')
})
it('does not invoke WSL when paths are not resolvable inside the distro', async () => {
const summary = await syncWslCodexSessionsIntoManagedHome({
distro: 'Ubuntu',
systemCodexHomePath: 'C:\\Users\\alice\\.codex',
managedCodexHomePath: 'C:\\Users\\alice\\AppData\\Roaming\\orca\\codex-runtime-home\\home'
})
expect(summary).toEqual({ scannedFiles: 0, linkedFiles: 0 })
expect(execFileMock).not.toHaveBeenCalled()
})
it('parses the summary after WSL profile stdout', async () => {
mockExecFileSuccess('Welcome to Ubuntu\nprofile output\n{"scannedFiles":4,"linkedFiles":3}\n')
const summary = await syncWslCodexSessionsIntoManagedHome({
distro: 'Ubuntu',
systemCodexHomePath: '\\\\wsl.localhost\\Ubuntu\\home\\alice\\.codex',
managedCodexHomePath:
'\\\\wsl.localhost\\Ubuntu\\home\\alice\\.local\\share\\orca\\codex-runtime-home\\home'
})
expect(summary).toEqual({ scannedFiles: 4, linkedFiles: 3 })
})
it('coalesces duplicate background bridges for the same WSL target', async () => {
mockExecFileSuccess()
const target = {
distro: 'Ubuntu',
systemCodexHomePath: '\\\\wsl.localhost\\Ubuntu\\home\\alice\\.codex',
managedCodexHomePath:
'\\\\wsl.localhost\\Ubuntu\\home\\alice\\.local\\share\\orca\\codex-runtime-home\\home'
}
const firstTask = startWslCodexSessionBridgeInBackground(target)
const secondTask = startWslCodexSessionBridgeInBackground(target)
expect(firstTask).toBe(secondTask)
await firstTask
expect(execFileMock).toHaveBeenCalledTimes(1)
})
})
describe('resolveWslCodexSessionBridgeLinuxPaths', () => {
it('requires both homes to belong to the requested distro', () => {
expect(
resolveWslCodexSessionBridgeLinuxPaths({
distro: 'Ubuntu',
systemCodexHomePath: '\\\\wsl.localhost\\Ubuntu\\home\\alice\\.codex',
managedCodexHomePath:
'\\\\wsl.localhost\\Debian\\home\\alice\\.local\\share\\orca\\codex-runtime-home\\home'
})
).toBeNull()
})
it('accepts Linux paths for direct script construction tests', () => {
expect(
resolveWslCodexSessionBridgeLinuxPaths({
distro: 'Ubuntu',
systemCodexHomePath: '/home/alice/.codex',
managedCodexHomePath: '/home/alice/.local/share/orca/codex-runtime-home/home'
})
).toEqual({
systemSessionsRoot: '/home/alice/.codex/sessions',
managedSessionsRoot: '/home/alice/.local/share/orca/codex-runtime-home/home/sessions'
})
})
})
describe('buildWslCodexSessionBridgeShellCommand', () => {
it('only targets JSONL session files under sessions', () => {
const shellCommand = buildWslCodexSessionBridgeShellCommand({
systemSessionsRoot: "/home/alice/.codex/sessions with 'quote'",
managedSessionsRoot: '/home/alice/.local/share/orca/codex-runtime-home/home/sessions'
})
expect(shellCommand).toContain(
`source_sessions_root='/home/alice/.codex/sessions with '\\''quote'\\'''`
)
expect(shellCommand).toContain(`-name '*.jsonl'`)
expect(shellCommand).not.toContain('.sqlite')
})
it('escapes Linux-side shell variable expansion for wsl.exe argv', () => {
const shellCommand = buildWslCodexSessionBridgeShellCommand({
systemSessionsRoot: '/home/alice/.codex/sessions',
managedSessionsRoot: '/home/alice/.local/share/orca/codex-runtime-home/home/sessions'
})
expect(shellCommand).toContain('\\$source_sessions_root')
expect(shellCommand).toContain('\\$source_file')
expect(shellCommand).toContain('\\$((scanned_files + 1))')
})
})

View File

@ -0,0 +1,185 @@
import { execFile } from 'node:child_process'
import { posix as pathPosix } from 'node:path'
import { escapeWslShCommandForWindows } from '../../shared/wsl-login-shell-command'
import { parseWslUncPath } from '../../shared/wsl-paths'
export type WslCodexSessionBridgeTarget = {
distro: string
systemCodexHomePath: string
managedCodexHomePath: string
}
export type WslCodexSessionBridgeLinuxPaths = {
systemSessionsRoot: string
managedSessionsRoot: string
}
export type WslCodexSessionBridgeSummary = {
scannedFiles: number
linkedFiles: number
}
const emptySummary: WslCodexSessionBridgeSummary = { scannedFiles: 0, linkedFiles: 0 }
const backgroundWslSessionBridgeTasks = new Map<string, Promise<void>>()
const WSL_SESSION_BRIDGE_TIMEOUT_MS = 30_000
export function startWslCodexSessionBridgeInBackground(
target: WslCodexSessionBridgeTarget
): Promise<void> {
const taskKey = getWslSessionBridgeTaskKey(target)
const existingTask = backgroundWslSessionBridgeTasks.get(taskKey)
if (existingTask) {
return existingTask
}
const task = syncWslCodexSessionsIntoManagedHome(target)
.catch((error: unknown) => {
console.warn('[codex-session-bridge] Background WSL session bridge failed:', error)
})
.then(() => undefined)
backgroundWslSessionBridgeTasks.set(taskKey, task)
void task.finally(() => {
if (backgroundWslSessionBridgeTasks.get(taskKey) === task) {
backgroundWslSessionBridgeTasks.delete(taskKey)
}
})
return task
}
export async function syncWslCodexSessionsIntoManagedHome(
target: WslCodexSessionBridgeTarget
): Promise<WslCodexSessionBridgeSummary> {
const paths = resolveWslCodexSessionBridgeLinuxPaths(target)
if (!paths) {
return emptySummary
}
const stdout = await execFileUtf8('wsl.exe', [
'-d',
target.distro,
'--',
'bash',
'-lc',
buildWslCodexSessionBridgeShellCommand(paths)
])
return parseWslSessionBridgeSummary(stdout)
}
export function resolveWslCodexSessionBridgeLinuxPaths(
target: WslCodexSessionBridgeTarget
): WslCodexSessionBridgeLinuxPaths | null {
const systemHomePath = getLinuxPathForWslDistro(target.systemCodexHomePath, target.distro)
const managedHomePath = getLinuxPathForWslDistro(target.managedCodexHomePath, target.distro)
if (!systemHomePath || !managedHomePath) {
return null
}
return {
systemSessionsRoot: joinLinuxPath(systemHomePath, 'sessions'),
managedSessionsRoot: joinLinuxPath(managedHomePath, 'sessions')
}
}
export function buildWslCodexSessionBridgeShellCommand(
paths: WslCodexSessionBridgeLinuxPaths
): string {
const shellCommand = [
'set -u',
`source_sessions_root=${quoteBashString(paths.systemSessionsRoot)}`,
`managed_sessions_root=${quoteBashString(paths.managedSessionsRoot)}`,
'scanned_files=0',
'linked_files=0',
'if [ ! -d "$source_sessions_root" ]; then',
` printf '{"scannedFiles":0,"linkedFiles":0}\\n'`,
' exit 0',
'fi',
"while IFS= read -r -d '' source_file; do",
' scanned_files=$((scanned_files + 1))',
' relative_path=${source_file#"$source_sessions_root"/}',
' target_file="$managed_sessions_root/$relative_path"',
' if [ -e "$target_file" ] || [ -L "$target_file" ]; then',
' continue',
' fi',
' target_dir=${target_file%/*}',
' mkdir -p -- "$target_dir" || continue',
// Why: Codex resume ignores symlinked JSONL, so WSL links must be
// Linux hardlinks created inside the distro filesystem.
' if ln -- "$source_file" "$target_file"; then',
' linked_files=$((linked_files + 1))',
' fi',
`done < <(find "$source_sessions_root" -type f -name '*.jsonl' -print0 2>/dev/null)`,
`printf '{"scannedFiles":%s,"linkedFiles":%s}\\n' "$scanned_files" "$linked_files"`
].join('\n')
return escapeWslShCommandForWindows(shellCommand)
}
function getWslSessionBridgeTaskKey(target: WslCodexSessionBridgeTarget): string {
return [target.distro, target.systemCodexHomePath, target.managedCodexHomePath].join('\0')
}
function getLinuxPathForWslDistro(path: string, distro: string): string | null {
const wslPath = parseWslUncPath(path)
if (wslPath) {
return wslDistroNamesMatch(wslPath.distro, distro) ? wslPath.linuxPath : null
}
return path.startsWith('/') ? path : null
}
function wslDistroNamesMatch(left: string, right: string): boolean {
return left.toLowerCase() === right.toLowerCase()
}
function joinLinuxPath(basePath: string, ...segments: string[]): string {
return pathPosix.join(basePath, ...segments)
}
function quoteBashString(value: string): string {
return `'${value.replace(/'/g, `'\\''`)}'`
}
function execFileUtf8(command: string, args: string[]): Promise<string> {
return new Promise((resolve, reject) => {
execFile(
command,
args,
{
encoding: 'utf-8',
maxBuffer: 1024 * 1024,
timeout: WSL_SESSION_BRIDGE_TIMEOUT_MS,
windowsHide: true
},
(error, stdout) => {
if (error) {
reject(error)
return
}
resolve(stdout)
}
)
})
}
function parseWslSessionBridgeSummary(stdout: string): WslCodexSessionBridgeSummary {
try {
// Why: login/profile scripts may write stdout before the bridge summary.
const summaryLine =
stdout
.split(/\r?\n/)
.findLast((line) => line.trim().length > 0)
?.trim() ?? ''
const parsed: unknown = JSON.parse(summaryLine)
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
return emptySummary
}
const summary = parsed as Record<string, unknown>
if (typeof summary.scannedFiles !== 'number' || typeof summary.linkedFiles !== 'number') {
return emptySummary
}
return {
scannedFiles: summary.scannedFiles,
linkedFiles: summary.linkedFiles
}
} catch {
return emptySummary
}
}