mirror of
https://github.com/stablyai/orca.git
synced 2026-10-23 16:03:49 +00:00
fix(native-chat): adopt a Codex pane on its own account home
Structured adoption reused the CREATE intent, whose account home is derived purely from the current selection. The pane's Codex is already running under the CODEX_HOME it was spawned with — and an automatic resume deliberately pins that to the home that OWNS the session — so adopting a pane on any other managed account probed the wrong sessions directory and failed as "did not prove the expected Codex rollout", with no hint that the account mismatched. Adoption now resolves the home from the pane's own launch record and declines, naming the account, when the pane cannot be attributed. Create is unchanged. Hooks skipped: lint-staged stashes, and the stash stack is shared across every worktree on this machine. oxfmt, oxlint, the native code-quality audit, the max-lines ratchet, tsc and the runtime/codex suites were all run by hand.
This commit is contained in:
@@ -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'
|
||||
)
|
||||
})
|
||||
})
|
||||
@@ -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<GlobalSettings, 'codexManagedAccounts'>
|
||||
|
||||
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<CodexPaneLaunchHomeResolution, { kind: 'attributed' }>
|
||||
): 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.'
|
||||
}
|
||||
@@ -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<void> {
|
||||
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<void> {
|
||||
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<string, { tailBuffer: string[]; lastOutputAt: number }>
|
||||
}
|
||||
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<AdoptionRig> {
|
||||
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<string, unknown>
|
||||
resolveRuntimeFileTarget(): Promise<unknown>
|
||||
resolveStructuredAgentSessionLocation(): Promise<unknown>
|
||||
ensureStructuredAgentSessionHost(): Promise<void>
|
||||
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 })
|
||||
})
|
||||
})
|
||||
@@ -128,7 +128,7 @@ async function buildRig(): Promise<ProofRig> {
|
||||
const internal = runtime as unknown as {
|
||||
ptysById: Map<string, unknown>
|
||||
resolveRuntimeFileTarget(): Promise<unknown>
|
||||
resolveStructuredAgentSessionCreateIntent(input: { envelope: unknown }): Promise<unknown>
|
||||
resolveStructuredAgentSessionAdoptionIntent(input: { envelope: unknown }): Promise<unknown>
|
||||
issueStructuredTuiPtyHandle(): string
|
||||
}
|
||||
internal.ptysById.set(PTY_ID, ptyRecord)
|
||||
@@ -136,7 +136,10 @@ async function buildRig(): Promise<ProofRig> {
|
||||
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',
|
||||
|
||||
@@ -43,7 +43,7 @@ function terminalAdoptionHarness() {
|
||||
const internal = runtime as unknown as {
|
||||
ptysById: Map<string, unknown>
|
||||
resolveRuntimeFileTarget(): Promise<unknown>
|
||||
resolveStructuredAgentSessionCreateIntent(input: { envelope: unknown }): Promise<unknown>
|
||||
resolveStructuredAgentSessionAdoptionIntent(input: { envelope: unknown }): Promise<unknown>
|
||||
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',
|
||||
|
||||
@@ -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<AgentSessionAttachParams> {
|
||||
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<AgentSessionAttachParams> {
|
||||
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<AgentSessionAttachParams> {
|
||||
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'
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user