mirror of
https://github.com/stablyai/orca.git
synced 2026-10-03 00:02:19 +00:00
* fix(codex): a native chat's thread opens on the model the chat chose * fix(codex): a resumed thread keeps its own saved model, provider and effort
124 lines
6.2 KiB
TypeScript
124 lines
6.2 KiB
TypeScript
// How a durable session record becomes a Codex process launch.
|
|
//
|
|
// Every input is read back from the record the store already made durable, not
|
|
// from the call that triggered the acquire. A client that attaches twice must
|
|
// land in the same working directory under the same account home, and a resume
|
|
// must name the thread this session actually proved — never one a caller asks
|
|
// for, which is how a resume becomes a fork wearing a resume's name.
|
|
|
|
import type { AgentSessionJournalIdentity } from '../../shared/agent-session-journal-types'
|
|
import { agentSessionProviderHandleChainHead } from '../../shared/agent-session-provider-handle'
|
|
import { LOCAL_EXECUTION_HOST_ID } from '../../shared/execution-host'
|
|
import { resolveCodexCommand } from '../codex-cli/command'
|
|
import type { AgentSessionRecordStore } from '../runtime/agent-session-record-store'
|
|
import type { CodexStructuredLaunch } from './codex-structured-session-adapter'
|
|
import type { CodexStructuredPermissionPolicy } from './codex-structured-permission-policy'
|
|
import { resolvePinnedCodexRolloutProof } from './codex-pinned-rollout-proof'
|
|
import { isWindowsProcessStartTimeAvailable } from '../windows/windows-process-table'
|
|
|
|
export type CodexStructuredLaunchResolverDeps = {
|
|
store: AgentSessionRecordStore
|
|
/** Absolute path of a workspace on this host. Rejects when the workspace no
|
|
* longer resolves, which is the case a stale mobile client hits. */
|
|
resolveWorkspacePath: (workspaceId: string) => Promise<string>
|
|
/** Overridden in tests; production scans the boot-cached PATH and version-manager dirs. */
|
|
resolveCommand?: (options?: { pathEnv?: string | null; homePath?: string }) => string
|
|
/** Fresh shell/configured environment for this spawn; never written to the session record. */
|
|
resolveEnvironment?: () => Promise<NodeJS.ProcessEnv>
|
|
resolveRollout?: typeof resolvePinnedCodexRolloutProof
|
|
/** Test seam for the host capability; production uses the native process table. */
|
|
isWindowsProcessStartTimeAvailable?: () => boolean
|
|
/** The user's Agent Permissions setting as thread policy, re-read per acquisition.
|
|
* States both postures outright — a resume inherits the last one for any field left absent. */
|
|
resolvePermissionPolicy?: () => CodexStructuredPermissionPolicy
|
|
}
|
|
|
|
export type CodexStructuredInvocation = {
|
|
command: string
|
|
environment: NodeJS.ProcessEnv | undefined
|
|
}
|
|
|
|
/**
|
|
* The one place a structured Codex child's binary and environment are
|
|
* resolved. The session launch and the session-less catalog probe both build
|
|
* on it, so a probe can never list under a different binary or env than the
|
|
* session it stands in for. Env VALUES stay out of the catalog fingerprint:
|
|
* drift there heals on the next refresh.
|
|
*/
|
|
export async function resolveCodexStructuredInvocation(
|
|
deps: Pick<CodexStructuredLaunchResolverDeps, 'resolveCommand' | 'resolveEnvironment'>
|
|
): Promise<CodexStructuredInvocation> {
|
|
const environment = await deps.resolveEnvironment?.()
|
|
const pathEnv = environment?.PATH ?? environment?.Path ?? null
|
|
const homePath = environment?.HOME ?? environment?.USERPROFILE
|
|
const command = (deps.resolveCommand ?? resolveCodexCommand)({
|
|
pathEnv,
|
|
...(homePath ? { homePath } : {})
|
|
})
|
|
return { command, environment }
|
|
}
|
|
|
|
export function createCodexStructuredLaunchResolver(
|
|
deps: CodexStructuredLaunchResolverDeps
|
|
): (input: { identity: AgentSessionJournalIdentity }) => Promise<CodexStructuredLaunch> {
|
|
return async ({ identity }) => {
|
|
const record = deps.store.getRecord(identity.sessionId)
|
|
if (!record) {
|
|
throw new Error(`no durable agent-session record for ${identity.sessionId}`)
|
|
}
|
|
const { location, accountHome } = record
|
|
if (record.provider !== 'codex') {
|
|
throw new Error(`session ${identity.sessionId} is a ${record.provider} session`)
|
|
}
|
|
// This adapter spawns a child on the machine the runtime itself runs on.
|
|
// A session pinned elsewhere belongs to that host's runtime, and quietly
|
|
// starting it here would put a second writer on the same thread.
|
|
if (location.executionHostId !== LOCAL_EXECUTION_HOST_ID || location.wslDistro !== null) {
|
|
throw new Error(
|
|
`codex structured sessions run on the local host, not ${location.executionHostId}`
|
|
)
|
|
}
|
|
// Refuse before resolving launch data; a PID alone cannot prove Windows ownership.
|
|
if (
|
|
process.platform === 'win32' &&
|
|
!(deps.isWindowsProcessStartTimeAvailable ?? isWindowsProcessStartTimeAvailable)()
|
|
) {
|
|
throw new Error('codex structured sessions require Windows process creation-time proof')
|
|
}
|
|
if (accountHome.variable !== 'CODEX_HOME') {
|
|
throw new Error(`codex sessions pin CODEX_HOME, not ${accountHome.variable}`)
|
|
}
|
|
const { command, environment } = await resolveCodexStructuredInvocation(deps)
|
|
// `record.launchArgs` is deliberately not read: the configured CLI arguments are a terminal
|
|
// concern, and the permission posture they used to smuggle in is derived per acquisition.
|
|
const permissionPolicy = deps.resolvePermissionPolicy?.()
|
|
const head = agentSessionProviderHandleChainHead(record.providerHandleChain)
|
|
const resumeThreadId = head?.handle.provider === 'codex' ? head.handle.threadId : null
|
|
// The same saved options every turn sends, so the thread and its turns name one model.
|
|
const model = record.options?.model
|
|
return {
|
|
command,
|
|
args: ['app-server'],
|
|
cwd: await deps.resolveWorkspacePath(location.workspaceId),
|
|
codexHome: accountHome.path,
|
|
...(environment ? { env: { ...environment } as Record<string, string> } : {}),
|
|
// An empty chain is a session that has never proved a thread, so it
|
|
// starts one; anything else resumes the last link this session proved.
|
|
resumeThreadId,
|
|
// Only a thread this session created may still be one Codex never saved: a resumed,
|
|
// forked or adopted head names a conversation Codex held.
|
|
...(resumeThreadId && head?.origin === 'created' ? { supersedeIfUnsaved: true } : {}),
|
|
...(permissionPolicy ? { permissionPolicy } : {}),
|
|
...(model ? { model } : {}),
|
|
...(resumeThreadId
|
|
? {
|
|
resumePath: await (deps.resolveRollout ?? resolvePinnedCodexRolloutProof)(
|
|
accountHome.path,
|
|
resumeThreadId
|
|
)
|
|
}
|
|
: {})
|
|
}
|
|
}
|
|
}
|