Files
orca/src/main/runtime/runtime-worktree-agent-startup.ts
T
Brennan Benson e463aea99c fix(agent-launch): keep a short-lined multi-line prompt on a local zsh launch line, as main did
#24257 moved every multi-line or over-512-byte prompt off the typed launch line and pasted it after readiness. The phone's AI buttons and review notes, whose multi-line prompts main typed whole into zsh, then reached Claude 0.5-3 s later and their RPC reply waited for the paste.

The host now names the shell a local macOS or Linux line is typed into, the way the spawn picks it, and a multi-line prompt rides a zsh line when every line is at most 512 bytes and the whole line at most 8 KB. A real-zsh test types such a line through Orca's own ready barrier and startup write, including when a slow user config makes the write land early. Elsewhere the measured unsafe cases keep the paste: bash 3.2 runs multi-line lines piecemeal, fish drops an early multi-line write, and any shell loses a line over 1 KB written early.
2026-09-30 23:21:15 -07:00

197 lines
7.2 KiB
TypeScript

import { agentStartedTelemetry } from '../agent-launch/agent-started-telemetry'
import type { AgentLaunchPreferences } from '../../shared/agent-session-host-authority'
import type { Repo } from '../../shared/repo-types'
import type { TuiAgent } from '../../shared/tui-agent'
import type { WorktreeStartupLaunch } from '../../shared/worktree/launch-types'
import { repoIsRemote } from '../../shared/agent-launch-remote'
import { getRepoSshConnectionId } from '../../shared/execution-host'
import { isTuiAgent } from '../../shared/tui-agent-config'
import { isTuiAgentEnabled, pickTuiAgent } from '../../shared/tui-agent-selection'
import { resolveAgentStartupPlanInputs } from '../../shared/agent-startup-plan-inputs'
import { buildAgentDraftLaunchPlan, buildAgentStartupPlan } from '../../shared/tui-agent-startup'
import { planStartupWithPromptCandidate } from '../../shared/startup-line-prompt-carry'
import { nameLocalTypedLineShell } from './agent-launch-typed-line-shell'
import {
detectInstalledAgentsWithShellPathHydration,
detectRemoteAgents
} from '../preflight/agent-detection'
import type { RuntimeStore } from './runtime-store-contract'
export type WorktreeStartupDraftPaste = { agent: TuiAgent; content: string }
export type WorktreeStartupFollowup = { expectedProcess: string; prompt: string }
/** A fresh agent the host builds always carries its `agent_started` record; dropping it fails to compile. */
type AttributedWorktreeStartupLaunch = WorktreeStartupLaunch & {
telemetry: NonNullable<WorktreeStartupLaunch['telemetry']>
}
type StartupEnvironment = {
repo: Repo
settings: ReturnType<RuntimeStore['getSettings']>
getLaunchPlatform: () => NodeJS.Platform
/** Replaces the configured arguments for this launch; `null` means none. */
agentArgs?: string | null
/** Caller-supplied telemetry attribution, validated leniently at the host boundary. */
launchSource?: string
}
export async function buildWorktreeStartupForDraft(
environment: StartupEnvironment & { draft: string; requestedAgent?: TuiAgent }
): Promise<{
agent: TuiAgent
startup: AttributedWorktreeStartupLaunch
draftPaste?: WorktreeStartupDraftPaste
} | null> {
const content = environment.draft.trim()
if (!content) {
return null
}
const { repo, settings } = environment
const preferredAgent = environment.requestedAgent ?? settings.defaultTuiAgent
// Why: `blank` is an explicit shell-only preference, so linked drafts must not auto-pick an agent.
if (preferredAgent === 'blank') {
return null
}
let agent =
isTuiAgent(preferredAgent) && isTuiAgentEnabled(preferredAgent, settings.disabledTuiAgents)
? preferredAgent
: null
if (!agent) {
let detected: string[] = []
// Why: detection has to run on the machine that will run the agent, and SSH ownership has two
// spellings — the raw field probes this client for an `executionHostId: 'ssh:*'`-only repo.
const sshConnectionId = getRepoSshConnectionId(repo)
try {
// Why: startup-draft fallback can run from sparse runtime launch envs too.
detected = sshConnectionId
? await detectRemoteAgents({ connectionId: sshConnectionId })
: await detectInstalledAgentsWithShellPathHydration()
} catch {
detected = []
}
agent = pickTuiAgent(null, detected.filter(isTuiAgent), settings.disabledTuiAgents)
}
if (!agent) {
return null
}
const launchArgs = resolveAgentStartupPlanInputs({
agent,
settings,
platform: environment.getLaunchPlatform(),
isRemote: repoIsRemote(repo),
...(environment.agentArgs !== undefined ? { agentArgs: environment.agentArgs } : {})
})
const telemetry = agentStartedTelemetry(agent, environment.launchSource)
const draftPlan = buildAgentDraftLaunchPlan({ ...launchArgs, draft: content })
if (draftPlan) {
return {
agent,
startup: {
command: draftPlan.launchCommand,
launchConfig: draftPlan.launchConfig,
...(draftPlan.startupCommandDelivery
? { startupCommandDelivery: draftPlan.startupCommandDelivery }
: {}),
...(draftPlan.env ? { env: draftPlan.env } : {}),
telemetry
}
}
}
const startupPlan = buildAgentStartupPlan({
...launchArgs,
prompt: '',
allowEmptyPromptLaunch: true
})
if (!startupPlan) {
return null
}
return {
agent,
startup: {
command: startupPlan.launchCommand,
launchConfig: startupPlan.launchConfig,
...(startupPlan.startupCommandDelivery
? { startupCommandDelivery: startupPlan.startupCommandDelivery }
: {}),
...(startupPlan.env ? { env: startupPlan.env } : {}),
telemetry
},
draftPaste: { agent, content }
}
}
export function buildWorktreeStartupForAgent(
environment: StartupEnvironment & {
agent: TuiAgent
prompt?: string
launchPreferences?: AgentLaunchPreferences
toSessionOptions: (
preferences?: AgentLaunchPreferences
) => Parameters<typeof buildAgentStartupPlan>[0]['sessionOptions'] | undefined
/** Set by a caller that delivers an uncarried prompt itself: the prompt then rides only a typed
* line that can carry it, and this reports whether it did. Absent keeps the CLI's fold. */
onPromptCarry?: (carried: boolean) => void
}
): {
agent: TuiAgent
startup: AttributedWorktreeStartupLaunch
followup?: WorktreeStartupFollowup
} {
const { agent, repo, settings } = environment
if (!isTuiAgentEnabled(agent, settings.disabledTuiAgents)) {
throw new Error('Selected agent is disabled. Choose an enabled agent before creating.')
}
const planInputs = resolveAgentStartupPlanInputs({
agent,
settings,
platform: environment.getLaunchPlatform(),
isRemote: repoIsRemote(repo),
...(environment.agentArgs !== undefined ? { agentArgs: environment.agentArgs } : {}),
sessionOptions: environment.toSessionOptions(environment.launchPreferences)
})
const prompt = environment.prompt ?? ''
let startupPlan: ReturnType<typeof buildAgentStartupPlan>
if (environment.onPromptCarry) {
const offered = planStartupWithPromptCandidate(
planInputs,
prompt,
nameLocalTypedLineShell({
isRemote: repoIsRemote(repo),
...(settings.terminalDefaultShell
? { defaultShellSetting: settings.terminalDefaultShell }
: {})
})
)
startupPlan = offered.plan
if (startupPlan && prompt.trim()) {
environment.onPromptCarry(offered.promptCarried)
}
} else {
startupPlan = buildAgentStartupPlan({ ...planInputs, prompt, allowEmptyPromptLaunch: true })
}
if (!startupPlan) {
throw new Error(`Could not build launch command for ${agent}.`)
}
return {
agent,
startup: {
command: startupPlan.launchCommand,
launchConfig: startupPlan.launchConfig,
...(startupPlan.startupCommandDelivery
? { startupCommandDelivery: startupPlan.startupCommandDelivery }
: {}),
...(startupPlan.env ? { env: startupPlan.env } : {}),
telemetry: agentStartedTelemetry(agent, environment.launchSource)
},
...(startupPlan.followupPrompt
? {
followup: {
expectedProcess: startupPlan.expectedProcess,
prompt: startupPlan.followupPrompt
}
}
: {})
}
}