Files
orca/src/main/runtime/runtime-worktree-agent-startup.ts
T
Brennan Benson d3fdcac605 fix(agent-launch): make where a launch prompt rides explicit, and delivery mean the agent got it
Round-1 review of the rebuilt step 3.

- One outcome, switched on by every caller. planLaunchPrompt answers none,
  on-line, launch-file or paste-after-ready; only the carried outcomes hold
  the plan to launch, so no caller can read "a plan exists" as "carried".
  buildAgentStartupPlan now builds only a prompt-less launch. The new agent
  tab and work items had dropped a prompt an agent could not take in a file
  (any agent but Claude and Codex) and reported it delivered; they now paste
  it. The composer paths, background sessions, agent.launch, agentSession.create
  and phone quick commands switch on the same outcome.
- One rule for hosts that type the line raw. A Windows host or a paired Orca
  keeps a line to #24257's 512-byte typed budget; anything longer, multi-line
  or damaged by the shell rides a launch file for Claude and Codex and is
  pasted for others. A paired host is one predicate (launchHostIsPaired).
- Delivered means the agent got it. A prompt the new tab's line carried
  resolves its delivery when the agent's own hook reports a turn or the
  agent shows its composer, and false when the PTY never spawns (a refused
  launch file) or exits first, so callers that resolve review threads or
  mark notes delivered wait for that, not for the tab.
- Daemon protocol v41 writes launch files. A spawn routed to an older daemon
  is refused with the launch-file reason instead of typing a pointer to a
  file nobody wrote.
- A refused launch file or WSL staged line reaches agent.launch as its reason
  instead of a 10 s handle timeout, and hands the prompt back with Copy prompt
  for a staged line too.
- A WSL distro is probed only when the spawn needs its directory, and a
  failed probe is remembered for 30 s.
- Every hook status row shows the prompt a launch file carries instead of its
  pointer sentence.
2026-10-02 01:30:25 -07:00

198 lines
7.1 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,
planLaunchPrompt,
type AgentStartupPlan
} from '../../shared/tui-agent-startup'
import type { LaunchFile } from '../../shared/launch-prompt-file'
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 pastes an uncarried prompt itself; reports whether it rode the command,
* and no follow-up is returned for it to paste twice. */
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 planned = planLaunchPrompt({ ...planInputs, prompt: environment.prompt ?? '' })
if (!planned) {
throw new Error(`Could not build launch command for ${agent}.`)
}
let startupPlan: AgentStartupPlan
let launchFile: LaunchFile | undefined
let followup: WorktreeStartupFollowup | undefined
switch (planned.carry) {
case 'none':
startupPlan = planned.plan
break
case 'on-line':
case 'launch-file':
startupPlan = planned.plan
launchFile = planned.carry === 'launch-file' ? planned.launchFile : undefined
environment.onPromptCarry?.(true)
break
case 'paste-after-ready':
startupPlan = planned.cleanPlan
if (environment.onPromptCarry) {
environment.onPromptCarry(false)
} else {
followup = { expectedProcess: startupPlan.expectedProcess, prompt: planned.text }
}
break
}
return {
agent,
startup: {
command: startupPlan.launchCommand,
launchConfig: startupPlan.launchConfig,
...(startupPlan.startupCommandDelivery
? { startupCommandDelivery: startupPlan.startupCommandDelivery }
: {}),
...(startupPlan.env ? { env: startupPlan.env } : {}),
...(launchFile ? { launchFile } : {}),
telemetry: agentStartedTelemetry(agent, environment.launchSource)
},
...(followup ? { followup } : {})
}
}