fix(codex): preserve session home on automatic resume

This commit is contained in:
Brennan Benson
2026-07-20 22:14:47 -07:00
parent 05c32c4757
commit 920cbdbe64
23 changed files with 514 additions and 26 deletions
@@ -0,0 +1,88 @@
import { describe, expect, it } from 'vitest'
import {
findTrustedCodexSessionResume,
resolveTrustedCodexSessionResumeHome
} from './codex-session-resume-home'
describe('resolveTrustedCodexSessionResumeHome', () => {
it('returns the trusted home containing a persisted rollout', () => {
expect(
resolveTrustedCodexSessionResumeHome({
transcriptPath: '/Users/example/.codex/sessions/2026/07/20/rollout-session.jsonl',
trustedCodexHomes: ['/managed/account/home', '/Users/example/.codex'],
fileExists: () => true
})
).toBe('/Users/example/.codex')
})
it('accepts Windows paths case-insensitively', () => {
expect(
resolveTrustedCodexSessionResumeHome({
transcriptPath: 'C:\\Users\\Example\\.codex\\sessions\\2026\\07\\20\\rollout-a.jsonl',
trustedCodexHomes: ['c:\\users\\example\\.codex'],
fileExists: () => true
})
).toBe('c:\\users\\example\\.codex')
})
it('rejects paths outside trusted homes or outside the rollout layout', () => {
const fileExists = (): boolean => true
expect(
resolveTrustedCodexSessionResumeHome({
transcriptPath: '/tmp/sessions/2026/07/20/rollout-a.jsonl',
trustedCodexHomes: ['/Users/example/.codex'],
fileExists
})
).toBeNull()
expect(
resolveTrustedCodexSessionResumeHome({
transcriptPath: '/Users/example/.codex/sessions/index.jsonl',
trustedCodexHomes: ['/Users/example/.codex'],
fileExists
})
).toBeNull()
})
it('rejects a trusted-looking path when the rollout no longer exists', () => {
expect(
resolveTrustedCodexSessionResumeHome({
transcriptPath: '/Users/example/.codex/sessions/2026/07/20/rollout-a.jsonl',
trustedCodexHomes: ['/Users/example/.codex'],
fileExists: () => false
})
).toBeNull()
})
it('finds older saved sessions by id when transcript provenance is absent', async () => {
const sessionId = '019f81b9-19a9-7651-a8d1-352d9420bd11'
const rolloutPath = `/managed/account/home/sessions/2026/07/20/rollout-2026-07-20T15-50-19-${sessionId}.jsonl`
const listSessionFiles = async function* (sessionsRoot: string): AsyncIterable<string> {
if (sessionsRoot === '/managed/account/home/sessions') {
yield rolloutPath
}
}
await expect(
findTrustedCodexSessionResume({
sessionId,
transcriptPath: undefined,
trustedCodexHomes: ['/Users/example/.codex', '/managed/account/home'],
listSessionFiles
})
).resolves.toEqual({ homePath: '/managed/account/home', transcriptPath: rolloutPath })
})
it('does not scan homes for an untrusted legacy session id shape', async () => {
const listSessionFiles = (): AsyncIterable<string> => {
throw new Error('must not scan')
}
await expect(
findTrustedCodexSessionResume({
sessionId: '../session',
transcriptPath: undefined,
trustedCodexHomes: ['/Users/example/.codex'],
listSessionFiles
})
).resolves.toBeNull()
})
})
@@ -0,0 +1,69 @@
import { existsSync } from 'node:fs'
import { join } from 'node:path'
import {
getRuntimePathBasename,
normalizeRuntimePathForComparison,
relativePathInsideRoot
} from '../../shared/cross-platform-path'
import { listCodexSessionJsonlFilesIncrementally } from './codex-session-file-listing'
const ROLLOUT_RELATIVE_PATH = /^\d{4}\/\d{2}\/\d{2}\/rollout-.+\.jsonl$/
export function resolveTrustedCodexSessionResumeHome(args: {
transcriptPath: string | undefined
trustedCodexHomes: readonly string[]
fileExists?: (filePath: string) => boolean
}): string | null {
const transcriptPath = args.transcriptPath?.trim()
if (!transcriptPath || !(args.fileExists ?? existsSync)(transcriptPath)) {
return null
}
for (const homePath of args.trustedCodexHomes) {
const relativePath = relativePathInsideRoot(join(homePath, 'sessions'), transcriptPath)
if (relativePath && ROLLOUT_RELATIVE_PATH.test(relativePath.replace(/\\/g, '/'))) {
return homePath
}
}
return null
}
export async function findTrustedCodexSessionResume(args: {
sessionId: string
transcriptPath: string | undefined
trustedCodexHomes: readonly string[]
fileExists?: (filePath: string) => boolean
listSessionFiles?: (sessionsRoot: string) => AsyncIterable<string>
}): Promise<{ homePath: string; transcriptPath: string } | null> {
const directHome = resolveTrustedCodexSessionResumeHome(args)
if (directHome && args.transcriptPath) {
return { homePath: directHome, transcriptPath: args.transcriptPath.trim() }
}
if (!/^[0-9a-f]{8}(?:-[0-9a-f]{4}){3}-[0-9a-f]{12}$/i.test(args.sessionId)) {
return null
}
const listSessionFiles =
args.listSessionFiles ??
((sessionsRoot: string) =>
listCodexSessionJsonlFilesIncrementally(sessionsRoot, { batchSize: 64, yieldMs: 0 }))
const expectedSuffix = `-${args.sessionId}.jsonl`.toLowerCase()
const seenHomes = new Set<string>()
for (const homePath of args.trustedCodexHomes) {
const comparisonHome = normalizeRuntimePathForComparison(homePath)
if (seenHomes.has(comparisonHome)) {
continue
}
seenHomes.add(comparisonHome)
const sessionsRoot = join(homePath, 'sessions')
if (!args.listSessionFiles && !existsSync(sessionsRoot)) {
continue
}
for await (const filePath of listSessionFiles(sessionsRoot)) {
if (getRuntimePathBasename(filePath).toLowerCase().endsWith(expectedSuffix)) {
return { homePath, transcriptPath: filePath }
}
}
}
return null
}
+73 -1
View File
@@ -147,6 +147,10 @@ import { startCodexSessionIndexHealInBackground } from './codex/codex-session-in
import { createCodexSessionMigrationScheduler } from './codex/codex-session-migration-scheduler'
import { prepareLegacySharedCodexSessionResume } from './codex/codex-legacy-session-resume'
import { resolveHostCodexSessionSourceHome } from './codex/codex-session-source-home'
import { findTrustedCodexSessionResume } from './codex/codex-session-resume-home'
import { 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'
import { ClaudeAccountService } from './claude-accounts/service'
import { ClaudeRuntimeAuthService } from './claude-accounts/runtime-auth-service'
@@ -784,6 +788,72 @@ function prepareCodexRuntimeHomeForLaunch(
return runtimeHomePath
}
async function prepareCodexSessionResumeForLaunch(args: {
providerSession: AgentProviderSessionMetadata
target: CodexAccountSelectionTarget
launchEnv?: NodeJS.ProcessEnv
workspacePath?: string
}): Promise<{ codexHomePath: string | null } | null> {
if (args.target.runtime === 'wsl' || !codexRuntimeHome || !store) {
return null
}
const configuredSourceHome = resolveHostCodexSessionSourceHome(store.getSettings())
const systemHomePath = getSystemCodexHomePath()
const trustedHomes = [
configuredSourceHome ?? systemHomePath,
...codexRuntimeHome.getHostCodexHomePathsForSessionDiscovery()
]
const sessionSource = await findTrustedCodexSessionResume({
sessionId: args.providerSession.id,
transcriptPath: args.providerSession.transcriptPath,
trustedCodexHomes: trustedHomes
})
if (!sessionSource) {
return null
}
const migrated = await prepareLegacySharedCodexSessionResume(
{
agent: 'codex',
executionHostId: 'local',
filePath: sessionSource.transcriptPath,
codexHome: sessionSource.homePath
},
{
isHostSystemDefaultRealHome: () => codexRuntimeHome!.isHostSystemDefaultRealHome(),
systemCodexHomePath: configuredSourceHome
}
)
const resumeHome = migrated.useRealCodexHome
? (configuredSourceHome ?? systemHomePath)
: sessionSource.homePath
if (args.workspacePath) {
try {
markCodexProjectTrusted(args.workspacePath)
} catch (error) {
console.warn('[codex-project-trust] failed to pre-mark resumed workspace:', error)
}
}
const isSystemHome =
normalizeRuntimePathForComparison(resumeHome) ===
normalizeRuntimePathForComparison(systemHomePath)
const hooksEnabled = isAgentStatusHooksEnabled(store.getSettings())
try {
if (isSystemHome) {
ensureRealHomeCodexHookState({ hooksEnabled, userDataPath: app.getPath('userData') })
} else if (hooksEnabled) {
codexHookService.install(resumeHome)
} else {
codexHookService.refreshRuntimeUserHooks(resumeHome)
}
} catch (error) {
// Why: hook repair is best-effort; session provenance must still win over the currently selected home.
console.warn('[codex-hook-service] failed to prepare automatic resume home:', error)
}
return { codexHomePath: isSystemHome ? null : resumeHome }
}
// Why: restore the window the close handler may have hidden to tray, or reopen it (dock-reactivation style) if fully torn down.
function showMainWindowFromTray(): void {
if (mainWindow && !mainWindow.isDestroyed()) {
@@ -1050,6 +1120,7 @@ function openMainWindow(): BrowserWindow {
prepareCodexRuntimeHomeForLaunch,
(target) => claudeRuntimeAuth!.prepareForClaudeLaunch(target),
{
prepareCodexSessionResume: prepareCodexSessionResumeForLaunch,
awaitLocalPtyStartup: () => localPtyStartupReady,
awaitLocalPtyProviderStartup: () => localPtyProviderStartupReady,
onBeforeRendererReload: ({ ignoreCache, webContentsId }) => {
@@ -2200,7 +2271,8 @@ app.whenReady().then(async () => {
prepareCodexRuntimeHomeForLaunch,
() => store!.getSettings(),
(target) => claudeRuntimeAuth!.prepareForClaudeLaunch(target),
store
store,
prepareCodexSessionResumeForLaunch
)
// Why: headless servers can't mount <webview> panes; use offscreen WebContents, gated on a real display so browser.headless.v1 stays honest.
if (headlessBrowserDisplayAvailable) {
+68
View File
@@ -931,6 +931,74 @@ describe('registerPtyHandlers', () => {
expect(env.ORCA_CODEX_HOME).toBe(TEST_CODEX_HOME)
})
it('resumes an automatic Codex session from its prepared originating home', async () => {
const selectedHome = vi.fn(() => '/managed/current/home')
const prepareResume = vi.fn(async () => ({ codexHomePath: '/managed/origin/home' }))
registerPtyHandlers(
mainWindow as never,
undefined,
selectedHome,
undefined,
undefined,
undefined,
{ prepareCodexSessionResume: prepareResume }
)
await handlers.get('pty:spawn')!(null, {
cols: 80,
rows: 24,
command: 'codex resume session-a',
launchAgent: 'codex',
resumeProviderSession: {
key: 'session_id',
id: 'session-a',
transcriptPath: '/managed/origin/home/sessions/2026/07/20/rollout-a.jsonl'
}
})
const env = spawnMock.mock.calls.at(-1)![2].env as Record<string, string>
expect(prepareResume).toHaveBeenCalledWith(
expect.objectContaining({
providerSession: expect.objectContaining({ id: 'session-a' }),
target: { runtime: 'host' }
})
)
expect(selectedHome).not.toHaveBeenCalled()
expect(env.CODEX_HOME).toBe('/managed/origin/home')
expect(env.ORCA_CODEX_HOME).toBe('/managed/origin/home')
})
it('strips the currently selected managed home when the resumed session originated in real home', async () => {
const selectedHome = vi.fn(() => '/managed/current/home')
registerPtyHandlers(
mainWindow as never,
undefined,
selectedHome,
undefined,
undefined,
undefined,
{ prepareCodexSessionResume: async () => ({ codexHomePath: null }) }
)
await handlers.get('pty:spawn')!(null, {
cols: 80,
rows: 24,
command: 'codex resume session-a',
env: { CODEX_HOME: '/managed/current/home', ORCA_CODEX_HOME: '/managed/current/home' },
launchAgent: 'codex',
resumeProviderSession: {
key: 'session_id',
id: 'session-a',
transcriptPath: '/Users/example/.codex/sessions/2026/07/20/rollout-a.jsonl'
}
})
const env = spawnMock.mock.calls.at(-1)![2].env as Record<string, string>
expect(selectedHome).not.toHaveBeenCalled()
expect(env.CODEX_HOME).toBeUndefined()
expect(env.ORCA_CODEX_HOME).toBeUndefined()
})
it('prepares Codex launch state for the workspace before spawning an interactive tab', async () => {
const workspacePath = '/repo/worktrees/new-feature'
const resolveHome = vi.fn(
+82 -15
View File
@@ -32,7 +32,11 @@ import {
} from '../../shared/pty-delivery-diagnostics'
import { recordCrashBreadcrumb } from '../crash-reporting/crash-breadcrumb-store'
import { isTuiAgent } from '../../shared/tui-agent-config'
import type { SleepingAgentLaunchConfig } from '../../shared/agent-session-resume'
import {
normalizeAgentProviderSession,
type AgentProviderSessionMetadata,
type SleepingAgentLaunchConfig
} from '../../shared/agent-session-resume'
import type { ProjectExecutionRuntimeResolution } from '../../shared/project-execution-runtime'
import {
isWslShellName,
@@ -624,6 +628,12 @@ export type GetSelectedCodexHomePath = (
launchEnv?: NodeJS.ProcessEnv,
launchContext?: { workspacePath?: string; launchAgent?: TuiAgent }
) => string | null
export type PrepareCodexSessionResume = (args: {
providerSession: AgentProviderSessionMetadata
target: CodexAccountSelectionTarget
launchEnv?: NodeJS.ProcessEnv
workspacePath?: string
}) => Promise<{ codexHomePath: string | null } | null>
type PrepareClaudeAuth = (
target?: ClaudeAccountSelectionTarget
) => Promise<ClaudeRuntimeAuthPreparation>
@@ -1381,6 +1391,7 @@ export function registerPtyHandlers(
prepareClaudeAuth?: PrepareClaudeAuth,
store?: Store,
options?: {
prepareCodexSessionResume?: PrepareCodexSessionResume
awaitLocalPtyStartup?: () => Promise<void>
awaitLocalPtyProviderStartup?: () => Promise<void>
// Why: returns true once for the crash-recovery reload so its did-finish-load skips the orphan sweep and keeps live PTYs (#5787).
@@ -1453,10 +1464,12 @@ export function registerPtyHandlers(
: { runtime: 'host' }
const selectedCodexHomePath = getCompatibleSelectedCodexHomePath(
codexSelectionTarget,
getSelectedCodexHomePath?.(codexSelectionTarget, baseEnv, {
workspacePath: ctx?.cwd,
launchAgent: ctx?.launchAgent
}) ?? null
ctx?.codexHomePathOverride
? ctx.codexHomePathOverride.value
: (getSelectedCodexHomePath?.(codexSelectionTarget, baseEnv, {
workspacePath: ctx?.cwd,
launchAgent: ctx?.launchAgent
}) ?? null)
)
const skipCodexHomeEnv = ctx?.isWsl === true && !selectedCodexHomePath
const env = buildPtyHostEnv(id, baseEnv, {
@@ -2699,6 +2712,29 @@ export function registerPtyHandlers(
}
}
const prepareCodexResumeHome = (args: {
connectionId?: string | null
launchAgent?: TuiAgent
providerSession?: AgentProviderSessionMetadata
target: CodexAccountSelectionTarget
launchEnv?: NodeJS.ProcessEnv
workspacePath?: string
}): Promise<{ codexHomePath: string | null } | null> | null => {
if (args.connectionId || args.launchAgent !== 'codex' || !options?.prepareCodexSessionResume) {
return null
}
const providerSession = normalizeAgentProviderSession(args.providerSession)
if (!providerSession) {
return null
}
return options.prepareCodexSessionResume({
providerSession,
target: args.target,
launchEnv: args.launchEnv,
workspacePath: args.workspacePath
})
}
// Why: route through getProviderForPty() so CLI commands work for remote PTYs too; localProvider would silently fail for them.
runtime?.setPtyController({
spawn: async (args) => {
@@ -2729,6 +2765,15 @@ export function registerPtyHandlers(
cwd,
terminalRuntimeOptions.terminalWindowsWslDistro ?? null
)
const codexResumePreparation = prepareCodexResumeHome({
connectionId: args.connectionId,
launchAgent: args.launchAgent,
providerSession: args.resumeProviderSession,
target: codexSelectionTarget,
launchEnv: args.env,
workspacePath: cwd
})
const codexResumeHome = codexResumePreparation ? await codexResumePreparation : null
const claudeAuth =
isClaudeLaunch && prepareClaudeAuth ? await prepareClaudeAuth(codexSelectionTarget) : null
if (isClaudeLaunch && isClaudeAuthSwitchInProgress()) {
@@ -2798,10 +2843,12 @@ export function registerPtyHandlers(
const selectedCodexHomePath = isDaemonHostSpawn
? getCompatibleSelectedCodexHomePath(
codexSelectionTarget,
getSelectedCodexHomePath?.(codexSelectionTarget, env, {
workspacePath: cwd,
launchAgent: isTuiAgent(args.launchAgent) ? args.launchAgent : undefined
}) ?? null
codexResumeHome
? codexResumeHome.codexHomePath
: (getSelectedCodexHomePath?.(codexSelectionTarget, env, {
workspacePath: cwd,
launchAgent: isTuiAgent(args.launchAgent) ? args.launchAgent : undefined
}) ?? null)
)
: null
const skipCodexHomeEnv =
@@ -2849,6 +2896,9 @@ export function registerPtyHandlers(
env,
...(isMintedSessionId ? { isNewSession: true } : {})
}
if (!isDaemonHostSpawn && codexResumeHome) {
spawnOptions.codexHomePathOverride = { value: codexResumeHome.codexHomePath }
}
const startupTerminalColorQueryReplyColors = getStartupTerminalColorQueryReplyColors(args)
if (startupTerminalColorQueryReplyColors) {
spawnOptions.startupIngress = {
@@ -3480,6 +3530,7 @@ export function registerPtyHandlers(
command?: string
commandDelivery?: 'renderer' | 'provider'
launchConfig?: SleepingAgentLaunchConfig
resumeProviderSession?: AgentProviderSessionMetadata
launchAgent?: TuiAgent
startupCommandDelivery?: StartupCommandDelivery
connectionId?: string | null
@@ -3700,13 +3751,24 @@ export function registerPtyHandlers(
cwd,
terminalRuntimeOptions.terminalWindowsWslDistro ?? null
)
const codexResumePreparation = prepareCodexResumeHome({
connectionId: args.connectionId,
launchAgent: args.launchAgent,
providerSession: args.resumeProviderSession,
target: codexSelectionTarget,
launchEnv: baseEnv,
workspacePath: cwd
})
const codexResumeHome = codexResumePreparation ? await codexResumePreparation : null
const selectedCodexHomePath = isDaemonHostSpawn
? getCompatibleSelectedCodexHomePath(
codexSelectionTarget,
getSelectedCodexHomePath?.(codexSelectionTarget, baseEnv, {
workspacePath: cwd,
launchAgent: isTuiAgent(args.launchAgent) ? args.launchAgent : undefined
}) ?? null
codexResumeHome
? codexResumeHome.codexHomePath
: (getSelectedCodexHomePath?.(codexSelectionTarget, baseEnv, {
workspacePath: cwd,
launchAgent: isTuiAgent(args.launchAgent) ? args.launchAgent : undefined
}) ?? null)
)
: null
const skipCodexHomeEnv =
@@ -3792,6 +3854,9 @@ export function registerPtyHandlers(
env: spawnEnv,
...(isMintedSessionId ? { isNewSession: true } : {})
}
if (!isDaemonHostSpawn && codexResumeHome) {
spawnOptions.codexHomePathOverride = { value: codexResumeHome.codexHomePath }
}
if (combinedEnvToDelete) {
spawnOptions.envToDelete = combinedEnvToDelete
}
@@ -4899,7 +4964,8 @@ export function registerHeadlessPtyRuntime(
getSelectedCodexHomePath?: GetSelectedCodexHomePath,
getSettings?: () => GlobalSettings,
prepareClaudeAuth?: PrepareClaudeAuth,
store?: Store
store?: Store,
prepareCodexSessionResume?: PrepareCodexSessionResume
): void {
// Why: headless `orca serve` has no renderer window but still needs the same PTY handlers so remote clients can drive terminals.
const headlessWindow = {
@@ -4916,7 +4982,8 @@ export function registerHeadlessPtyRuntime(
getSelectedCodexHomePath,
getSettings,
prepareClaudeAuth,
store
store,
{ prepareCodexSessionResume }
)
}
+2
View File
@@ -473,6 +473,7 @@ export type LocalPtyProviderOptions = {
ctx?: {
command?: string
launchAgent?: PtySpawnOptions['launchAgent']
codexHomePathOverride?: PtySpawnOptions['codexHomePathOverride']
cwd?: string
shellPath?: string
isWsl?: boolean
@@ -675,6 +676,7 @@ export class LocalPtyProvider implements IPtyProvider {
? this.opts.buildSpawnEnv(id, spawnEnv, {
command: args.command,
launchAgent: args.launchAgent,
codexHomePathOverride: args.codexHomePathOverride,
cwd,
shellPath,
isWsl: isWslShell,
+2
View File
@@ -56,6 +56,8 @@ export type PtySpawnOptions = {
cwd?: string
env?: Record<string, string>
envToDelete?: string[]
/** Main-validated home provenance for an automatic Codex session resume. */
codexHomePathOverride?: { value: string | null }
command?: string
commandDelivery?: 'renderer' | 'provider'
startupCommandDelivery?: StartupCommandDelivery
+10 -1
View File
@@ -152,7 +152,10 @@ import {
parseExecutionHostId,
type ExecutionHostId
} from '../../shared/execution-host'
import type { SleepingAgentLaunchConfig } from '../../shared/agent-session-resume'
import type {
AgentProviderSessionMetadata,
SleepingAgentLaunchConfig
} from '../../shared/agent-session-resume'
import type { RuntimeClientEvent } from '../../shared/runtime-client-events'
import { toRuntimeActivateWorktreeEvent } from '../../shared/runtime-client-events'
import {
@@ -1089,6 +1092,7 @@ type TerminalCreateOptions = {
env?: Record<string, string>
envToDelete?: string[]
launchConfig?: WorktreeStartupLaunch['launchConfig']
resumeProviderSession?: AgentProviderSessionMetadata
launchToken?: string
launchAgent?: TuiAgent
terminalColorQueryReplies?: TerminalOscColorQueryReplyColors
@@ -1283,6 +1287,7 @@ type RuntimePtyController = {
startupCommandDelivery?: WorktreeStartupLaunch['startupCommandDelivery']
env?: Record<string, string>
envToDelete?: string[]
resumeProviderSession?: AgentProviderSessionMetadata
telemetry?: WorktreeStartupLaunch['telemetry']
connectionId?: string | null
worktreeId?: string
@@ -19317,6 +19322,7 @@ export class OrcaRuntimeService {
launchOpts.envToDelete,
agentTeamsPlan?.envToDelete
),
resumeProviderSession: launchOpts.resumeProviderSession,
telemetry: launchOpts.telemetry,
connectionId: workspace.connectionId,
worktreeId: workspace.id,
@@ -19461,6 +19467,9 @@ export class OrcaRuntimeService {
cwd,
...(launchOpts.env ? { env: launchOpts.env } : {}),
...(launchOpts.launchConfig ? { launchConfig: launchOpts.launchConfig } : {}),
...(launchOpts.resumeProviderSession
? { resumeProviderSession: launchOpts.resumeProviderSession }
: {}),
...(launchOpts.launchToken ? { launchToken: launchOpts.launchToken } : {}),
...(launchOpts.launchAgent ? { launchAgent: launchOpts.launchAgent } : {}),
...(launchOpts.viewMode ? { viewMode: launchOpts.viewMode } : {}),
+10
View File
@@ -921,6 +921,13 @@ const TerminalCreateParams = z.object({
agentEnv: z.record(z.string(), z.string())
})
.optional(),
resumeProviderSession: z
.object({
key: z.enum(['session_id', 'conversation_id']),
id: z.string().min(1).max(512),
transcriptPath: z.string().min(1).max(32_768).optional()
})
.optional(),
launchToken: OptionalString,
launchAgent: z.string().refine(isTuiAgent).optional(),
terminalColorQueryReplies: z
@@ -1362,6 +1369,9 @@ export const TERMINAL_METHODS: RpcAnyMethod[] = [
env: params.env,
envToDelete: params.envToDelete,
...(params.launchConfig ? { launchConfig: params.launchConfig } : {}),
...(params.resumeProviderSession
? { resumeProviderSession: params.resumeProviderSession }
: {}),
...(params.launchToken ? { launchToken: params.launchToken } : {}),
...(params.launchAgent ? { launchAgent: params.launchAgent } : {}),
...(params.terminalColorQueryReplies
@@ -12,7 +12,12 @@ import type {
import { registerRepoHandlers } from '../ipc/repos'
import { registerWorktreeHandlers } from '../ipc/worktrees'
import { registerWorkspaceCleanupHandlers } from '../ipc/workspace-cleanup'
import { getLocalPtyProvider, registerPtyHandlers, type GetSelectedCodexHomePath } from '../ipc/pty'
import {
getLocalPtyProvider,
registerPtyHandlers,
type GetSelectedCodexHomePath,
type PrepareCodexSessionResume
} from '../ipc/pty'
import { registerDaemonManagementHandlers } from '../ipc/pty-management'
import { registerSshHandlers } from '../ipc/ssh'
import { registerRemoteWorkspaceHandlers } from '../ipc/remote-workspace'
@@ -70,6 +75,7 @@ export function attachMainWindowServices(
target?: ClaudeAccountSelectionTarget
) => Promise<ClaudeRuntimeAuthPreparation>,
options?: {
prepareCodexSessionResume?: PrepareCodexSessionResume
awaitLocalPtyStartup?: () => Promise<void>
awaitLocalPtyProviderStartup?: () => Promise<void>
onBeforeRendererReload?: (args: { webContentsId: number; ignoreCache: boolean }) => void
@@ -93,6 +99,7 @@ export function attachMainWindowServices(
prepareClaudeAuth,
store,
{
prepareCodexSessionResume: options?.prepareCodexSessionResume,
awaitLocalPtyStartup: options?.awaitLocalPtyStartup,
awaitLocalPtyProviderStartup: options?.awaitLocalPtyProviderStartup,
isRecoveryReloadInFlight: options?.isRecoveryReloadInFlight
+6 -1
View File
@@ -63,7 +63,10 @@ import type { TaskSourceContext } from '../shared/task-source-context'
import type { LinearIssueAttributeFilter } from '../shared/linear-issue-attribute-filter'
import type { ProjectExecutionRuntimeResolution } from '../shared/project-execution-runtime'
import type { StartupCommandDelivery } from '../shared/codex-startup-delivery'
import type { SleepingAgentLaunchConfig } from '../shared/agent-session-resume'
import type {
AgentProviderSessionMetadata,
SleepingAgentLaunchConfig
} from '../shared/agent-session-resume'
import type {
LocalhostWorktreeLabelResult,
LocalhostWorktreeLabelRoute
@@ -1290,6 +1293,7 @@ export type PreloadApi = {
envToDelete?: string[]
command?: string
launchConfig?: SleepingAgentLaunchConfig
resumeProviderSession?: AgentProviderSessionMetadata
launchToken?: string
launchAgent?: TuiAgent
startupCommandDelivery?: StartupCommandDelivery
@@ -2878,6 +2882,7 @@ export type PreloadApi = {
cwd?: string
env?: Record<string, string>
launchConfig?: SleepingAgentLaunchConfig
resumeProviderSession?: AgentProviderSessionMetadata
launchToken?: string
launchAgent?: TuiAgent
viewMode?: 'terminal' | 'chat'
+7 -1
View File
@@ -14,7 +14,10 @@ import type { AgentHookInstallStatus } from '../shared/agent-hook-types'
import type { TerminalPaneSplitSource } from '../shared/feature-education-telemetry'
import type { ProjectExecutionRuntimeResolution } from '../shared/project-execution-runtime'
import type { StartupCommandDelivery } from '../shared/codex-startup-delivery'
import type { SleepingAgentLaunchConfig } from '../shared/agent-session-resume'
import type {
AgentProviderSessionMetadata,
SleepingAgentLaunchConfig
} from '../shared/agent-session-resume'
import type { MobileRelayStatus } from '../shared/mobile-relay-status'
import type { MobilePairingConnectionMode } from '../shared/mobile-pairing-connection-mode'
import type {
@@ -797,6 +800,7 @@ const api = {
envToDelete?: string[]
command?: string
launchConfig?: SleepingAgentLaunchConfig
resumeProviderSession?: AgentProviderSessionMetadata
launchToken?: string
launchAgent?: TuiAgent
startupCommandDelivery?: StartupCommandDelivery
@@ -3487,6 +3491,7 @@ const api = {
cwd?: string
env?: Record<string, string>
launchConfig?: SleepingAgentLaunchConfig
resumeProviderSession?: AgentProviderSessionMetadata
launchToken?: string
launchAgent?: TuiAgent
viewMode?: 'terminal' | 'chat'
@@ -3510,6 +3515,7 @@ const api = {
cwd?: string
env?: Record<string, string>
launchConfig?: SleepingAgentLaunchConfig
resumeProviderSession?: AgentProviderSessionMetadata
launchToken?: string
launchAgent?: TuiAgent
viewMode?: 'terminal' | 'chat'
@@ -6,7 +6,10 @@ import type { EventProps } from '../../../../shared/telemetry-events'
import type { TerminalColorSchemeMode } from '../../../../shared/terminal-color-scheme-protocol'
import type { StartupCommandDelivery } from '../../../../shared/codex-startup-delivery'
import type { SetupSplitDirection, TuiAgent } from '../../../../shared/types'
import type { SleepingAgentLaunchConfig } from '../../../../shared/agent-session-resume'
import type {
AgentProviderSessionMetadata,
SleepingAgentLaunchConfig
} from '../../../../shared/agent-session-resume'
import type { TerminalKittyKeyboardModeTracker } from '../../../../shared/terminal-kitty-keyboard-mode-tracker'
export type PtyConnectionDeps = {
@@ -22,6 +25,7 @@ export type PtyConnectionDeps = {
env?: Record<string, string>
envToDelete?: string[]
launchConfig?: SleepingAgentLaunchConfig
resumeProviderSession?: AgentProviderSessionMetadata
launchToken?: string
launchAgent?: TuiAgent
draftPrompt?: string
@@ -7444,7 +7444,11 @@ describe('connectPanePty', () => {
updatedAt: 1,
stateStartedAt: 1,
stateHistory: [],
providerSession: { key: 'session_id', id: 'codex-session-1' }
providerSession: {
key: 'session_id',
id: 'codex-session-1',
transcriptPath: '/Users/example/.codex/sessions/2026/07/20/rollout-session.jsonl'
}
}
}
} as StoreState
@@ -7470,6 +7474,11 @@ describe('connectPanePty', () => {
expect.objectContaining({
sessionId: 'lost-pty',
command: "codex '--dangerously-bypass-approvals-and-sandbox' 'resume' 'codex-session-1'",
resumeProviderSession: {
key: 'session_id',
id: 'codex-session-1',
transcriptPath: '/Users/example/.codex/sessions/2026/07/20/rollout-session.jsonl'
},
env: expect.objectContaining({
ORCA_PANE_KEY: paneKey,
ORCA_TAB_ID: 'tab-1',
@@ -235,6 +235,7 @@ import {
agentProviderSessionsEqual,
isResumableTuiAgent,
normalizeAgentProviderSession,
type AgentProviderSessionMetadata,
type ResumableTuiAgent,
type SleepingAgentSessionRecord
} from '../../../../shared/agent-session-resume'
@@ -481,6 +482,7 @@ type FreshSpawnOptions = {
type ColdRestoreAgentResumeStartup = PendingStartupCommand & {
agent: ResumableTuiAgent
resumeProviderSession: AgentProviderSessionMetadata
launchConfig: NonNullable<ReturnType<typeof buildAgentResumeStartupPlan>>['launchConfig']
launchToken: string
useLiveEntry: boolean
@@ -3348,6 +3350,9 @@ export function connectPanePty(
...(projectRuntime ? { projectRuntime } : {}),
...(terminalColorQueryReplies ? { terminalColorQueryReplies } : {}),
...(paneStartup?.launchConfig ? { launchConfig: paneStartup.launchConfig } : {}),
...(paneStartup?.resumeProviderSession
? { resumeProviderSession: paneStartup.resumeProviderSession }
: {}),
...(launchToken ? { launchToken } : {}),
...(paneStartup?.launchAgent ? { launchAgent: paneStartup.launchAgent } : {}),
...(paneStartup?.telemetry ? { telemetry: paneStartup.telemetry } : {}),
@@ -4487,6 +4492,7 @@ export function connectPanePty(
ORCA_AGENT_LAUNCH_TOKEN: coldRestoreLaunchToken
},
launchConfig: startupPlan.launchConfig,
resumeProviderSession: providerSession,
launchToken: coldRestoreLaunchToken,
useLiveEntry: Boolean(useLiveEntry),
hasSleepingRecord: Boolean(sleepingRecord),
@@ -4700,6 +4706,9 @@ export function connectPanePty(
? { env: mergeStartupEnvWithPaneIdentity(startupOverride.env) }
: {}),
...(coldRestoreOverride ? { launchConfig: coldRestoreOverride.launchConfig } : {}),
...(coldRestoreOverride
? { resumeProviderSession: coldRestoreOverride.resumeProviderSession }
: {}),
...(coldRestoreOverride ? { launchToken: coldRestoreOverride.launchToken } : {}),
...(coldRestoreOverride ? { launchAgent: coldRestoreOverride.agent } : {}),
...(shouldDeclareHiddenAtSpawn() ? { initiallyHidden: true } : {}),
@@ -7552,6 +7561,9 @@ export function connectPanePty(
...(coldRestoreStartup?.launchConfig
? { launchConfig: coldRestoreStartup.launchConfig }
: {}),
...(coldRestoreStartup?.resumeProviderSession
? { resumeProviderSession: coldRestoreStartup.resumeProviderSession }
: {}),
...(coldRestoreStartup?.launchToken
? { launchToken: coldRestoreStartup.launchToken }
: {}),
@@ -7763,6 +7775,9 @@ export function connectPanePty(
...(coldRestoreStartup?.launchConfig
? { launchConfig: coldRestoreStartup.launchConfig }
: {}),
...(coldRestoreStartup?.resumeProviderSession
? { resumeProviderSession: coldRestoreStartup.resumeProviderSession }
: {}),
...(coldRestoreStartup?.launchToken ? { launchToken: coldRestoreStartup.launchToken } : {}),
...(coldRestoreStartup?.agent ? { launchAgent: coldRestoreStartup.agent } : {}),
...(shouldDeclareHiddenAtSpawn() ? { initiallyHidden: true } : {}),
@@ -1,5 +1,8 @@
import type { ParsedAgentStatusPayload } from '../../../../shared/agent-status-types'
import type { SleepingAgentLaunchConfig } from '../../../../shared/agent-session-resume'
import type {
AgentProviderSessionMetadata,
SleepingAgentLaunchConfig
} from '../../../../shared/agent-session-resume'
import type { StartupCommandDelivery } from '../../../../shared/codex-startup-delivery'
import type { ProjectExecutionRuntimeResolution } from '../../../../shared/project-execution-runtime'
import type { EventProps } from '../../../../shared/telemetry-events'
@@ -86,6 +89,7 @@ export type PtyTransport = {
env?: Record<string, string>
envToDelete?: string[]
launchConfig?: SleepingAgentLaunchConfig
resumeProviderSession?: AgentProviderSessionMetadata
launchToken?: string
launchAgent?: TuiAgent
startupCommandDelivery?: StartupCommandDelivery
@@ -144,6 +148,7 @@ export type IpcPtyTransportOptions = {
envToDelete?: string[]
command?: string
launchConfig?: SleepingAgentLaunchConfig
resumeProviderSession?: AgentProviderSessionMetadata
launchToken?: string
launchAgent?: TuiAgent
startupCommandDelivery?: StartupCommandDelivery
@@ -104,6 +104,21 @@ describe('createIpcPtyTransport', () => {
)
})
it('forwards automatic resume provenance to the PTY spawn', async () => {
const { createIpcPtyTransport } = await import('./pty-transport')
const spawn = window.api.pty.spawn as unknown as ReturnType<typeof vi.fn>
const resumeProviderSession = {
key: 'session_id' as const,
id: 'session-a',
transcriptPath: '/Users/example/.codex/sessions/2026/07/20/rollout-a.jsonl'
}
const transport = createIpcPtyTransport({ resumeProviderSession })
await transport.connect({ url: '', callbacks: {} })
expect(spawn).toHaveBeenCalledWith(expect.objectContaining({ resumeProviderSession }))
})
it('leaves the transport silently unbound after a failed connect — sendInput drops with no write IPC (frozen-terminal repro)', async () => {
const { createIpcPtyTransport } = await import('./pty-transport')
const spawn = window.api.pty.spawn as unknown as ReturnType<typeof vi.fn>
@@ -468,6 +468,7 @@ export function createIpcPtyTransport(opts: IpcPtyTransportOptions = {}): PtyTra
envToDelete,
command,
launchConfig,
resumeProviderSession,
launchToken,
launchAgent,
startupCommandDelivery,
@@ -686,6 +687,11 @@ export function createIpcPtyTransport(opts: IpcPtyTransportOptions = {}): PtyTra
...((options.launchConfig ?? launchConfig)
? { launchConfig: options.launchConfig ?? launchConfig }
: {}),
...((options.resumeProviderSession ?? resumeProviderSession)
? {
resumeProviderSession: options.resumeProviderSession ?? resumeProviderSession
}
: {}),
...((options.launchToken ?? launchToken)
? { launchToken: options.launchToken ?? launchToken }
: {}),
@@ -1182,6 +1182,11 @@ describe('createRemoteRuntimePtyTransport', () => {
},
launchToken: 'fresh-token',
launchAgent: 'codex',
resumeProviderSession: {
key: 'session_id',
id: 'session-1',
transcriptPath: '/home/example/.codex/sessions/2026/07/20/rollout-a.jsonl'
},
callbacks: {}
})
@@ -1197,7 +1202,12 @@ describe('createRemoteRuntimePtyTransport', () => {
agentEnv: { CODEX_PROFILE: 'captured' }
},
launchToken: 'fresh-token',
launchAgent: 'codex'
launchAgent: 'codex',
resumeProviderSession: {
key: 'session_id',
id: 'session-1',
transcriptPath: '/home/example/.codex/sessions/2026/07/20/rollout-a.jsonl'
}
})
})
)
@@ -68,6 +68,7 @@ export function createRemoteRuntimePtyTransport(
env,
envToDelete,
launchConfig,
resumeProviderSession,
launchToken,
launchAgent,
terminalColorQueryReplies,
@@ -781,6 +782,7 @@ export function createRemoteRuntimePtyTransport(
const envToSend = options.env ?? env
const envToDeleteToSend = options.envToDelete ?? envToDelete
const launchConfigToSend = options.launchConfig ?? launchConfig
const resumeProviderSessionToSend = options.resumeProviderSession ?? resumeProviderSession
const launchTokenToSend = options.launchToken ?? launchToken
const launchAgentToSend = options.launchAgent ?? launchAgent
const created = await callRuntime<{ terminal: RuntimeTerminalCreate }>('terminal.create', {
@@ -792,6 +794,9 @@ export function createRemoteRuntimePtyTransport(
...(envToSend !== undefined ? { env: envToSend } : {}),
...(envToDeleteToSend !== undefined ? { envToDelete: envToDeleteToSend } : {}),
...(launchConfigToSend !== undefined ? { launchConfig: launchConfigToSend } : {}),
...(resumeProviderSessionToSend !== undefined
? { resumeProviderSession: resumeProviderSessionToSend }
: {}),
...(launchTokenToSend !== undefined ? { launchToken: launchTokenToSend } : {}),
...(launchAgentToSend !== undefined ? { launchAgent: launchAgentToSend } : {}),
...(terminalColorQueryReplies ? { terminalColorQueryReplies } : {}),
@@ -50,7 +50,10 @@ import type {
import type { TerminalPaneSplitSource } from '../../../../shared/feature-education-telemetry'
import type { EventProps } from '../../../../shared/telemetry-events'
import type { StartupCommandDelivery } from '../../../../shared/codex-startup-delivery'
import type { SleepingAgentLaunchConfig } from '../../../../shared/agent-session-resume'
import type {
AgentProviderSessionMetadata,
SleepingAgentLaunchConfig
} from '../../../../shared/agent-session-resume'
import { resolveTerminalFontWeights } from '../../../../shared/terminal-fonts'
import {
buildFontFamily,
@@ -209,7 +212,9 @@ type UseTerminalPaneLifecycleDeps = {
delivery?: 'terminal-paste'
startupCommandDelivery?: StartupCommandDelivery
env?: Record<string, string>
envToDelete?: string[]
launchConfig?: SleepingAgentLaunchConfig
resumeProviderSession?: AgentProviderSessionMetadata
launchToken?: string
launchAgent?: TuiAgent
draftPrompt?: string
+5
View File
@@ -1387,6 +1387,7 @@ export function useIpcEvents(): void {
cwd,
env,
launchConfig,
resumeProviderSession,
launchToken,
launchAgent,
viewMode,
@@ -1572,6 +1573,7 @@ export function useIpcEvents(): void {
command,
...(env ? { env } : {}),
...(launchConfig ? { launchConfig } : {}),
...(resumeProviderSession ? { resumeProviderSession } : {}),
...(launchToken ? { launchToken } : {}),
...(launchAgent ? { launchAgent } : {})
})
@@ -1723,6 +1725,9 @@ export function useIpcEvents(): void {
...(data.env ? { env: data.env } : {}),
...(data.envToDelete ? { envToDelete: data.envToDelete } : {}),
...(data.launchConfig ? { launchConfig: data.launchConfig } : {}),
...(data.resumeProviderSession
? { resumeProviderSession: data.resumeProviderSession }
: {}),
...(data.launchToken ? { launchToken: data.launchToken } : {}),
...(data.launchAgent ? { launchAgent: data.launchAgent } : {}),
...(data.startupCommandDelivery
+5 -1
View File
@@ -32,7 +32,10 @@ import type {
} from './mobile-markdown-document'
import type { RuntimeCapability } from './protocol-version'
import type { RemoteRuntimeSharedConnectionDiagnostics } from './remote-runtime-shared-control-types'
import type { SleepingAgentLaunchConfig } from './agent-session-resume'
import type {
AgentProviderSessionMetadata,
SleepingAgentLaunchConfig
} from './agent-session-resume'
import type { StartupCommandDelivery } from './codex-startup-delivery'
export type { RuntimeMarkdownReadTabResult, RuntimeMarkdownSaveTabResult }
@@ -506,6 +509,7 @@ type RuntimeTerminalCreateBaseRequestPayload = {
env?: Record<string, string>
envToDelete?: string[]
launchConfig?: SleepingAgentLaunchConfig
resumeProviderSession?: AgentProviderSessionMetadata
launchToken?: string
launchAgent?: TuiAgent
viewMode?: 'terminal' | 'chat'