mirror of
https://github.com/stablyai/orca.git
synced 2026-10-19 00:03:20 +00:00
Fix Codex automatic resume across account switches (#9696)
* fix(codex): preserve session home on automatic resume * fix(codex): keep automatic resume homes launch-authoritative * fix(codex): resume compressed rollout representations * fix(codex): keep resume migration best effort * fix(codex): reject stale automatic resume provenance * fix(codex): enforce real home on automatic resume * fix(codex): keep automatic resume home authoritative
This commit is contained in:
@@ -9,7 +9,7 @@ import {
|
||||
writeFileSync
|
||||
} from 'node:fs'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import { basename, join } from 'node:path'
|
||||
import { prepareLegacySharedCodexSessionResume } from './codex-legacy-session-resume'
|
||||
|
||||
describe('prepareLegacySharedCodexSessionResume', () => {
|
||||
@@ -47,6 +47,16 @@ describe('prepareLegacySharedCodexSessionResume', () => {
|
||||
expect(statSync(targetPath).ino).toBe(statSync(rolloutPath).ino)
|
||||
})
|
||||
|
||||
it('materializes a compressed legacy rollout without changing its representation', async () => {
|
||||
const compressedPath = `${rolloutPath}.zst`
|
||||
rmSync(rolloutPath)
|
||||
rolloutPath = compressedPath
|
||||
writeFileSync(rolloutPath, 'compressed-rollout', 'utf-8')
|
||||
|
||||
await expect(prepare()).resolves.toEqual({ useRealCodexHome: true })
|
||||
expect(readFileSync(targetRolloutPath(), 'utf-8')).toBe('compressed-rollout')
|
||||
})
|
||||
|
||||
it('coalesces concurrent resumes of the same legacy rollout', async () => {
|
||||
const [first, second] = await Promise.all([prepare(), prepare()])
|
||||
|
||||
@@ -129,14 +139,7 @@ describe('prepareLegacySharedCodexSessionResume', () => {
|
||||
}
|
||||
|
||||
function targetRolloutPath(): string {
|
||||
return join(
|
||||
systemHome,
|
||||
'sessions',
|
||||
'2026',
|
||||
'07',
|
||||
'20',
|
||||
'rollout-2026-07-20T12-00-00-session.jsonl'
|
||||
)
|
||||
return join(systemHome, 'sessions', '2026', '07', '20', basename(rolloutPath))
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -162,7 +162,7 @@ function isLegacyRolloutRelativePath(relativePath: string): boolean {
|
||||
/^\d{4}$/.test(parts[0] ?? '') &&
|
||||
/^\d{2}$/.test(parts[1] ?? '') &&
|
||||
/^\d{2}$/.test(parts[2] ?? '') &&
|
||||
/^rollout-.+\.jsonl$/.test(parts[3] ?? '')
|
||||
/^rollout-.+\.jsonl(?:\.zst)?$/.test(parts[3] ?? '')
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -58,6 +58,34 @@ export async function* listCodexSessionJsonlFilesIncrementally(
|
||||
rootPath: string,
|
||||
options: CodexSessionBridgeIncrementalOptions,
|
||||
onDirectoryError?: (directoryPath: string, error: unknown) => void | Promise<void>
|
||||
): AsyncGenerator<string> {
|
||||
yield* listCodexSessionFilesIncrementally(
|
||||
rootPath,
|
||||
options,
|
||||
(fileName) => fileName.endsWith('.jsonl'),
|
||||
onDirectoryError
|
||||
)
|
||||
}
|
||||
|
||||
/** Yields both physical representations that current Codex can resume. */
|
||||
export async function* listCodexSessionRolloutFilesIncrementally(
|
||||
rootPath: string,
|
||||
options: CodexSessionBridgeIncrementalOptions,
|
||||
onDirectoryError?: (directoryPath: string, error: unknown) => void | Promise<void>
|
||||
): AsyncGenerator<string> {
|
||||
yield* listCodexSessionFilesIncrementally(
|
||||
rootPath,
|
||||
options,
|
||||
(fileName) => fileName.endsWith('.jsonl') || fileName.endsWith('.jsonl.zst'),
|
||||
onDirectoryError
|
||||
)
|
||||
}
|
||||
|
||||
async function* listCodexSessionFilesIncrementally(
|
||||
rootPath: string,
|
||||
options: CodexSessionBridgeIncrementalOptions,
|
||||
isSessionFile: (fileName: string) => boolean,
|
||||
onDirectoryError?: (directoryPath: string, error: unknown) => void | Promise<void>
|
||||
): AsyncGenerator<string> {
|
||||
const batchSize = Math.max(1, options.batchSize ?? INCREMENTAL_BRIDGE_BATCH_SIZE)
|
||||
const yieldMs = Math.max(0, options.yieldMs ?? INCREMENTAL_BRIDGE_YIELD_MS)
|
||||
@@ -75,7 +103,7 @@ export async function* listCodexSessionJsonlFilesIncrementally(
|
||||
const childPath = join(currentDirectory, entry.name)
|
||||
if (entry.isDirectory()) {
|
||||
pendingDirectories.push(childPath)
|
||||
} else if (entry.isFile() && entry.name.endsWith('.jsonl')) {
|
||||
} else if (entry.isFile() && isSessionFile(entry.name)) {
|
||||
yield childPath
|
||||
}
|
||||
entriesSinceYield += 1
|
||||
|
||||
@@ -0,0 +1,226 @@
|
||||
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import {
|
||||
findTrustedCodexSessionResume,
|
||||
resolveTrustedCodexSessionResumeHome
|
||||
} from './codex-session-resume-home'
|
||||
|
||||
const tempRoots: string[] = []
|
||||
|
||||
afterEach(() => {
|
||||
for (const root of tempRoots.splice(0)) {
|
||||
rmSync(root, { recursive: true, force: true })
|
||||
}
|
||||
})
|
||||
|
||||
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'],
|
||||
fileIsRegular: () => 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'],
|
||||
fileIsRegular: () => true
|
||||
})
|
||||
).toBe('c:\\users\\example\\.codex')
|
||||
})
|
||||
|
||||
it('rejects paths outside trusted homes or outside the rollout layout', () => {
|
||||
const fileIsRegular = vi.fn((): boolean => true)
|
||||
expect(
|
||||
resolveTrustedCodexSessionResumeHome({
|
||||
transcriptPath: '/tmp/sessions/2026/07/20/rollout-a.jsonl',
|
||||
trustedCodexHomes: ['/Users/example/.codex'],
|
||||
fileIsRegular
|
||||
})
|
||||
).toBeNull()
|
||||
expect(
|
||||
resolveTrustedCodexSessionResumeHome({
|
||||
transcriptPath: '/Users/example/.codex/sessions/index.jsonl',
|
||||
trustedCodexHomes: ['/Users/example/.codex'],
|
||||
fileIsRegular
|
||||
})
|
||||
).toBeNull()
|
||||
expect(
|
||||
resolveTrustedCodexSessionResumeHome({
|
||||
transcriptPath:
|
||||
'/Users/example/.codex/sessions/2026/07/20/rollout-a/../../../../outside.jsonl',
|
||||
trustedCodexHomes: ['/Users/example/.codex'],
|
||||
fileIsRegular
|
||||
})
|
||||
).toBeNull()
|
||||
expect(fileIsRegular).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
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'],
|
||||
fileIsRegular: () => false
|
||||
})
|
||||
).toBeNull()
|
||||
})
|
||||
|
||||
it('requires the transcript provenance to name a regular rollout file', () => {
|
||||
const homePath = mkdtempSync(join(tmpdir(), 'orca-codex-resume-home-'))
|
||||
tempRoots.push(homePath)
|
||||
const rolloutDirectory = join(homePath, 'sessions', '2026', '07', '20', 'rollout-a.jsonl')
|
||||
mkdirSync(rolloutDirectory, { recursive: true })
|
||||
|
||||
expect(
|
||||
resolveTrustedCodexSessionResumeHome({
|
||||
transcriptPath: rolloutDirectory,
|
||||
trustedCodexHomes: [homePath]
|
||||
})
|
||||
).toBeNull()
|
||||
|
||||
const rolloutFile = join(homePath, 'sessions', '2026', '07', '20', 'rollout-b.jsonl')
|
||||
writeFileSync(rolloutFile, '{}\n')
|
||||
expect(
|
||||
resolveTrustedCodexSessionResumeHome({
|
||||
transcriptPath: rolloutFile,
|
||||
trustedCodexHomes: [homePath]
|
||||
})
|
||||
).toBe(homePath)
|
||||
})
|
||||
|
||||
it('follows Codex when a persisted plain rollout was compressed in place', async () => {
|
||||
const homePath = mkdtempSync(join(tmpdir(), 'orca-codex-resume-home-'))
|
||||
tempRoots.push(homePath)
|
||||
const plainPath = join(
|
||||
homePath,
|
||||
'sessions',
|
||||
'2026',
|
||||
'07',
|
||||
'20',
|
||||
'rollout-2026-07-20T12-00-00-session.jsonl'
|
||||
)
|
||||
const compressedPath = `${plainPath}.zst`
|
||||
mkdirSync(join(plainPath, '..'), { recursive: true })
|
||||
writeFileSync(compressedPath, 'compressed-rollout')
|
||||
|
||||
await expect(
|
||||
findTrustedCodexSessionResume({
|
||||
sessionId: 'session-a',
|
||||
transcriptPath: plainPath,
|
||||
trustedCodexHomes: [homePath]
|
||||
})
|
||||
).resolves.toEqual({ homePath, transcriptPath: compressedPath })
|
||||
|
||||
writeFileSync(plainPath, 'active-rollout')
|
||||
await expect(
|
||||
findTrustedCodexSessionResume({
|
||||
sessionId: 'session-a',
|
||||
transcriptPath: compressedPath,
|
||||
trustedCodexHomes: [homePath]
|
||||
})
|
||||
).resolves.toEqual({ homePath, transcriptPath: plainPath })
|
||||
})
|
||||
|
||||
it('finds compressed rollouts for legacy records without transcript provenance', async () => {
|
||||
const homePath = mkdtempSync(join(tmpdir(), 'orca-codex-resume-home-'))
|
||||
tempRoots.push(homePath)
|
||||
const sessionId = '019f81b9-19a9-7651-a8d1-352d9420bd11'
|
||||
const compressedPath = join(
|
||||
homePath,
|
||||
'sessions',
|
||||
'2026',
|
||||
'07',
|
||||
'20',
|
||||
`rollout-2026-07-20T12-00-00-${sessionId}.jsonl.zst`
|
||||
)
|
||||
mkdirSync(join(compressedPath, '..'), { recursive: true })
|
||||
writeFileSync(compressedPath, 'compressed-rollout')
|
||||
|
||||
await expect(
|
||||
findTrustedCodexSessionResume({
|
||||
sessionId,
|
||||
transcriptPath: undefined,
|
||||
trustedCodexHomes: [homePath]
|
||||
})
|
||||
).resolves.toEqual({ homePath, transcriptPath: compressedPath })
|
||||
})
|
||||
|
||||
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 `/managed/account/home/sessions/misplaced-${sessionId}.jsonl`
|
||||
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 session trees when exact transcript provenance is valid', async () => {
|
||||
const transcriptPath =
|
||||
'/managed/account/home/sessions/2026/07/20/rollout-2026-07-20-session.jsonl'
|
||||
const listSessionFiles = vi.fn((): AsyncIterable<string> => {
|
||||
throw new Error('must not scan')
|
||||
})
|
||||
|
||||
await expect(
|
||||
findTrustedCodexSessionResume({
|
||||
sessionId: 'session-a',
|
||||
transcriptPath,
|
||||
trustedCodexHomes: ['/managed/account/home'],
|
||||
fileIsRegular: () => true,
|
||||
listSessionFiles
|
||||
})
|
||||
).resolves.toEqual({ homePath: '/managed/account/home', transcriptPath })
|
||||
expect(listSessionFiles).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('does not replace rejected transcript provenance with a same-id rollout from another home', async () => {
|
||||
const sessionId = '019f81b9-19a9-7651-a8d1-352d9420bd11'
|
||||
const listSessionFiles = vi.fn((): AsyncIterable<string> => {
|
||||
throw new Error('must not scan')
|
||||
})
|
||||
|
||||
await expect(
|
||||
findTrustedCodexSessionResume({
|
||||
sessionId,
|
||||
transcriptPath: `/managed/origin/home/sessions/2026/07/20/rollout-${sessionId}.jsonl`,
|
||||
trustedCodexHomes: ['/managed/origin/home', '/managed/other/home'],
|
||||
fileIsRegular: () => false,
|
||||
listSessionFiles
|
||||
})
|
||||
).resolves.toBeNull()
|
||||
expect(listSessionFiles).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
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,135 @@
|
||||
import { existsSync, lstatSync } from 'node:fs'
|
||||
import { join } from 'node:path'
|
||||
import {
|
||||
getRuntimePathBasename,
|
||||
normalizeRuntimePathForComparison,
|
||||
relativePathInsideRoot
|
||||
} from '../../shared/cross-platform-path'
|
||||
import { listCodexSessionRolloutFilesIncrementally } from './codex-session-file-listing'
|
||||
|
||||
// Why: only Codex's dated rollout layout may establish account-home provenance; nested/misplaced JSONL must not select credentials.
|
||||
const ROLLOUT_RELATIVE_PATH = /^\d{4}\/\d{2}\/\d{2}\/rollout-[^/]+\.jsonl(?:\.zst)?$/
|
||||
|
||||
function isCodexRolloutInsideSessionsRoot(sessionsRoot: string, filePath: string): boolean {
|
||||
const relativePath = relativePathInsideRoot(sessionsRoot, filePath)
|
||||
return Boolean(relativePath && ROLLOUT_RELATIVE_PATH.test(relativePath.replace(/\\/g, '/')))
|
||||
}
|
||||
|
||||
function isRegularFile(filePath: string): boolean {
|
||||
try {
|
||||
return lstatSync(filePath).isFile()
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
function resolveExistingRolloutPath(
|
||||
transcriptPath: string,
|
||||
fileIsRegular: (filePath: string) => boolean
|
||||
): string | null {
|
||||
const plainPath = transcriptPath.endsWith('.jsonl.zst')
|
||||
? transcriptPath.slice(0, -'.zst'.length)
|
||||
: transcriptPath.endsWith('.jsonl')
|
||||
? transcriptPath
|
||||
: null
|
||||
if (!plainPath) {
|
||||
return fileIsRegular(transcriptPath) ? transcriptPath : null
|
||||
}
|
||||
if (fileIsRegular(plainPath)) {
|
||||
return plainPath
|
||||
}
|
||||
const compressedPath = `${plainPath}.zst`
|
||||
return fileIsRegular(compressedPath) ? compressedPath : null
|
||||
}
|
||||
|
||||
function resolveTrustedCodexSessionResume(args: {
|
||||
transcriptPath: string | undefined
|
||||
trustedCodexHomes: readonly string[]
|
||||
fileIsRegular?: (filePath: string) => boolean
|
||||
}): { homePath: string; transcriptPath: string } | null {
|
||||
const persistedPath = args.transcriptPath?.trim()
|
||||
if (!persistedPath) {
|
||||
return null
|
||||
}
|
||||
|
||||
for (const homePath of args.trustedCodexHomes) {
|
||||
const sessionsRoot = join(homePath, 'sessions')
|
||||
if (!isCodexRolloutInsideSessionsRoot(sessionsRoot, persistedPath)) {
|
||||
continue
|
||||
}
|
||||
const transcriptPath = resolveExistingRolloutPath(
|
||||
persistedPath,
|
||||
args.fileIsRegular ?? isRegularFile
|
||||
)
|
||||
if (transcriptPath) {
|
||||
return { homePath, transcriptPath }
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
export function resolveTrustedCodexSessionResumeHome(args: {
|
||||
transcriptPath: string | undefined
|
||||
trustedCodexHomes: readonly string[]
|
||||
fileIsRegular?: (filePath: string) => boolean
|
||||
}): string | null {
|
||||
return resolveTrustedCodexSessionResume(args)?.homePath ?? null
|
||||
}
|
||||
|
||||
export async function findTrustedCodexSessionResume(args: {
|
||||
sessionId: string
|
||||
transcriptPath: string | undefined
|
||||
trustedCodexHomes: readonly string[]
|
||||
fileIsRegular?: (filePath: string) => boolean
|
||||
listSessionFiles?: (sessionsRoot: string) => AsyncIterable<string>
|
||||
}): Promise<{ homePath: string; transcriptPath: string } | null> {
|
||||
const directSession = resolveTrustedCodexSessionResume(args)
|
||||
if (directSession) {
|
||||
return directSession
|
||||
}
|
||||
if (args.transcriptPath?.trim()) {
|
||||
// Why: stale/rejected provenance must not select a same-id rollout under different account credentials; scanning is legacy-only.
|
||||
return null
|
||||
}
|
||||
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) =>
|
||||
listCodexSessionRolloutFilesIncrementally(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)) {
|
||||
const plainFilePath = filePath.endsWith('.jsonl.zst')
|
||||
? filePath.slice(0, -'.zst'.length)
|
||||
: filePath
|
||||
// Why: the directory entry already proves the compressed file exists; only probe its preferred plain sibling.
|
||||
const preferredFilePath =
|
||||
plainFilePath !== filePath && (args.fileIsRegular ?? isRegularFile)(plainFilePath)
|
||||
? plainFilePath
|
||||
: filePath
|
||||
const plainFileName = getRuntimePathBasename(preferredFilePath)
|
||||
.toLowerCase()
|
||||
.replace(/\.zst$/, '')
|
||||
if (
|
||||
isCodexRolloutInsideSessionsRoot(sessionsRoot, preferredFilePath) &&
|
||||
plainFileName.endsWith(expectedSuffix)
|
||||
) {
|
||||
return { homePath, transcriptPath: preferredFilePath }
|
||||
}
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
+85
-1
@@ -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,84 @@ 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 systemHomePath = getSystemCodexHomePath()
|
||||
// Why: codexSessionSourceHome is import-only; treating it as CODEX_HOME would mutate history sources and bypass account auth.
|
||||
const trustedHomes = [
|
||||
systemHomePath,
|
||||
...codexRuntimeHome.getHostCodexHomePathsForSessionDiscovery()
|
||||
]
|
||||
const sessionSource = await findTrustedCodexSessionResume({
|
||||
sessionId: args.providerSession.id,
|
||||
transcriptPath: args.providerSession.transcriptPath,
|
||||
trustedCodexHomes: trustedHomes
|
||||
})
|
||||
if (!sessionSource) {
|
||||
if (args.providerSession.transcriptPath) {
|
||||
throw new Error(
|
||||
'Orca could not verify the originating Codex session file, so automatic resume was stopped to avoid using a different account.'
|
||||
)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
let migrated = { useRealCodexHome: false }
|
||||
try {
|
||||
migrated = await prepareLegacySharedCodexSessionResume(
|
||||
{
|
||||
agent: 'codex',
|
||||
executionHostId: 'local',
|
||||
filePath: sessionSource.transcriptPath,
|
||||
codexHome: sessionSource.homePath
|
||||
},
|
||||
{
|
||||
isHostSystemDefaultRealHome: () => codexRuntimeHome!.isHostSystemDefaultRealHome(),
|
||||
systemCodexHomePath: systemHomePath
|
||||
}
|
||||
)
|
||||
} catch (error) {
|
||||
// Why: migration is a compatibility repair; its failure must not prevent the PTY from resuming from its trusted origin home.
|
||||
console.warn(
|
||||
'[codex-session-resume] Legacy rollout migration failed; using origin home:',
|
||||
error
|
||||
)
|
||||
}
|
||||
const resumeHome = migrated.useRealCodexHome ? 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: 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 +1132,7 @@ function openMainWindow(): BrowserWindow {
|
||||
prepareCodexRuntimeHomeForLaunch,
|
||||
(target) => claudeRuntimeAuth!.prepareForClaudeLaunch(target),
|
||||
{
|
||||
prepareCodexSessionResume: prepareCodexSessionResumeForLaunch,
|
||||
awaitLocalPtyStartup: () => localPtyStartupReady,
|
||||
awaitLocalPtyProviderStartup: () => localPtyProviderStartupReady,
|
||||
onBeforeRendererReload: ({ ignoreCache, webContentsId }) => {
|
||||
@@ -2195,7 +2278,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) {
|
||||
|
||||
@@ -931,6 +931,111 @@ 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('overrides an unmarked custom home when the resumed session originated in real home', async () => {
|
||||
const selectedHome = vi.fn(() => '/managed/current/home')
|
||||
const systemHome = '/Users/example/.codex'
|
||||
registerPtyHandlers(
|
||||
mainWindow as never,
|
||||
undefined,
|
||||
selectedHome,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
{ prepareCodexSessionResume: async () => ({ codexHomePath: systemHome }) }
|
||||
)
|
||||
|
||||
await handlers.get('pty:spawn')!(null, {
|
||||
cols: 80,
|
||||
rows: 24,
|
||||
command: 'codex resume session-a',
|
||||
env: { CODEX_HOME: '/custom/codex', REMOVE_ME: 'stale' },
|
||||
envToDelete: ['CODEX_HOME', 'ORCA_CODEX_HOME', 'REMOVE_ME'],
|
||||
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).toBe(systemHome)
|
||||
expect(env.ORCA_CODEX_HOME).toBe(systemHome)
|
||||
expect(env.REMOVE_ME).toBeUndefined()
|
||||
})
|
||||
|
||||
it('does not fall back to the selected account when automatic resume provenance is rejected', async () => {
|
||||
const selectedHome = vi.fn(() => '/managed/current/home')
|
||||
registerPtyHandlers(
|
||||
mainWindow as never,
|
||||
undefined,
|
||||
selectedHome,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
{
|
||||
prepareCodexSessionResume: async () => {
|
||||
throw new Error('origin unavailable')
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
await expect(
|
||||
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'
|
||||
}
|
||||
})
|
||||
).rejects.toThrow('origin unavailable')
|
||||
|
||||
expect(selectedHome).not.toHaveBeenCalled()
|
||||
expect(spawnMock).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('prepares Codex launch state for the workspace before spawning an interactive tab', async () => {
|
||||
const workspacePath = '/repo/worktrees/new-feature'
|
||||
const resolveHome = vi.fn(
|
||||
@@ -1729,6 +1834,103 @@ describe('registerPtyHandlers', () => {
|
||||
expect(env.ORCA_CODEX_HOME).toBe(TEST_CODEX_HOME)
|
||||
})
|
||||
|
||||
it('overrides an unmarked custom home for an authoritative daemon resume', async () => {
|
||||
const daemonSpawn = setupDaemonAdapter()
|
||||
const selectedHome = vi.fn(() => '/managed/current/home')
|
||||
const systemHome = '/Users/example/.codex'
|
||||
handlers.clear()
|
||||
registerPtyHandlers(
|
||||
mainWindow as never,
|
||||
undefined,
|
||||
selectedHome,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
{ prepareCodexSessionResume: async () => ({ codexHomePath: systemHome }) }
|
||||
)
|
||||
|
||||
await handlers.get('pty:spawn')!(null, {
|
||||
cols: 80,
|
||||
rows: 24,
|
||||
command: 'codex resume session-a',
|
||||
env: { CODEX_HOME: '/custom/codex', REMOVE_ME: 'stale' },
|
||||
envToDelete: ['CODEX_HOME', 'ORCA_CODEX_HOME', 'REMOVE_ME'],
|
||||
launchAgent: 'codex',
|
||||
resumeProviderSession: {
|
||||
key: 'session_id',
|
||||
id: 'session-a',
|
||||
transcriptPath: `${systemHome}/sessions/2026/07/20/rollout-a.jsonl`
|
||||
}
|
||||
})
|
||||
|
||||
const env = daemonSpawn.mock.calls.at(-1)![0].env
|
||||
expect(selectedHome).not.toHaveBeenCalled()
|
||||
expect(env.CODEX_HOME).toBe(systemHome)
|
||||
expect(env.ORCA_CODEX_HOME).toBe(systemHome)
|
||||
expect(env.REMOVE_ME).toBeUndefined()
|
||||
})
|
||||
|
||||
it('keeps the authoritative home for runtime-created daemon resumes', async () => {
|
||||
type RuntimeSpawnController = {
|
||||
spawn(args: {
|
||||
cols: number
|
||||
rows: number
|
||||
command: string
|
||||
env: Record<string, string>
|
||||
envToDelete: string[]
|
||||
launchAgent: 'codex'
|
||||
resumeProviderSession: {
|
||||
key: 'session_id'
|
||||
id: string
|
||||
transcriptPath: string
|
||||
}
|
||||
}): Promise<{ id: string }>
|
||||
}
|
||||
const daemonSpawn = setupDaemonAdapter()
|
||||
const runtime = {
|
||||
setPtyController: vi.fn(),
|
||||
registerPty: vi.fn(),
|
||||
noteTerminalSpawnCommand: vi.fn(),
|
||||
onPtySpawned: vi.fn(),
|
||||
onPtyExit: vi.fn(),
|
||||
onPtyData: vi.fn()
|
||||
}
|
||||
const systemHome = '/Users/example/.codex'
|
||||
handlers.clear()
|
||||
registerPtyHandlers(
|
||||
mainWindow as never,
|
||||
runtime as never,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
{ prepareCodexSessionResume: async () => ({ codexHomePath: systemHome }) }
|
||||
)
|
||||
const controller = runtime.setPtyController.mock.calls[0]?.[0] as RuntimeSpawnController
|
||||
|
||||
await controller.spawn({
|
||||
cols: 80,
|
||||
rows: 24,
|
||||
command: 'codex resume session-a',
|
||||
env: { CODEX_HOME: '/custom/codex', REMOVE_ME: 'stale' },
|
||||
envToDelete: ['CODEX_HOME', 'ORCA_CODEX_HOME', 'REMOVE_ME'],
|
||||
launchAgent: 'codex',
|
||||
resumeProviderSession: {
|
||||
key: 'session_id',
|
||||
id: 'session-a',
|
||||
transcriptPath: `${systemHome}/sessions/2026/07/20/rollout-a.jsonl`
|
||||
}
|
||||
})
|
||||
|
||||
const spawnOptions = daemonSpawn.mock.calls.at(-1)?.[0] as DaemonSpawnCall
|
||||
expect(spawnOptions.env.CODEX_HOME).toBe(systemHome)
|
||||
expect(spawnOptions.env.ORCA_CODEX_HOME).toBe(systemHome)
|
||||
expect(spawnOptions.env.REMOVE_ME).toBeUndefined()
|
||||
expect(spawnOptions.envToDelete ?? []).not.toContain('CODEX_HOME')
|
||||
expect(spawnOptions.envToDelete ?? []).not.toContain('ORCA_CODEX_HOME')
|
||||
expect(spawnOptions.envToDelete).toContain('REMOVE_ME')
|
||||
})
|
||||
|
||||
it('prepares Codex project trust before a daemon-backed interactive launch', async () => {
|
||||
const workspacePath = '/repo/worktrees/new-feature'
|
||||
const resolveHome = vi.fn(
|
||||
|
||||
+95
-16
@@ -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>
|
||||
@@ -747,6 +757,12 @@ function mergePtyEnvDeletions(
|
||||
return Array.from(new Set([...(existingKeys ?? []), ...additionalKeys]))
|
||||
}
|
||||
|
||||
function removeCodexHomeDeletionRequests(keys: string[] | undefined): string[] | undefined {
|
||||
// Why: resume provenance is launch-authoritative; late deletions must not fall back to the current account.
|
||||
const filtered = keys?.filter((key) => key !== 'CODEX_HOME' && key !== 'ORCA_CODEX_HOME')
|
||||
return filtered?.length ? filtered : undefined
|
||||
}
|
||||
|
||||
function getInheritedAgentHookEnvKeysToDelete(
|
||||
spawnEnv: Record<string, string> | undefined
|
||||
): string[] {
|
||||
@@ -1381,6 +1397,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 +1470,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 +2718,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 +2771,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 +2849,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 +2902,9 @@ export function registerPtyHandlers(
|
||||
env,
|
||||
...(isMintedSessionId ? { isNewSession: true } : {})
|
||||
}
|
||||
if (!isDaemonHostSpawn && codexResumeHome) {
|
||||
spawnOptions.codexHomePathOverride = { value: codexResumeHome.codexHomePath }
|
||||
}
|
||||
spawnOptions.envToDelete = mergePtyEnvDeletions(
|
||||
mergePtyEnvDeletions(authEnvToDelete, args.envToDelete ?? []),
|
||||
isDaemonHostSpawn ? getInheritedAgentHookEnvKeysToDelete(env) : []
|
||||
@@ -2865,6 +2921,9 @@ export function registerPtyHandlers(
|
||||
'ORCA_CODEX_HOME'
|
||||
])
|
||||
}
|
||||
if (codexResumeHome?.codexHomePath) {
|
||||
spawnOptions.envToDelete = removeCodexHomeDeletionRequests(spawnOptions.envToDelete)
|
||||
}
|
||||
deleteRequestedEnvKeys(env, spawnOptions.envToDelete)
|
||||
promoteAgentTeamsShimPath(env, requestedAgentTeamsPath)
|
||||
if (args.command !== undefined) {
|
||||
@@ -3466,6 +3525,7 @@ export function registerPtyHandlers(
|
||||
command?: string
|
||||
commandDelivery?: 'renderer' | 'provider'
|
||||
launchConfig?: SleepingAgentLaunchConfig
|
||||
resumeProviderSession?: AgentProviderSessionMetadata
|
||||
launchAgent?: TuiAgent
|
||||
startupCommandDelivery?: StartupCommandDelivery
|
||||
connectionId?: string | null
|
||||
@@ -3686,13 +3746,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 =
|
||||
@@ -3754,7 +3825,7 @@ export function registerPtyHandlers(
|
||||
const envToDelete = claudeAuth?.stripAuthEnv
|
||||
? [...CLAUDE_AUTH_ENV_VARS, 'ANTHROPIC_CUSTOM_HEADERS']
|
||||
: undefined
|
||||
const combinedEnvToDelete = mergePtyEnvDeletions(
|
||||
let combinedEnvToDelete = mergePtyEnvDeletions(
|
||||
mergePtyEnvDeletions(
|
||||
mergePtyEnvDeletions(
|
||||
mergePtyEnvDeletions(
|
||||
@@ -3769,6 +3840,9 @@ export function registerPtyHandlers(
|
||||
// main cannot safely decide ownership for a process it may not parent.
|
||||
stripInheritedOrcaCodexHome ? ['ORCA_CODEX_HOME'] : []
|
||||
)
|
||||
if (codexResumeHome?.codexHomePath) {
|
||||
combinedEnvToDelete = removeCodexHomeDeletionRequests(combinedEnvToDelete)
|
||||
}
|
||||
deleteRequestedEnvKeys(spawnEnv, combinedEnvToDelete)
|
||||
promoteAgentTeamsShimPath(spawnEnv, requestedAgentTeamsPath)
|
||||
const spawnOptions: PtySpawnOptions = {
|
||||
@@ -3778,6 +3852,9 @@ export function registerPtyHandlers(
|
||||
env: spawnEnv,
|
||||
...(isMintedSessionId ? { isNewSession: true } : {})
|
||||
}
|
||||
if (!isDaemonHostSpawn && codexResumeHome) {
|
||||
spawnOptions.codexHomePathOverride = { value: codexResumeHome.codexHomePath }
|
||||
}
|
||||
if (combinedEnvToDelete) {
|
||||
spawnOptions.envToDelete = combinedEnvToDelete
|
||||
}
|
||||
@@ -4888,7 +4965,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 = {
|
||||
@@ -4905,7 +4983,8 @@ export function registerHeadlessPtyRuntime(
|
||||
getSelectedCodexHomePath,
|
||||
getSettings,
|
||||
prepareClaudeAuth,
|
||||
store
|
||||
store,
|
||||
{ prepareCodexSessionResume }
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -471,6 +471,7 @@ export type LocalPtyProviderOptions = {
|
||||
ctx?: {
|
||||
command?: string
|
||||
launchAgent?: PtySpawnOptions['launchAgent']
|
||||
codexHomePathOverride?: PtySpawnOptions['codexHomePathOverride']
|
||||
cwd?: string
|
||||
shellPath?: string
|
||||
isWsl?: boolean
|
||||
@@ -673,6 +674,7 @@ export class LocalPtyProvider implements IPtyProvider {
|
||||
? this.opts.buildSpawnEnv(id, spawnEnv, {
|
||||
command: args.command,
|
||||
launchAgent: args.launchAgent,
|
||||
codexHomePathOverride: args.codexHomePathOverride,
|
||||
cwd,
|
||||
shellPath,
|
||||
isWsl: isWslShell,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -151,7 +151,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 type { SshConnectionState } from '../../shared/ssh-types'
|
||||
@@ -1075,6 +1078,7 @@ type TerminalCreateOptions = {
|
||||
env?: Record<string, string>
|
||||
envToDelete?: string[]
|
||||
launchConfig?: WorktreeStartupLaunch['launchConfig']
|
||||
resumeProviderSession?: AgentProviderSessionMetadata
|
||||
launchToken?: string
|
||||
launchAgent?: TuiAgent
|
||||
viewMode?: 'terminal' | 'chat'
|
||||
@@ -1268,6 +1272,7 @@ type RuntimePtyController = {
|
||||
startupCommandDelivery?: WorktreeStartupLaunch['startupCommandDelivery']
|
||||
env?: Record<string, string>
|
||||
envToDelete?: string[]
|
||||
resumeProviderSession?: AgentProviderSessionMetadata
|
||||
telemetry?: WorktreeStartupLaunch['telemetry']
|
||||
connectionId?: string | null
|
||||
worktreeId?: string
|
||||
@@ -19205,6 +19210,7 @@ export class OrcaRuntimeService {
|
||||
launchOpts.envToDelete,
|
||||
agentTeamsPlan?.envToDelete
|
||||
),
|
||||
resumeProviderSession: launchOpts.resumeProviderSession,
|
||||
telemetry: launchOpts.telemetry,
|
||||
connectionId: workspace.connectionId,
|
||||
worktreeId: workspace.id,
|
||||
@@ -19348,6 +19354,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 } : {}),
|
||||
|
||||
@@ -913,6 +913,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(),
|
||||
title: OptionalString,
|
||||
@@ -1348,6 +1355,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 } : {}),
|
||||
title: params.title,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -93,6 +93,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 }
|
||||
: {}),
|
||||
|
||||
@@ -1180,6 +1180,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: {}
|
||||
})
|
||||
|
||||
@@ -1195,7 +1200,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,
|
||||
worktreeId,
|
||||
@@ -778,6 +779,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', {
|
||||
@@ -789,6 +791,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 } : {}),
|
||||
tabId,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 }
|
||||
@@ -501,6 +504,7 @@ type RuntimeTerminalCreateBaseRequestPayload = {
|
||||
env?: Record<string, string>
|
||||
envToDelete?: string[]
|
||||
launchConfig?: SleepingAgentLaunchConfig
|
||||
resumeProviderSession?: AgentProviderSessionMetadata
|
||||
launchToken?: string
|
||||
launchAgent?: TuiAgent
|
||||
viewMode?: 'terminal' | 'chat'
|
||||
|
||||
Reference in New Issue
Block a user