Files
orca/src/shared/tui-agent-startup.ts
T
Mark Xian e3adb20917 fix(agents): include OMP terminals in cold session restoration (#8991)
Preserve OMP session identity and exact resume paths across cold restoration, AI Vault, mobile, WSL/SSH, and host-authority routes. Add mixed-version fallback and completed-session recovery coverage.
2026-07-23 19:05:22 -07:00

327 lines
11 KiB
TypeScript

import { isShellProcess } from './agent-detection'
import {
getAgentResumeArgv,
type AgentProviderSessionMetadata,
type ResumableTuiAgent,
type SleepingAgentLaunchConfig
} from './agent-session-resume'
import {
clearEnvCommand,
commandSeparator,
quoteStartupArg,
resolveStartupShell,
type AgentStartupShell
} from './tui-agent-startup-shell'
import { TUI_AGENT_CONFIG } from './tui-agent-config'
import type { StartupCommandDelivery } from './codex-startup-delivery'
import { buildSleepingAgentLaunchConfig } from './sleeping-agent-launch-config'
import { planHermesStartupQuery } from './hermes-startup-query'
import { inlineAgentDraftFitsPlatform } from './agent-draft-platform-limit'
import type { TuiAgent } from './types'
import type { SessionOptionValue } from './native-chat-session-options'
import { resolveAgentLaunchCommand } from './tui-agent-launch-command'
export type AgentStartupPlan = {
agent: TuiAgent
launchCommand: string
expectedProcess: string
followupPrompt: string | null
launchConfig: SleepingAgentLaunchConfig
launchToken?: string
draftPrompt?: string | null
env?: Record<string, string>
startupCommandDelivery?: StartupCommandDelivery
/** Values actually emitted into this launch command, kept as base model ids
* so the native-chat surface can render only launch-backed state. */
sessionOptions?: Record<string, SessionOptionValue>
}
function appliedSessionOptionProps(
values: Record<string, SessionOptionValue>
): Pick<AgentStartupPlan, 'sessionOptions'> {
return Object.keys(values).length > 0 ? { sessionOptions: { ...values } } : {}
}
export function buildAgentStartupPlan(args: {
agent: TuiAgent
prompt: string
cmdOverrides: Partial<Record<TuiAgent, string>>
platform: NodeJS.Platform
shell?: AgentStartupShell
allowEmptyPromptLaunch?: boolean
agentArgs?: string | null
agentEnv?: Record<string, string> | null
sessionOptions?: Record<string, SessionOptionValue>
/** Why: SSH remotes deploy the CLI shim as plain `orca`, so the Linux-only
* `orca-ide` rename must be skipped for remote launches. */
isRemote?: boolean
}): AgentStartupPlan | null {
const { agent, prompt, cmdOverrides, platform, allowEmptyPromptLaunch = false } = args
const shell = resolveStartupShell(platform, args.shell)
const trimmedPrompt = prompt.trim()
const config = TUI_AGENT_CONFIG[agent]
const usesQuery = config.promptInjectionMode === 'hermes-query' && Boolean(trimmedPrompt)
const baseCommand = resolveAgentLaunchCommand({
agent,
cmdOverrides,
platform,
shell,
agentArgs: usesQuery ? null : args.agentArgs,
sessionOptions: args.sessionOptions,
isRemote: args.isRemote
})
if (!baseCommand.ok) {
return null
}
const launchConfig = buildSleepingAgentLaunchConfig({
...args,
// Why: picker flags are a one-time launch choice; a resumed provider
// session restores its own state and must retain only explicit user args.
agentCommand: baseCommand.commandWithoutSessionOptions
})
if (!trimmedPrompt) {
if (!allowEmptyPromptLaunch) {
return null
}
return {
agent,
launchCommand: baseCommand.command,
expectedProcess: config.expectedProcess,
followupPrompt: null,
launchConfig,
...appliedSessionOptionProps(baseCommand.appliedSessionOptions),
...(args.agentEnv ? { env: { ...args.agentEnv } } : {})
}
}
const quotedPrompt = quoteStartupArg(trimmedPrompt, shell)
if (config.promptInjectionMode === 'argv') {
const promptSeparator = config.argvPromptSeparator ? ` ${config.argvPromptSeparator}` : ''
return {
agent,
launchCommand: `${baseCommand.command}${promptSeparator} ${quotedPrompt}`,
expectedProcess: config.expectedProcess,
followupPrompt: null,
launchConfig,
...appliedSessionOptionProps(baseCommand.appliedSessionOptions),
...(agent === 'codex' ? { startupCommandDelivery: 'shell-ready' as const } : {}),
...(args.agentEnv ? { env: { ...args.agentEnv } } : {})
}
}
if (config.promptInjectionMode === 'flag-prompt') {
return {
agent,
launchCommand: `${baseCommand.command} --prompt ${quotedPrompt}`,
expectedProcess: config.expectedProcess,
followupPrompt: null,
launchConfig,
...appliedSessionOptionProps(baseCommand.appliedSessionOptions),
...(args.agentEnv ? { env: { ...args.agentEnv } } : {})
}
}
if (config.promptInjectionMode === 'hermes-query') {
const queryPlan = planHermesStartupQuery({
baseCommand: baseCommand.command,
agentArgs: args.agentArgs,
prompt: trimmedPrompt,
agentEnv: args.agentEnv,
platform,
shell,
isRemote: args.isRemote
})
if (!queryPlan) {
return null
}
return {
agent,
// Why: Hermes owns readiness and submission for `chat --query`; Orca
// only bounds and quotes the native invocation before starting the TUI.
launchCommand: queryPlan.command,
expectedProcess: config.expectedProcess,
followupPrompt: null,
launchConfig,
...appliedSessionOptionProps(baseCommand.appliedSessionOptions),
...(queryPlan.env ? { env: queryPlan.env } : {})
}
}
if (config.promptInjectionMode === 'flag-prompt-interactive') {
return {
agent,
launchCommand: `${baseCommand.command} --prompt-interactive ${quotedPrompt}`,
expectedProcess: config.expectedProcess,
followupPrompt: null,
launchConfig,
...appliedSessionOptionProps(baseCommand.appliedSessionOptions),
...(args.agentEnv ? { env: { ...args.agentEnv } } : {})
}
}
if (config.promptInjectionMode === 'flag-interactive') {
return {
agent,
launchCommand: `${baseCommand.command} -i ${quotedPrompt}`,
expectedProcess: config.expectedProcess,
followupPrompt: null,
launchConfig,
...appliedSessionOptionProps(baseCommand.appliedSessionOptions),
...(args.agentEnv ? { env: { ...args.agentEnv } } : {})
}
}
return {
agent,
launchCommand: baseCommand.command,
expectedProcess: config.expectedProcess,
followupPrompt: trimmedPrompt,
launchConfig,
...appliedSessionOptionProps(baseCommand.appliedSessionOptions),
...(args.agentEnv ? { env: { ...args.agentEnv } } : {})
}
}
export function buildAgentResumeStartupPlan(args: {
agent: ResumableTuiAgent
providerSession: AgentProviderSessionMetadata
cmdOverrides: Partial<Record<TuiAgent, string>>
platform: NodeJS.Platform
shell?: AgentStartupShell
agentArgs?: string | null
agentEnv?: Record<string, string> | null
agentCommand?: string | null
ompResumeFilePath?: string | null
sessionOptions?: Record<string, SessionOptionValue>
/** Why: see buildAgentStartupPlan — remote launches use the plain `orca` shim. */
isRemote?: boolean
}): AgentStartupPlan | null {
const argv = getAgentResumeArgv(args.agent, args.providerSession, args.ompResumeFilePath)
if (!argv) {
return null
}
const shell = resolveStartupShell(args.platform, args.shell)
const config = TUI_AGENT_CONFIG[args.agent]
const resolvedAgentCommand = args.agentCommand?.trim()
const baseCommand = resolvedAgentCommand
? ({ ok: true, command: resolvedAgentCommand } as const)
: resolveAgentLaunchCommand({
agent: args.agent,
cmdOverrides: args.cmdOverrides,
platform: args.platform,
shell,
agentArgs: args.agentArgs,
isRemote: args.isRemote
})
if (!baseCommand.ok) {
return null
}
const launchConfig = buildSleepingAgentLaunchConfig({
...args,
agentCommand: baseCommand.command
})
const resumeArgs = argv
.slice(1)
.map((arg) => quoteStartupArg(arg, shell))
.join(' ')
const launchCommand = resumeArgs ? `${baseCommand.command} ${resumeArgs}` : baseCommand.command
return {
agent: args.agent,
launchCommand,
expectedProcess: config.expectedProcess,
followupPrompt: null,
launchConfig,
...(args.agentEnv ? { env: { ...args.agentEnv } } : {})
}
}
export type AgentDraftLaunchPlan = {
agent: TuiAgent
launchCommand: string
expectedProcess: string
launchConfig: SleepingAgentLaunchConfig
env?: Record<string, string>
startupCommandDelivery?: StartupCommandDelivery
sessionOptions?: Record<string, SessionOptionValue>
}
export function buildAgentDraftLaunchPlan(args: {
agent: TuiAgent
draft: string
cmdOverrides: Partial<Record<TuiAgent, string>>
platform: NodeJS.Platform
shell?: AgentStartupShell
agentArgs?: string | null
agentEnv?: Record<string, string> | null
sessionOptions?: Record<string, SessionOptionValue>
/** Why: see buildAgentStartupPlan — remote launches use the plain `orca` shim. */
isRemote?: boolean
}): AgentDraftLaunchPlan | null {
const { agent, draft, cmdOverrides, platform } = args
const shell = resolveStartupShell(platform, args.shell)
const config = TUI_AGENT_CONFIG[agent]
const trimmed = draft.trim()
if (!trimmed) {
return null
}
const baseCommand = resolveAgentLaunchCommand({
agent,
cmdOverrides,
platform,
shell,
agentArgs: args.agentArgs,
sessionOptions: args.sessionOptions,
isRemote: args.isRemote
})
if (!baseCommand.ok) {
return null
}
const launchConfig = buildSleepingAgentLaunchConfig({
...args,
// Why: see the new-session path above — resume must not replay picker flags.
agentCommand: baseCommand.commandWithoutSessionOptions
})
let plan: AgentDraftLaunchPlan | null = null
if (config.draftPromptFlag) {
const quoted = quoteStartupArg(trimmed, shell)
plan = {
agent,
launchCommand: `${baseCommand.command} ${config.draftPromptFlag} ${quoted}`,
expectedProcess: config.expectedProcess,
launchConfig,
...appliedSessionOptionProps(baseCommand.appliedSessionOptions),
// Why: native draft flags carry user text on argv and must survive rc-file startup.
...(agent === 'codex' ? { startupCommandDelivery: 'shell-ready' as const } : {}),
...(args.agentEnv ? { env: { ...args.agentEnv } } : {})
}
} else if (config.draftPromptEnvVar) {
const clearVar = clearEnvCommand(config.draftPromptEnvVar, shell)
plan = {
agent,
launchCommand: `${baseCommand.command}${commandSeparator(shell)}${clearVar}`,
expectedProcess: config.expectedProcess,
launchConfig,
...appliedSessionOptionProps(baseCommand.appliedSessionOptions),
env: { ...args.agentEnv, [config.draftPromptEnvVar]: trimmed }
}
}
if (
!plan ||
!inlineAgentDraftFitsPlatform({ command: plan.launchCommand, env: plan.env, platform })
) {
return null
}
return plan
}
export { isShellProcess }
export {
buildShellCommandFromArgv,
planAgentCliArgsSuffix,
quoteStartupArg,
resolveStartupShell
} from './tui-agent-startup-shell'
export type { AgentCliArgsPlan, AgentStartupShell } from './tui-agent-startup-shell'