fix(orchestration): launch the configured agent CLI, not the raw agent id

worker-start passed the Orca agent id straight to the shell as the worker
terminal command. That only worked for agents whose id matches their launch
command; `--agent cursor` ran `cursor`, which on Windows resolves to Cursor
IDE's cursor.cmd and opened the desktop app, leaving a blank shell that timed
out at agent_readiness. The same gap hit continue/aug/kiro/qwen-code and the
other agents whose CLI binary differs from their id.

Add TerminalCreateOptions.startupAgent so a caller can name the agent outright.
createTerminal then builds the launch through the TUI agent config (command,
default args/env, preflight trust) instead of sniffing the command string, and
this also covers repo-less folder workspaces, which previously skipped
resolution entirely.

Fixes #11926
This commit is contained in:
OrcaWin
2026-08-02 17:05:34 -07:00
parent 7c7167028c
commit 3e38d67c14
5 changed files with 146 additions and 13 deletions
+88
View File
@@ -12550,6 +12550,94 @@ describe('OrcaRuntimeService', () => {
)
})
// Why: `cursor` on PATH is the Cursor desktop launcher; only `cursor-agent` is
// the CLI Orca can host (issue #11926).
it('launches the configured agent CLI for a startupAgent id, not the raw id', async () => {
const spawn = vi.fn().mockResolvedValue({ id: 'pty-bg' })
const runtimeStore = {
...store,
getSettings: () => ({
...store.getSettings(),
disabledTuiAgents: [],
agentCmdOverrides: {},
agentDefaultArgs: { cursor: '--force' },
agentDefaultEnv: { cursor: { CURSOR_PROFILE: 'captured' } }
})
}
const runtime = new OrcaRuntimeService(runtimeStore)
runtime.setPtyController({
spawn,
write: () => true,
kill: () => true,
getForegroundProcess: async () => null
})
await runtime.createTerminal(`path:${TEST_WORKTREE_PATH}`, {
startupAgent: 'cursor',
title: 'worker'
})
const spawnCall = spawn.mock.calls[0]?.[0] as
| { command?: string; launchAgent?: string; env?: Record<string, string> }
| undefined
expect(spawnCall?.command).toBe("cursor-agent '--force'")
expect(spawnCall?.launchAgent).toBe('cursor')
expect(spawnCall?.env).toMatchObject({ CURSOR_PROFILE: 'captured' })
expect(markCursorWorkspaceTrustedMock).toHaveBeenCalledWith(TEST_WORKTREE_PATH)
})
it('resolves a startupAgent to the CLI binary on Windows, where `cursor` is the IDE', async () => {
setPlatform('win32')
const spawn = vi.fn().mockResolvedValue({ id: 'pty-bg' })
const runtimeStore = {
...store,
getSettings: () => ({
...store.getSettings(),
disabledTuiAgents: [],
terminalWindowsShell: 'cmd.exe',
agentCmdOverrides: {},
agentDefaultArgs: {},
agentDefaultEnv: {}
})
}
const runtime = new OrcaRuntimeService(runtimeStore)
runtime.setPtyController({
spawn,
write: () => true,
kill: () => true,
getForegroundProcess: async () => null
})
await runtime.createTerminal(`path:${TEST_WORKTREE_PATH}`, { startupAgent: 'cursor' })
const spawnCall = spawn.mock.calls[0]?.[0] as { command?: string } | undefined
expect(spawnCall?.command).toMatch(/^cursor-agent(\s|$)/)
})
it('rejects a startupAgent create for a disabled agent', async () => {
const spawn = vi.fn().mockResolvedValue({ id: 'pty-bg' })
const runtimeStore = {
...store,
getSettings: () => ({
...store.getSettings(),
disabledTuiAgents: ['cursor' as const],
agentCmdOverrides: {}
})
}
const runtime = new OrcaRuntimeService(runtimeStore)
runtime.setPtyController({
spawn,
write: () => true,
kill: () => true,
getForegroundProcess: async () => null
})
await expect(
runtime.createTerminal(`path:${TEST_WORKTREE_PATH}`, { startupAgent: 'cursor' })
).rejects.toThrow(/disabled/)
expect(spawn).not.toHaveBeenCalled()
})
it('quotes local Windows bare agent command defaults for cmd.exe terminal creates', async () => {
setPlatform('win32')
const spawn = vi.fn().mockResolvedValue({ id: 'pty-bg' })
+26 -9
View File
@@ -1273,6 +1273,10 @@ type TerminalCreateOptions = {
resumeProviderSession?: AgentProviderSessionMetadata
launchToken?: string
launchAgent?: TuiAgent
// Why: agent ids are not shell commands (`cursor` is the Cursor desktop app; its
// CLI is `cursor-agent`). Callers that know the agent name it here instead of
// guessing a command, and the runtime builds the configured launch.
startupAgent?: TuiAgent
terminalColorQueryReplies?: TerminalOscColorQueryReplyColors
viewMode?: 'terminal' | 'chat'
startupCommandDelivery?: WorktreeStartupLaunch['startupCommandDelivery']
@@ -23873,32 +23877,40 @@ export class OrcaRuntimeService {
// Why: raw shell commands like `codex exec` must remain user-authored shell.
// Only unmanaged, repo-backed, bare agent launches get Settings defaults.
if (
!opts.command ||
opts.env ||
opts.launchConfig ||
opts.launchAgent ||
opts.startupCommandDelivery ||
opts.claudeAgentTeamsSourceCommand ||
!workspace.repo ||
!this.store
) {
return opts
}
// An explicit startupAgent is already the agent's identity, so it needs no
// command sniffing and works for repo-less folder workspaces too.
if (!opts.startupAgent && (!opts.command || !workspace.repo)) {
return opts
}
const settings = this.store.getSettings()
const platform = this.getAgentLaunchPlatformForWorkspace(workspace)
const isRemote = repoIsRemote(workspace.repo)
const isRemote = workspace.repo ? repoIsRemote(workspace.repo) : Boolean(workspace.connectionId)
const queuedShell = resolveLocalWindowsAgentStartupShell({
platform,
isRemote,
terminalWindowsShell: settings.terminalWindowsShell
})
const agent = resolveBareAgentLaunchCommand({
command: opts.command,
settings,
platform,
isRemote
})
if (opts.startupAgent && !isTuiAgentEnabled(opts.startupAgent, settings.disabledTuiAgents)) {
throw new Error(`Agent ${opts.startupAgent} is disabled. Choose an enabled agent.`)
}
const agent =
opts.startupAgent ??
resolveBareAgentLaunchCommand({
command: opts.command,
settings,
platform,
isRemote
})
if (!agent) {
return opts
}
@@ -23915,6 +23927,11 @@ export class OrcaRuntimeService {
allowEmptyPromptLaunch: true
})
if (!startupPlan) {
// Why: an explicit agent that yields no plan would otherwise spawn a bare
// shell that never reaches agent readiness.
if (opts.startupAgent) {
throw new Error(`Could not build launch command for ${opts.startupAgent}.`)
}
return opts
}
@@ -191,7 +191,9 @@ export const ORCHESTRATION_FEDERATION_ATTACH_METHODS: RpcMethod[] = [
} else {
failedStage = 'terminal_create'
const terminal = await runtime.createTerminal(`id:${worktree.id}`, {
command: agent,
// Why: agent ids are not shell commands (`cursor` is the desktop app,
// its CLI is `cursor-agent`); resolve through the TUI agent config.
startupAgent: agent as TuiAgent,
title: `worker-${params.taskId}`,
presentation: 'background'
})
@@ -59,7 +59,10 @@ export async function createExistingWorktreeWorkerTerminal(args: {
effects: WorkerEffect[]
}): Promise<{ handle: string; warning?: string }> {
const terminal = await args.runtime.createTerminal(`id:${args.worktreeId}`, {
command: args.agent,
// Why: the agent id is not a shell command — `cursor` resolves to the Cursor
// desktop app while its CLI is `cursor-agent`. Let the runtime build the
// configured launcher instead of executing the raw id.
startupAgent: args.agent,
title: `worker-${args.taskId}`,
// Why: dispatching a worker is background work; it must not pull the sidebar
// to the worker's workspace while the user is reading somewhere else.
@@ -2186,7 +2186,7 @@ describe('orchestration RPC methods', () => {
// Why: dispatching a worker is background work — surfaceOwner:false adopts
// the tab without scrolling the sidebar to the worker's workspace.
expect(runtime.createTerminal).toHaveBeenCalledWith('id:repo::worktree', {
command: 'codex',
startupAgent: 'codex',
title: `worker-${task.id}`,
surfaceOwner: false
})
@@ -2196,6 +2196,29 @@ describe('orchestration RPC methods', () => {
)
})
// Why: `cursor` on PATH is the Cursor desktop app; passing the agent id as a
// shell command opened the IDE and left a blank shell (issue #11926).
it('never passes the agent id to the worker terminal as a shell command', async () => {
setup()
mockCurrentWorkerStart()
const task = db.createTask({ spec: 'start a cursor worker' })
await call('orchestration.workerStart', {
task: task.id,
from: 'term_coord',
agent: 'cursor'
})
expect(runtime.createTerminal).toHaveBeenCalledWith(
'id:repo::worktree',
expect.objectContaining({ startupAgent: 'cursor' })
)
expect(runtime.createTerminal).toHaveBeenCalledWith(
'id:repo::worktree',
expect.not.objectContaining({ command: expect.anything() })
)
})
it('commits the launched worker token with its durable authority', async () => {
setup()
mockCurrentWorkerStart()
@@ -2289,7 +2312,7 @@ describe('orchestration RPC methods', () => {
'id:repo::other',
// Why: starting a worker in an existing worktree must not pull the sidebar
// away from whatever the user is looking at.
expect.objectContaining({ command: 'codex', surfaceOwner: false })
expect.objectContaining({ startupAgent: 'codex', surfaceOwner: false })
)
expect(createWorktree).not.toHaveBeenCalled()
})