diff --git a/src/main/codex/codex-pane-launch-home.test.ts b/src/main/codex/codex-pane-launch-home.test.ts new file mode 100644 index 00000000000..d4c1847eb32 --- /dev/null +++ b/src/main/codex/codex-pane-launch-home.test.ts @@ -0,0 +1,95 @@ +import { describe, expect, it } from 'vitest' +import type { CodexPaneAccountRecord } from './codex-pane-account-registry' +import { + describeCodexPaneLaunchHomeFailure, + resolveCodexPaneLaunchHome +} from './codex-pane-launch-home' + +const SYSTEM_HOME = '/Users/dev/.codex' +const SHARED_HOME = '/Users/dev/Library/Application Support/orca/codex-runtime-home/home' +const ACCOUNT_HOME = '/Users/dev/Library/Application Support/orca/codex-accounts/acct-1/home' + +const SETTINGS = { + codexManagedAccounts: [ + { + id: 'acct-1', + email: 'dev@example.com', + managedHomePath: ACCOUNT_HOME, + createdAt: 1, + updatedAt: 1, + lastAuthenticatedAt: 1 + } + ] +} + +function resolve(record: CodexPaneAccountRecord | null) { + return resolveCodexPaneLaunchHome({ + record, + settings: SETTINGS, + systemCodexHomePath: SYSTEM_HOME, + sharedRuntimeCodexHomePath: SHARED_HOME + }) +} + +describe('resolveCodexPaneLaunchHome', () => { + it('names the account home a managed pane launched under', () => { + expect( + resolve({ selectionKey: 'host', accountId: 'acct-1', homeRoute: 'account-home' }) + ).toEqual({ kind: 'attributed', path: ACCOUNT_HOME }) + }) + + it('routes real-home and shared-home panes to their own homes', () => { + expect(resolve({ selectionKey: 'host', accountId: null, homeRoute: 'real-home' })).toEqual({ + kind: 'attributed', + path: SYSTEM_HOME + }) + expect(resolve({ selectionKey: 'host', accountId: null, homeRoute: 'shared-home' })).toEqual({ + kind: 'attributed', + path: SHARED_HOME + }) + }) + + it('prefers a recorded pane-local override over the route it was classified as', () => { + expect( + resolve({ + selectionKey: 'host', + accountId: null, + homeRoute: 'real-home', + environmentHomeOverride: { codexHome: '/tmp/isolated/home' } + }) + ).toEqual({ kind: 'attributed', path: '/tmp/isolated/home' }) + expect( + resolve({ + selectionKey: 'host', + accountId: null, + homeRoute: 'shared-home', + shellStartupHomeOverride: { home: '/Users/dev', codexHome: '/tmp/shell/home' } + }) + ).toEqual({ kind: 'attributed', path: '/tmp/shell/home' }) + }) + + it('declines rather than guessing when the pane cannot be attributed', () => { + expect(resolve(null)).toEqual({ kind: 'unrecorded' }) + // A WSL lane can never answer for the local pane adoption asks about. + expect(resolve({ selectionKey: 'wsl:Ubuntu', accountId: null, homeRoute: 'wsl-home' })).toEqual( + { kind: 'unrecorded' } + ) + // Recorded before route provenance existed, so the launch home is unknown. + expect(resolve({ selectionKey: 'host', accountId: null })).toEqual({ kind: 'unnameable-home' }) + expect(resolve({ selectionKey: 'host', accountId: null, homeRoute: 'custom-home' })).toEqual({ + kind: 'unnameable-home' + }) + expect( + resolve({ selectionKey: 'host', accountId: 'acct-gone', homeRoute: 'account-home' }) + ).toEqual({ kind: 'unknown-account', accountId: 'acct-gone' }) + }) + + it('names the account in the failure the user is shown', () => { + expect( + describeCodexPaneLaunchHomeFailure({ kind: 'unknown-account', accountId: 'acct-gone' }) + ).toContain('acct-gone') + expect(describeCodexPaneLaunchHomeFailure({ kind: 'unrecorded' })).toContain( + 'which Codex account' + ) + }) +}) diff --git a/src/main/codex/codex-pane-launch-home.ts b/src/main/codex/codex-pane-launch-home.ts new file mode 100644 index 00000000000..56600a653a5 --- /dev/null +++ b/src/main/codex/codex-pane-launch-home.ts @@ -0,0 +1,78 @@ +import type { GlobalSettings } from '../../shared/global-settings-types' +import { getCodexSelectionTargetForAccount } from '../codex-accounts/runtime-selection' +import type { CodexPaneAccountRecord } from './codex-pane-account-registry' + +type CodexPaneLaunchHomeSettings = Pick + +export type CodexPaneLaunchHomeResolution = + | { kind: 'attributed'; path: string } + | { kind: 'unrecorded' } + | { kind: 'unknown-account'; accountId: string } + | { kind: 'unnameable-home' } + +/** + * Resolves the CODEX_HOME a live host pane's Codex is ALREADY running under. + * + * Why this is not the selected account's home: `CODEX_HOME` is baked into a + * shell's environment at spawn, and an automatic resume deliberately pins it to + * the home that OWNS the session rather than to the current selection. Only that + * home's `sessions` directory can hold the pane's rollout, so anything reading + * the live pane's transcript must ask which account it launched under — the + * record read here is the one `resolveCodexPaneLaunchAccount` wrote at spawn. + * + * Declines rather than guessing: probing the wrong account's home reports a + * broken session when the real cause is an account mismatch. + */ +export function resolveCodexPaneLaunchHome(args: { + record: CodexPaneAccountRecord | null + settings: CodexPaneLaunchHomeSettings + systemCodexHomePath: string + sharedRuntimeCodexHomePath: string +}): CodexPaneLaunchHomeResolution { + const record = args.record + if (!record || record.selectionKey !== 'host') { + return { kind: 'unrecorded' } + } + // Pane-local overrides carry the literal CODEX_HOME the launch resolved, so + // they answer before the route, which only classifies it. + const overrideHome = + record.environmentHomeOverride?.codexHome ?? record.shellStartupHomeOverride?.codexHome + if (overrideHome) { + return { kind: 'attributed', path: overrideHome } + } + if (record.homeRoute === 'real-home') { + return { kind: 'attributed', path: args.systemCodexHomePath } + } + if (record.homeRoute === 'shared-home') { + return { kind: 'attributed', path: args.sharedRuntimeCodexHomePath } + } + if (record.homeRoute !== 'account-home') { + // `custom-home` recorded a home Orca cannot re-derive; an absent route predates + // provenance; `wsl-home` cannot belong to a host pane. + return { kind: 'unnameable-home' } + } + if (!record.accountId) { + return { kind: 'unnameable-home' } + } + const account = args.settings.codexManagedAccounts?.find( + (candidate) => + candidate.id === record.accountId && + getCodexSelectionTargetForAccount(candidate).runtime === 'host' + ) + return account + ? { kind: 'attributed', path: account.managedHomePath } + : { kind: 'unknown-account', accountId: record.accountId } +} + +/** Names the account cause, so a mismatch never reads as a broken session. */ +export function describeCodexPaneLaunchHomeFailure( + resolution: Exclude +): string { + if (resolution.kind === 'unknown-account') { + return `This terminal runs Codex under managed account ${resolution.accountId}, which is no longer configured, so its Codex session cannot be located.` + } + if (resolution.kind === 'unnameable-home') { + return 'This terminal runs Codex under a CODEX_HOME Orca cannot identify, so its Codex session cannot be located.' + } + return 'Orca has no record of which Codex account this terminal launched under, so its Codex session cannot be located. Start a new Codex chat instead.' +} diff --git a/src/main/runtime/orca-runtime-structured-tui-adoption-account-home.test.ts b/src/main/runtime/orca-runtime-structured-tui-adoption-account-home.test.ts new file mode 100644 index 00000000000..8fa66d3c903 --- /dev/null +++ b/src/main/runtime/orca-runtime-structured-tui-adoption-account-home.test.ts @@ -0,0 +1,299 @@ +/** + * Adoption against the REAL account-home resolution. + * + * Why this rig exists: every other adoption test stubs the intent resolver with a hardcoded + * `accountHome`, so none of them could see that adoption resolved the CURRENTLY SELECTED Codex + * account's home instead of the one the pane's Codex is actually running under. Here the pane's + * home comes from the real pane-account registry and the real settings, and the rollout fixture + * exists ONLY under the pane's home — so probing the selected account's home fails the proof. + */ + +import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import { randomUUID } from 'node:crypto' +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import { createStructuredAgentSessionOperationId } from '../../shared/structured-agent-session-mutation' +import { StructuredAgentSessionHost } from '../native-chat/agent-session-wire/structured-agent-session-host' +import { setStructuredAgentSessionHost } from '../native-chat/agent-session-wire/structured-agent-session-registry' +import { + _internals as codexPaneAccountRegistryInternals, + type CodexPaneAccountRecord +} from '../codex/codex-pane-account-registry' +import { AgentSessionRecordStore } from './agent-session-record-store' +import { agentSessionPtyWriteGate } from './agent-session-pty-write-gate' +import { createStructuredAgentSessionOwnerProbe } from './structured-agent-session-runtime' +import { OrcaRuntimeService } from './orca-runtime' + +const { readStructuredTuiProcessIdentity } = vi.hoisted(() => ({ + readStructuredTuiProcessIdentity: vi.fn() +})) +// The only OS-dependent step in the flow; the rollout proof and the account-home resolution stay real. +vi.mock('./structured-tui-process-identity', () => ({ readStructuredTuiProcessIdentity })) + +const WORKTREE_ID = 'repo-1::/tmp/structured-adoption-home' +const PTY_ID = 'pty-adopt' +const PANE_KEY = 'tab-adopt:leaf-adopt' +const THREAD_ID = '019fd532-7c11-7a90-b6de-4e1a2c3d5f61' +const SESSION_ID = `codex_${THREAD_ID.replaceAll('-', '_')}` +const PANE_ACCOUNT_ID = 'codex-account-pane' +const SELECTED_ACCOUNT_ID = 'codex-account-selected' + +type AdoptionRig = { + runtime: OrcaRuntimeService + store: AgentSessionRecordStore + host: StructuredAgentSessionHost + proofWrites: string[] +} + +let root: string +let paneAccountHome: string +let selectedAccountHome: string +let rig: AdoptionRig + +async function writeRolloutFixture(codexHome: string, threadId: string): Promise { + const dir = join(codexHome, 'sessions', '2026', '08', '19') + await mkdir(dir, { recursive: true }) + await writeFile( + join(dir, `rollout-2026-08-19T10-00-00-${threadId}.jsonl`), + `${JSON.stringify({ type: 'session_meta', payload: { id: threadId } })}\n`, + 'utf8' + ) +} + +/** Writes the registry file the real `getCodexPaneAccount` reads at adoption time. */ +async function recordPaneAccount(record: CodexPaneAccountRecord | null): Promise { + await writeFile( + join(root, 'codex-pane-accounts.json'), + `${JSON.stringify({ version: 2, panes: record ? { [PTY_ID]: record } : {} })}\n`, + 'utf8' + ) + codexPaneAccountRegistryInternals.resetCache() +} + +function buildSettings() { + const account = (id: string, managedHomePath: string) => ({ + id, + email: `${id}@example.com`, + managedHomePath, + createdAt: 1, + updatedAt: 1, + lastAuthenticatedAt: 1 + }) + return { + // Why a configured CODEX_HOME: it is what the create intent resolves to in this rig, so an + // adoption that follows the selection probes THIS home instead of the pane's. + agentDefaultEnv: { codex: { CODEX_HOME: selectedAccountHome } }, + activeCodexManagedAccountId: SELECTED_ACCOUNT_ID, + codexManagedAccounts: [ + account(PANE_ACCOUNT_ID, paneAccountHome), + account(SELECTED_ACCOUNT_ID, selectedAccountHome) + ] + } +} + +function installProofPty(rigState: AdoptionRig, ptyRecord: { lastOutputAt: number }) { + return (ptyId: string, data: string, authority: { sessionId: string; spawnToken: string }) => { + if (!agentSessionPtyWriteGate.admitProof(ptyId, authority)) { + return false + } + rigState.proofWrites.push(data) + if (data === '\r') { + const internal = rigState.runtime as unknown as { + ptysById: Map + } + const pty = internal.ptysById.get(PTY_ID)! + pty.tailBuffer = [`Session ID: ${THREAD_ID}`] + pty.lastOutputAt = ptyRecord.lastOutputAt + 1_000 + } + return true + } +} + +async function buildRig(): Promise { + const store = await AgentSessionRecordStore.open({ + directory: join(root, 'agent-sessions'), + hostId: 'local' + }) + agentSessionPtyWriteGate.attachRecordLookup((sessionId) => store.getRecord(sessionId)) + const host = new StructuredAgentSessionHost({ + store, + adapter: { + supportsCreate: () => true, + acquire: vi.fn(), + dispatch: vi.fn(), + cancelTurn: vi.fn(), + answerPrompt: vi.fn(), + setOption: vi.fn() + }, + journalRoot: join(root, 'journals'), + claimKeyId: 'key-1', + probeOwner: createStructuredAgentSessionOwnerProbe( + 'local', + async () => ({ outcome: 'identity-matched', matchedOn: ['process-start-time'] }), + async () => [] + ) + }) + setStructuredAgentSessionHost(host) + + const runtime = new OrcaRuntimeService({ getSettings: () => buildSettings() } as never) + const state: AdoptionRig = { runtime, store, host, proofWrites: [] } + const ptyRecord = { + ptyId: PTY_ID, + connected: true, + connectionId: null, + wslDistro: null, + tabId: 'tab-adopt', + paneKey: PANE_KEY, + worktreeId: WORKTREE_ID, + incarnationId: 'inc-adopt', + tailBuffer: [] as string[], + tailPartialLine: '', + preview: '', + lastOutputAt: 1_000 + } + const internal = runtime as unknown as { + ptysById: Map + resolveRuntimeFileTarget(): Promise + resolveStructuredAgentSessionLocation(): Promise + ensureStructuredAgentSessionHost(): Promise + issueStructuredTuiPtyHandle(): string + } + internal.ptysById.set(PTY_ID, ptyRecord) + internal.resolveRuntimeFileTarget = vi.fn(async () => ({ + connectionId: null, + worktree: { id: WORKTREE_ID, path: join(root, 'workspace') } + })) + internal.resolveStructuredAgentSessionLocation = vi.fn(async () => ({ + executionHostId: 'local', + wslDistro: null, + workspaceId: WORKTREE_ID, + workspaceKind: 'git-worktree' + })) + // The rig owns the host; installing the real one would replace it with an on-disk singleton. + internal.ensureStructuredAgentSessionHost = vi.fn(async () => {}) + internal.issueStructuredTuiPtyHandle = vi.fn(() => 'term-adopt') + runtime.setPtyController({ + listProcesses: async () => [{ id: PTY_ID, incarnationId: 'inc-adopt', rootProcessId: 31337 }], + writeAgentSessionProof: installProofPty(state, ptyRecord) + } as never) + return state +} + +function adoptInput() { + return { + envelope: { + sessionId: SESSION_ID, + clientOperationId: createStructuredAgentSessionOperationId(() => randomUUID()), + expectedRuntimeFence: null as null, + payloadFingerprint: 'a'.repeat(64) + }, + worktree: `id:${WORKTREE_ID}`, + tabId: 'tab-adopt', + paneKey: PANE_KEY, + ptyId: PTY_ID, + threadId: THREAD_ID + } +} + +beforeEach(async () => { + root = await mkdtemp(join(tmpdir(), 'orca-adopt-home-')) + vi.stubEnv('ORCA_USER_DATA_PATH', root) + paneAccountHome = join(root, 'codex-accounts', PANE_ACCOUNT_ID, 'home') + selectedAccountHome = join(root, 'codex-accounts', SELECTED_ACCOUNT_ID, 'home') + await recordPaneAccount({ + selectionKey: 'host', + accountId: PANE_ACCOUNT_ID, + homeRoute: 'account-home' + }) + readStructuredTuiProcessIdentity.mockImplementation( + async (input: { hostId: string; spawnToken: string }) => ({ + hostId: input.hostId, + pid: 4242, + processStartTimeMs: 1_700_000_000_000, + spawnToken: input.spawnToken + }) + ) + rig = await buildRig() +}) + +afterEach(async () => { + agentSessionPtyWriteGate.unbindPty(PTY_ID) + agentSessionPtyWriteGate.detachRecordLookup() + setStructuredAgentSessionHost(null) + codexPaneAccountRegistryInternals.resetCache() + vi.unstubAllEnvs() + await rig.host.flushAllStreamedEvents() + await rm(root, { recursive: true, force: true }) +}) + +describe('structured Codex adoption resolves the account home from the pane', () => { + it('adopts a pane whose Codex runs under a non-selected account home', async () => { + // Only the pane's account owns the rollout; the selected account's home has none. + await writeRolloutFixture(paneAccountHome, THREAD_ID) + + const result = await rig.runtime.adoptStructuredAgentSessionTerminal(adoptInput(), { + callerKey: 'renderer-1' + }) + + expect(result).toMatchObject({ ok: true }) + expect(rig.store.getRecord(SESSION_ID)).toMatchObject({ + accountHome: { variable: 'CODEX_HOME', path: paneAccountHome }, + lease: { runtimeKind: 'tui', claimStatus: 'live' } + }) + }, 20_000) + + it('still adopts a pane whose account is the selected one', async () => { + await recordPaneAccount({ + selectionKey: 'host', + accountId: SELECTED_ACCOUNT_ID, + homeRoute: 'account-home' + }) + await writeRolloutFixture(selectedAccountHome, THREAD_ID) + + const result = await rig.runtime.adoptStructuredAgentSessionTerminal(adoptInput(), { + callerKey: 'renderer-1' + }) + + expect(result).toMatchObject({ ok: true }) + expect(rig.store.getRecord(SESSION_ID)).toMatchObject({ + accountHome: { path: selectedAccountHome } + }) + }, 20_000) + + it('refuses a pane whose Codex home cannot be attributed instead of probing another account', async () => { + await recordPaneAccount(null) + await writeRolloutFixture(selectedAccountHome, THREAD_ID) + + await expect( + rig.runtime.adoptStructuredAgentSessionTerminal(adoptInput(), { callerKey: 'renderer-1' }) + ).rejects.toThrow('no record of which Codex account this terminal launched under') + // Nothing was probed and nothing was reserved, so the wrong home was never consulted. + expect(rig.proofWrites).toEqual([]) + expect(agentSessionPtyWriteGate.boundSessionId(PTY_ID)).toBeNull() + expect(rig.store.getRecord(SESSION_ID)).toBeNull() + }) + + it('names the account when the pane launched under one that is gone', async () => { + await recordPaneAccount({ + selectionKey: 'host', + accountId: 'codex-account-removed', + homeRoute: 'account-home' + }) + + await expect( + rig.runtime.adoptStructuredAgentSessionTerminal(adoptInput(), { callerKey: 'renderer-1' }) + ).rejects.toThrow('managed account codex-account-removed, which is no longer configured') + }) + + it('leaves create following the selected account, not the adopted pane', async () => { + const created = await rig.runtime.resolveStructuredAgentSessionCreateIntent({ + envelope: { sessionId: SESSION_ID, clientOperationId: 'op-create' }, + worktree: `id:${WORKTREE_ID}`, + agent: 'codex' + }) + + // The registry still names the pane's account; a create must ignore it. + expect(created.accountHome).toEqual({ variable: 'CODEX_HOME', path: selectedAccountHome }) + }) +}) diff --git a/src/main/runtime/orca-runtime-structured-tui-adoption-write-gate.test.ts b/src/main/runtime/orca-runtime-structured-tui-adoption-write-gate.test.ts index 861cd434f0e..a3d1db82f63 100644 --- a/src/main/runtime/orca-runtime-structured-tui-adoption-write-gate.test.ts +++ b/src/main/runtime/orca-runtime-structured-tui-adoption-write-gate.test.ts @@ -128,7 +128,7 @@ async function buildRig(): Promise { const internal = runtime as unknown as { ptysById: Map resolveRuntimeFileTarget(): Promise - resolveStructuredAgentSessionCreateIntent(input: { envelope: unknown }): Promise + resolveStructuredAgentSessionAdoptionIntent(input: { envelope: unknown }): Promise issueStructuredTuiPtyHandle(): string } internal.ptysById.set(PTY_ID, ptyRecord) @@ -136,7 +136,10 @@ async function buildRig(): Promise { connectionId: null, worktree: { id: WORKTREE_ID } })) - internal.resolveStructuredAgentSessionCreateIntent = vi.fn(async ({ envelope }) => ({ + // Why the adoption intent and not the create one: adoption resolves the pane's OWN account + // home, which this rig has no pane-account record for. The pane-home resolution itself is + // covered by orca-runtime-structured-tui-adoption-account-home.test.ts. + internal.resolveStructuredAgentSessionAdoptionIntent = vi.fn(async ({ envelope }) => ({ envelope, location: { executionHostId: 'local', diff --git a/src/main/runtime/orca-runtime-structured-tui-tab-binding.test.ts b/src/main/runtime/orca-runtime-structured-tui-tab-binding.test.ts index 778c9154447..5d25439c1e7 100644 --- a/src/main/runtime/orca-runtime-structured-tui-tab-binding.test.ts +++ b/src/main/runtime/orca-runtime-structured-tui-tab-binding.test.ts @@ -43,7 +43,7 @@ function terminalAdoptionHarness() { const internal = runtime as unknown as { ptysById: Map resolveRuntimeFileTarget(): Promise - resolveStructuredAgentSessionCreateIntent(input: { envelope: unknown }): Promise + resolveStructuredAgentSessionAdoptionIntent(input: { envelope: unknown }): Promise issueStructuredTuiPtyHandle(): string } internal.ptysById.set('pty-adopt', { @@ -60,7 +60,10 @@ function terminalAdoptionHarness() { connectionId: null, worktree: { id: WORKTREE_ID } })) - internal.resolveStructuredAgentSessionCreateIntent = vi.fn(async ({ envelope }) => ({ + // Why the adoption intent and not the create one: adoption resolves the pane's OWN account + // home, which this rig has no pane-account record for. The pane-home resolution itself is + // covered by orca-runtime-structured-tui-adoption-account-home.test.ts. + internal.resolveStructuredAgentSessionAdoptionIntent = vi.fn(async ({ envelope }) => ({ envelope, location: { executionHostId: 'local', diff --git a/src/main/runtime/orca-runtime.ts b/src/main/runtime/orca-runtime.ts index 524ce8c5585..5f42ad4ddd8 100644 --- a/src/main/runtime/orca-runtime.ts +++ b/src/main/runtime/orca-runtime.ts @@ -122,7 +122,12 @@ import { readStructuredTuiProcessIdentity } from './structured-tui-process-ident import { hasStructuredTuiIdleEvidence } from './structured-tui-idle-evidence' import { evaluateStructuredTuiRecoveryClaim } from './structured-tui-recovery-claim-match' import { getProfileUserDataPath } from '../orca-profiles/profile-storage-paths' -import { getSystemCodexHomePath } from '../codex/codex-home-paths' +import { getSystemCodexHomePath, resolveOrcaManagedCodexHomePath } from '../codex/codex-home-paths' +import { getCodexPaneAccount } from '../codex/codex-pane-account-registry' +import { + describeCodexPaneLaunchHomeFailure, + resolveCodexPaneLaunchHome +} from '../codex/codex-pane-launch-home' import { agentSessionPtyWriteGate, type AgentSessionPtyWriteAdmittance @@ -11451,10 +11456,11 @@ export class OrcaRuntimeService { if (target.connectionId || target.worktree.id !== pty.worktreeId) { throw new Error('Structured Codex chat is unavailable for remote terminals.') } - const intent = await this.resolveStructuredAgentSessionCreateIntent({ + const intent = await this.resolveStructuredAgentSessionAdoptionIntent({ envelope: input.envelope, worktree: input.worktree, - agent: 'codex' + agent: 'codex', + ptyId: input.ptyId }) const listing = (await this.ptyController?.listProcesses?.())?.find( (candidate) => @@ -11678,6 +11684,60 @@ export class OrcaRuntimeService { worktree: string agent: 'codex' }): Promise { + return this.resolveStructuredAgentSessionIntent(input, ({ workspacePath, launchEnv }) => { + // A create has no process yet, so the current selection is what it must follow. + const preparedHome = this.prepareCodexStructuredLaunchFn?.({ workspacePath, launchEnv }) + const configuredHome = launchEnv.CODEX_HOME + return ( + preparedHome?.trim() || + (this.prepareCodexStructuredLaunchFn ? getSystemCodexHomePath() : configuredHome?.trim()) || + getSystemCodexHomePath() + ) + }) + } + + /** + * Why adoption resolves its own intent: the pane's Codex is already running under the + * CODEX_HOME it was spawned with, and only that home's sessions directory can hold its + * rollout. Following the create intent here probed the currently selected account instead, + * so adopting a pane on any other account failed with no hint that the account mismatched. + */ + async resolveStructuredAgentSessionAdoptionIntent(input: { + envelope: { sessionId: string; clientOperationId: string } + worktree: string + agent: 'codex' + ptyId: string + }): Promise { + return this.resolveStructuredAgentSessionIntent(input, () => + this.resolveAdoptedCodexPaneHomePath(input.ptyId) + ) + } + + private resolveAdoptedCodexPaneHomePath(ptyId: string): string { + const resolved = resolveCodexPaneLaunchHome({ + record: getCodexPaneAccount(ptyId), + settings: this.requireStore().getSettings(), + systemCodexHomePath: getSystemCodexHomePath(), + sharedRuntimeCodexHomePath: resolveOrcaManagedCodexHomePath() + }) + // Never fall back to the selected home: a silent wrong-home probe is what hid the mismatch. + if (resolved.kind !== 'attributed') { + throw new Error(describeCodexPaneLaunchHomeFailure(resolved)) + } + return resolved.path + } + + private async resolveStructuredAgentSessionIntent( + input: { + envelope: { sessionId: string; clientOperationId: string } + worktree: string + agent: 'codex' + }, + resolveAccountHomePath: (context: { + workspacePath: string + launchEnv: NodeJS.ProcessEnv + }) => string + ): Promise { const support = await this.getStructuredAgentSessionCreateSupport(input.worktree, input.agent) if (!support.supported) { throw new Error('structured_agent_session_unsupported') @@ -11686,8 +11746,6 @@ export class OrcaRuntimeService { const launchEnv = resolveTuiAgentLaunchEnv('codex', settings.agentDefaultEnv) const location = await this.resolveStructuredAgentSessionLocation(input.worktree) const workspacePath = (await this.resolveRuntimeFileTarget(input.worktree)).worktree.path - const preparedHome = this.prepareCodexStructuredLaunchFn?.({ workspacePath, launchEnv }) - const configuredHome = launchEnv.CODEX_HOME return { envelope: { sessionId: input.envelope.sessionId, @@ -11700,12 +11758,7 @@ export class OrcaRuntimeService { agent: 'codex', accountHome: { variable: 'CODEX_HOME', - path: - preparedHome?.trim() || - (this.prepareCodexStructuredLaunchFn - ? getSystemCodexHomePath() - : configuredHome?.trim()) || - getSystemCodexHomePath() + path: resolveAccountHomePath({ workspacePath, launchEnv }) }, runtimeKind: 'native' }