mirror of
https://github.com/stablyai/orca.git
synced 2026-10-29 00:03:37 +00:00
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:
@@ -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' })
|
||||
|
||||
@@ -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()
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user