diff --git a/src/main/daemon/pty-subprocess-windows-shell-launch.test.ts b/src/main/daemon/pty-subprocess-windows-shell-launch.test.ts index 8dd0b420008..fde0925719c 100644 --- a/src/main/daemon/pty-subprocess-windows-shell-launch.test.ts +++ b/src/main/daemon/pty-subprocess-windows-shell-launch.test.ts @@ -231,12 +231,9 @@ describe('createPtySubprocess', () => { expect(spawnMock).toHaveBeenCalledWith( 'cmd.exe', - [ - '/K', - 'chcp 65001 > nul & if defined ORCA_CODEX_LAUNCH_PREFLIGHT call %ORCA_CODEX_LAUNCH_PREFLIGHT_CMD_QUOTE%%ORCA_CODEX_LAUNCH_PREFLIGHT%%ORCA_CODEX_LAUNCH_PREFLIGHT_CMD_QUOTE% agent hooks prepare-codex > nul 2>&1' - ], + ['/K', 'chcp 65001 > nul'], expect.objectContaining({ - env: expect.objectContaining({ ORCA_CODEX_LAUNCH_PREFLIGHT_CMD_QUOTE: '"' }) + env: expect.objectContaining({ ORCA_CODEX_LAUNCH_PREFLIGHT: CODEX_LAUNCH_PREFLIGHT }) }) ) }) @@ -297,10 +294,7 @@ describe('createPtySubprocess', () => { expect(spawnMock).toHaveBeenCalledWith( 'cmd.exe', - [ - '/K', - 'chcp 65001 > nul & if defined ORCA_CODEX_LAUNCH_PREFLIGHT call %ORCA_CODEX_LAUNCH_PREFLIGHT_CMD_QUOTE%%ORCA_CODEX_LAUNCH_PREFLIGHT%%ORCA_CODEX_LAUNCH_PREFLIGHT_CMD_QUOTE% agent hooks prepare-codex > nul 2>&1' - ], + ['/K', 'chcp 65001 > nul'], expect.any(Object) ) expect(handle!.startupCommandDeliveredInShellArgs).toBeUndefined() diff --git a/src/main/daemon/pty-subprocess.ts b/src/main/daemon/pty-subprocess.ts index 2f983e4f7c5..249f896dc05 100644 --- a/src/main/daemon/pty-subprocess.ts +++ b/src/main/daemon/pty-subprocess.ts @@ -22,10 +22,7 @@ import { import { resolveProcessExitCause, type TerminalExitCause } from '../../shared/terminal-exit-cause' import { signalPosixPtyForegroundGroup } from '../pty/posix-pty-foreground-group' import { readPtsName } from '../pty/node-pty-pts-name' -import { - ORCA_CODEX_LAUNCH_PREFLIGHT_CMD_QUOTE_ENV, - resolveWindowsShellLaunchArgs -} from '../providers/windows-shell-args' +import { resolveWindowsShellLaunchArgs } from '../providers/windows-shell-args' import { resolveEffectiveWindowsPowerShell, shouldProbeWindowsPowerShellAvailability, @@ -747,13 +744,6 @@ export async function createPtySubprocess(opts: PtySubprocessOptions): Promise vi.fn(() => null)) +vi.mock('../codex/managed-home-shell-preflight', () => ({ + prepareManagedCodexHomeBeforeShellLaunch: prepareManagedCodexHomeMock, + resolveManagedCodexShellPreflightHome: () => null +})) vi.mock('electron', () => import('./pty-ipc-mock-registry').then((m) => m.electronModuleMock())) vi.mock('fs', () => import('./pty-ipc-mock-registry').then((m) => m.fsModuleMock())) vi.mock('node-pty', () => import('./pty-ipc-mock-registry').then((m) => m.nodePtyModuleMock())) @@ -244,6 +249,34 @@ describe('registerPtyHandlers', () => { // wrapper inherits, so the preflight must carry the CLI's verified absolute path. expect(env.ORCA_CODEX_LAUNCH_PREFLIGHT).toBe(BUNDLED_CLI_PATH) }) + // Why: cmd.exe has no deferred `codex` wrapper, so its preflight ran inline at + // shell start as a CLI round-trip into this app — slow enough to push agent + // launches past their startup budget. Same prep, done in-process instead. + it('prepares the managed Codex home in-process instead of from the shell', async () => { + prepareManagedCodexHomeMock.mockClear() + const env = await withBundledCli(() => + spawnAndGetEnv(undefined, undefined, () => TEST_CODEX_HOME) + ) + + expect(env.ORCA_CODEX_LAUNCH_PREFLIGHT).toBe(BUNDLED_CLI_PATH) + expect(prepareManagedCodexHomeMock).toHaveBeenCalledWith( + expect.objectContaining({ + hooksEnabled: true, + env: expect.objectContaining({ + CODEX_HOME: TEST_CODEX_HOME, + ORCA_CODEX_HOME: TEST_CODEX_HOME + }) + }) + ) + }) + it('does not prepare a managed Codex home when no preflight is installed', async () => { + prepareManagedCodexHomeMock.mockClear() + await withBundledCli(() => spawnAndGetEnv(undefined, undefined, () => TEST_CODEX_HOME), { + launcherExecutable: false + }) + + expect(prepareManagedCodexHomeMock).not.toHaveBeenCalled() + }) it('skips the Codex launch preflight when the bundled CLI is not executable', async () => { const env = await withBundledCli( () => spawnAndGetEnv(undefined, undefined, () => TEST_CODEX_HOME), diff --git a/src/main/ipc/pty.ts b/src/main/ipc/pty.ts index cac4538be37..d906cb4a686 100644 --- a/src/main/ipc/pty.ts +++ b/src/main/ipc/pty.ts @@ -61,7 +61,10 @@ import { import { agentHookServer } from '../agent-hooks/server' import { wslHookRelayManager } from '../agent-hooks/wsl-hook-relay-manager' import { ensureWslHookRelayForReattach } from '../agent-hooks/wsl-hook-relay-reattach' -import { isAgentStatusHooksEnabled } from '../agent-hooks/managed-agent-hook-controls' +import { + isAgentStatusHooksEnabled, + prepareManagedCodexHomeBeforeShellLaunch +} from '../agent-hooks/managed-agent-hook-controls' import { piTitlebarExtensionService } from '../pi/titlebar-extension-service' import { detectExplicitPiAgentKindFromCommand, @@ -1933,6 +1936,14 @@ export function buildPtyHostEnv( }) if (preflightCommand) { baseEnv.ORCA_CODEX_LAUNCH_PREFLIGHT = preflightCommand + // Why: cmd.exe has no deferred `codex` wrapper, so its preflight ran inline + // at shell start — a CLI round-trip back into this app that stalled agent + // launches for seconds. Same idempotent prep, done in-process instead. + prepareManagedCodexHomeBeforeShellLaunch({ + env: baseEnv, + userDataPath: opts.userDataPath, + hooksEnabled: true + }) } else { delete baseEnv.ORCA_CODEX_LAUNCH_PREFLIGHT } diff --git a/src/main/providers/local-pty-provider-windows-shell-launch.test.ts b/src/main/providers/local-pty-provider-windows-shell-launch.test.ts index 5f95812d5ae..0a71db4acd4 100644 --- a/src/main/providers/local-pty-provider-windows-shell-launch.test.ts +++ b/src/main/providers/local-pty-provider-windows-shell-launch.test.ts @@ -580,7 +580,7 @@ describe('LocalPtyProvider', () => { ) }) - it('runs the Codex preflight once in the cmd.exe startup chain', async () => { + it('keeps the Codex preflight out of the cmd.exe startup chain', async () => { const platform = Object.getOwnPropertyDescriptor(process, 'platform') Object.defineProperty(process, 'platform', { value: 'win32' }) provider.configure({ @@ -601,14 +601,10 @@ describe('LocalPtyProvider', () => { expect(spawnMock).toHaveBeenCalledWith( 'cmd.exe', - [ - '/K', - 'chcp 65001 > nul & if defined ORCA_CODEX_LAUNCH_PREFLIGHT call %ORCA_CODEX_LAUNCH_PREFLIGHT_CMD_QUOTE%%ORCA_CODEX_LAUNCH_PREFLIGHT%%ORCA_CODEX_LAUNCH_PREFLIGHT_CMD_QUOTE% agent hooks prepare-codex > nul 2>&1' - ], + ['/K', 'chcp 65001 > nul'], expect.objectContaining({ env: expect.objectContaining({ - ORCA_CODEX_LAUNCH_PREFLIGHT: CODEX_LAUNCH_PREFLIGHT, - ORCA_CODEX_LAUNCH_PREFLIGHT_CMD_QUOTE: '"' + ORCA_CODEX_LAUNCH_PREFLIGHT: CODEX_LAUNCH_PREFLIGHT }) }) ) diff --git a/src/main/providers/local-pty-provider.ts b/src/main/providers/local-pty-provider.ts index 6678e75c288..0736b0dfe48 100644 --- a/src/main/providers/local-pty-provider.ts +++ b/src/main/providers/local-pty-provider.ts @@ -1,10 +1,7 @@ /* eslint-disable max-lines -- Why: splitting spawn() would scatter tightly coupled PTY lifecycle logic (scan → ready → write → exit) with no cleaner ownership seam. */ import { basename, delimiter, win32 as pathWin32 } from 'node:path' import { randomUUID } from 'node:crypto' -import { - ORCA_CODEX_LAUNCH_PREFLIGHT_CMD_QUOTE_ENV, - resolveWindowsShellLaunchArgs -} from './windows-shell-args' +import { resolveWindowsShellLaunchArgs } from './windows-shell-args' import { resolveEffectiveWindowsPowerShell, shouldProbeWindowsPowerShellAvailability, @@ -796,16 +793,8 @@ export class LocalPtyProvider implements IPtyProvider { delete finalEnv.ORCA_CODEX_HOME } - const shellBasename = pathWin32.basename(shellPath).toLowerCase() const codexLaunchPreflightCommand = finalEnv.ORCA_CODEX_LAUNCH_PREFLIGHT - if ( - codexLaunchPreflightCommand && - (shellBasename === 'cmd.exe' || isWindowsGitBashShellPath(shellPath)) - ) { - if (shellBasename === 'cmd.exe') { - // Why: node-pty backslash-escapes argv quotes; expand the quote inside cmd.exe instead. - finalEnv[ORCA_CODEX_LAUNCH_PREFLIGHT_CMD_QUOTE_ENV] = '"' - } + if (codexLaunchPreflightCommand && isWindowsGitBashShellPath(shellPath)) { const resolved = resolveWindowsShellLaunchArgs( shellPath, cwd, diff --git a/src/main/providers/windows-shell-args.test.ts b/src/main/providers/windows-shell-args.test.ts index c71b9b06722..4447539791d 100644 --- a/src/main/providers/windows-shell-args.test.ts +++ b/src/main/providers/windows-shell-args.test.ts @@ -9,8 +9,6 @@ import { resolveWindowsShellLaunchArgs } from './windows-shell-args' import { getShellReadyWrapperRoot } from './local-pty-shell-ready-wrapper-root' const CODEX_LAUNCH_PREFLIGHT = 'C:\\Program Files\\Orca\\orca.exe' -const CMD_CODEX_LAUNCH_PREFLIGHT = - 'if defined ORCA_CODEX_LAUNCH_PREFLIGHT call %ORCA_CODEX_LAUNCH_PREFLIGHT_CMD_QUOTE%%ORCA_CODEX_LAUNCH_PREFLIGHT%%ORCA_CODEX_LAUNCH_PREFLIGHT_CMD_QUOTE% agent hooks prepare-codex > nul 2>&1' function expectedWslArgs(linuxCwd: string, distro?: string): string[] { const command = `cd '${linuxCwd}' && export PATH="$HOME/.local/bin:$PATH" && ${buildWslInteractiveLoginShellCommand()}` @@ -66,7 +64,7 @@ describe('resolveWindowsShellLaunchArgs', () => { undefined, CODEX_LAUNCH_PREFLIGHT ) - expect(result.shellArgs).toEqual(['/K', `chcp 65001 > nul & ${CMD_CODEX_LAUNCH_PREFLIGHT}`]) + expect(result.shellArgs).toEqual(['/K', 'chcp 65001 > nul']) expect(result.startupCommandDeliveredInShellArgs).toBeUndefined() expect(result.effectiveCwd).toBe('C:\\Users\\alice') expect(result.validationCwd).toBe('C:\\Users\\alice') @@ -81,10 +79,7 @@ describe('resolveWindowsShellLaunchArgs', () => { 'codex --no-alt-screen', CODEX_LAUNCH_PREFLIGHT ) - expect(result.shellArgs).toEqual([ - '/K', - `chcp 65001 > nul & ${CMD_CODEX_LAUNCH_PREFLIGHT} & codex --no-alt-screen` - ]) + expect(result.shellArgs).toEqual(['/K', 'chcp 65001 > nul & codex --no-alt-screen']) expect(result.startupCommandDeliveredInShellArgs).toBe(true) }) @@ -111,7 +106,7 @@ describe('resolveWindowsShellLaunchArgs', () => { `codex ${'x'.repeat(7000)}`, CODEX_LAUNCH_PREFLIGHT ) - expect(result.shellArgs).toEqual(['/K', `chcp 65001 > nul & ${CMD_CODEX_LAUNCH_PREFLIGHT}`]) + expect(result.shellArgs).toEqual(['/K', 'chcp 65001 > nul']) expect(result.startupCommandDeliveredInShellArgs).toBeUndefined() }) @@ -308,7 +303,7 @@ describe('resolveWindowsShellLaunchArgs', () => { expect(result.startupCommandDeliveredInShellArgs).toBeUndefined() }) - it('quotes a spaced preflight path through each shell environment', () => { + it('keeps the Codex preflight out of cmd.exe args and in the Git Bash wrapper', () => { const cmd = resolveWindowsShellLaunchArgs( 'cmd.exe', 'C:\\Users\\alice', @@ -326,10 +321,9 @@ describe('resolveWindowsShellLaunchArgs', () => { CODEX_LAUNCH_PREFLIGHT ) - expect(cmd.shellArgs[1]).toContain( - 'call %ORCA_CODEX_LAUNCH_PREFLIGHT_CMD_QUOTE%%ORCA_CODEX_LAUNCH_PREFLIGHT%%ORCA_CODEX_LAUNCH_PREFLIGHT_CMD_QUOTE%' - ) - expect(cmd.shellArgs[1]).not.toContain('"') + // Why: cmd.exe cannot defer the preflight behind a `codex` wrapper, so an + // inline call would block every pane on a CLI round-trip into the app. + expect(cmd.shellArgs[1]).not.toContain('prepare-codex') expect(cmd.shellArgs[1]).not.toContain(CODEX_LAUNCH_PREFLIGHT) const rcfilePath = getGitBashRcfilePath(gitBash.shellArgs[1]) expect(readFileSync(rcfilePath, 'utf8')).toContain('"${ORCA_CODEX_LAUNCH_PREFLIGHT}"') diff --git a/src/main/providers/windows-shell-args.ts b/src/main/providers/windows-shell-args.ts index 4f984559a63..dd2a365ff6f 100644 --- a/src/main/providers/windows-shell-args.ts +++ b/src/main/providers/windows-shell-args.ts @@ -18,8 +18,6 @@ const CMD_EXE_COMMAND_LINE_MAX_CHARS = 8191 const STARTUP_COMMAND_TEXT_MAX_CHARS = 6000 const POWERSHELL_ENCODED_COMMAND_ARG_MAX_CHARS = 28_000 const CMD_UTF8_SETUP_COMMAND = 'chcp 65001 > nul' -export const ORCA_CODEX_LAUNCH_PREFLIGHT_CMD_QUOTE_ENV = 'ORCA_CODEX_LAUNCH_PREFLIGHT_CMD_QUOTE' -const CMD_CODEX_LAUNCH_PREFLIGHT = `if defined ORCA_CODEX_LAUNCH_PREFLIGHT call %${ORCA_CODEX_LAUNCH_PREFLIGHT_CMD_QUOTE_ENV}%%ORCA_CODEX_LAUNCH_PREFLIGHT%%${ORCA_CODEX_LAUNCH_PREFLIGHT_CMD_QUOTE_ENV}% agent hooks prepare-codex > nul 2>&1` // Why: Git for Windows' bash inherits the ConPTY console's OEM code page // (CP437), so a TUI that writes UTF-8 bytes straight to the console — agents // like Claude Code use WriteFile, not WriteConsoleW — renders as mojibake @@ -183,11 +181,14 @@ export function resolveWindowsShellLaunchArgs( const shellBasename = pathWin32.basename(shellPath).toLowerCase() const nativeCwd = normalizeWindowsTerminalCwd(cwd) + // Why no Codex preflight here: cmd.exe has no deferred-wrapper equivalent, so + // an inline `call orca agent hooks prepare-codex` would block the agent behind + // a CLI round-trip into the running app. buildPtyHostEnv does that prep + // in-process instead. if (shellBasename === 'cmd.exe') { const shellArgStartupCommand = getCmdShellArgStartupCommand(startupCommand) const startupCommands = [ CMD_UTF8_SETUP_COMMAND, - ...(codexLaunchPreflightCommand ? [CMD_CODEX_LAUNCH_PREFLIGHT] : []), ...(shellArgStartupCommand ? [shellArgStartupCommand] : []) ] return { diff --git a/src/main/providers/windows-shell-preflight-runtime.windows.test.ts b/src/main/providers/windows-shell-preflight-runtime.windows.test.ts index bae27db3a59..e1b1039fe99 100644 --- a/src/main/providers/windows-shell-preflight-runtime.windows.test.ts +++ b/src/main/providers/windows-shell-preflight-runtime.windows.test.ts @@ -12,10 +12,7 @@ import { join } from 'node:path' import * as pty from 'node-pty' import { afterEach, describe, expect, it } from 'vitest' import { resolveGitBashPath } from '../git-bash' -import { - ORCA_CODEX_LAUNCH_PREFLIGHT_CMD_QUOTE_ENV, - resolveWindowsShellLaunchArgs -} from './windows-shell-args' +import { resolveWindowsShellLaunchArgs } from './windows-shell-args' const describeWindows = process.platform === 'win32' ? describe : describe.skip const tempDirs: string[] = [] @@ -116,7 +113,7 @@ afterEach(() => { }) describeWindows('Windows Codex shell preflight runtime', () => { - it('runs a spaced executable through cmd.exe and continues after failure', async () => { + it('starts the cmd.exe startup command without blocking on the Codex preflight', async () => { const root = makeTempDir() const preflight = writeFailingPreflight(root) const preflightMarker = join(root, 'cmd-preflight-ran') @@ -130,6 +127,10 @@ describeWindows('Windows Codex shell preflight runtime', () => { preflight ) + // Why: an inline preflight is a CLI round-trip into the running app; a slow + // one delayed the agent past its launch budget, so it must not be in argv. + expect(resolved.shellArgs.join(' ')).not.toContain('prepare-codex') + await runPty({ shellPath: 'cmd.exe', shellArgs: resolved.shellArgs, @@ -137,12 +138,11 @@ describeWindows('Windows Codex shell preflight runtime', () => { env: { ...process.env, ORCA_CODEX_LAUNCH_PREFLIGHT: preflight, - [ORCA_CODEX_LAUNCH_PREFLIGHT_CMD_QUOTE_ENV]: '"', ORCA_PREFLIGHT_MARKER: preflightMarker } }) - expect(readFileSync(preflightMarker, 'utf8')).toBe('ran') + expect(existsSync(preflightMarker)).toBe(false) expect(readFileSync(startupMarker, 'utf8').trim()).toBe('launched') })