Merge latest origin/main into brennanb2025/mobile-bound-host-autoconnect

This commit is contained in:
Brennan Benson
2026-08-05 21:25:52 -07:00
33 changed files with 1039 additions and 164 deletions
@@ -275,6 +275,16 @@ describe('orchestration skill guidance', () => {
)
})
it('documents per-invocation model and effort for supervised workers', () => {
const workerLoop = getSection(readSkill(), 'Preferred Supervised Worker Loop')
expect(workerLoop).toContain('opaque provider model id with `--model`')
expect(workerLoop).toContain('`--effort` requires `--model`')
expect(workerLoop).toContain('neither option can combine with `--terminal`')
expect(workerLoop).toContain('--agent claude --model aws-bedrock-opus-5 --effort high --json')
expect(workerLoop).toContain('`launch.requested` and `launch.effective`')
})
it('never authorizes release from idle, timeout, or worker-side triggers', () => {
const skill = readSkill()
const workerLoop = getSection(skill, 'Preferred Supervised Worker Loop')
+8
View File
@@ -192,6 +192,14 @@ orca orchestration worker-start --task <task_b> --worktree current --agent claud
`current` and exact existing worktrees create a fresh agent terminal and do not rerun setup. Reuse an existing agent only with `--terminal <handle>`.
For a per-invocation Claude, Codex, or Cursor launch, pass an opaque provider model id with `--model`; add `--effort` only when that agent/model supports the level. These options apply only to fresh agent terminals, override general agent default arguments, and are reported under `launch.requested` and `launch.effective` in the receipt:
```bash
orca orchestration worker-start --task <task_id> --worktree current --agent claude --model aws-bedrock-opus-5 --effort high --json
```
`--effort` requires `--model`, and neither option can combine with `--terminal`. A connected worker server must advertise launch-preference support before Orca forwards either option.
For a new worktree, setup runs by default and agent-first creation reuses the returned startup agent terminal:
```bash
File diff suppressed because one or more lines are too long
@@ -8,6 +8,7 @@ vi.mock('../selectors', () => ({ getTerminalHandle: vi.fn() }))
import { ORCHESTRATION_HANDLERS } from './orchestration'
import { printResult } from '../format'
import { ORCHESTRATION_WORKER_LAUNCH_PREFERENCES_RUNTIME_CAPABILITY } from '../../shared/protocol-version'
describe('orchestration worker-start CLI contract', () => {
beforeEach(() => {
@@ -84,6 +85,63 @@ describe('orchestration worker-start CLI contract', () => {
expect(process.exitCode).toBeUndefined()
})
it('capability-gates and forwards per-invocation launch preferences', async () => {
callMock
.mockResolvedValueOnce({
result: {
capabilities: [ORCHESTRATION_WORKER_LAUNCH_PREFERENCES_RUNTIME_CAPABILITY]
}
})
.mockResolvedValueOnce({
result: {
runId: 'run_1',
taskId: 'task_1',
dispatchId: 'ctx_1',
state: 'ready',
effects: [],
residualResources: []
}
})
await invokeWorkerStart(
new Map<string, string | boolean>([
['task', 'task_1'],
['agent', 'claude'],
['model', 'aws-bedrock-opus-5'],
['effort', 'high'],
['from', 'term_coord']
])
)
expect(callMock).toHaveBeenNthCalledWith(1, 'status.get')
expect(callMock).toHaveBeenNthCalledWith(
2,
'orchestration.workerStart',
expect.objectContaining({
agent: 'claude',
model: 'aws-bedrock-opus-5',
effort: 'high'
})
)
})
it('fails before worker-start when the runtime would strip launch preferences', async () => {
callMock.mockResolvedValueOnce({ result: { capabilities: [] } })
await expect(
invokeWorkerStart(
new Map<string, string | boolean>([
['task', 'task_1'],
['agent', 'codex'],
['model', 'gpt-5.6-sol'],
['from', 'term_coord']
])
)
).rejects.toMatchObject({ code: 'incompatible_runtime' })
expect(callMock).toHaveBeenCalledTimes(1)
})
it('sets an unsuccessful exit code for failed and unknown receipts', async () => {
callMock.mockResolvedValue({
result: {
+19 -1
View File
@@ -20,7 +20,8 @@ import type {
OrchestrationWorkerReadSource
} from '../../shared/orchestration-worker-output'
import type { NativeChatMessage } from '../../shared/native-chat-types'
import type { RuntimeTerminalRead } from '../../shared/runtime-types'
import type { RuntimeStatus, RuntimeTerminalRead } from '../../shared/runtime-types'
import { ORCHESTRATION_WORKER_LAUNCH_PREFERENCES_RUNTIME_CAPABILITY } from '../../shared/protocol-version'
import { orchestrationMigrationData } from '../../shared/orchestration-rpc-contract'
import { ORCHESTRATION_RUN_PAGE_LIMIT } from '../../shared/orchestration-run-pagination'
import {
@@ -852,6 +853,21 @@ export const ORCHESTRATION_HANDLERS: Record<string, CommandHandler> = {
},
'orchestration worker-start': async ({ flags, client, cwd, json }) => {
const model = getOptionalStringFlag(flags, 'model')
const effort = getOptionalStringFlag(flags, 'effort')
if (model || effort) {
const status = await client.call<RuntimeStatus>('status.get')
if (
!status.result.capabilities?.includes(
ORCHESTRATION_WORKER_LAUNCH_PREFERENCES_RUNTIME_CAPABILITY
)
) {
throw new RuntimeClientError(
'incompatible_runtime',
'The connected Orca runtime does not support worker model or effort overrides. Update or restart Orca and try again.'
)
}
}
const result = await callMutation<{
runId: string
taskId: string
@@ -873,6 +889,8 @@ export const ORCHESTRATION_HANDLERS: Record<string, CommandHandler> = {
comment: getOptionalStringFlag(flags, 'comment'),
setup: getOptionalStringFlag(flags, 'setup'),
agent: getOptionalStringFlag(flags, 'agent'),
model,
effort,
terminal: getOptionalStringFlag(flags, 'terminal'),
retryOf: getOptionalStringFlag(flags, 'retry-of'),
timeoutMs: getOptionalPositiveIntegerValueFlag(flags, 'timeout-ms'),
+2
View File
@@ -552,6 +552,8 @@ export function formatFlagHelp(flag: string): string {
local: '--local Target the current project instead of the global install',
skill: '--skill <name> Bundled skill to act on; repeat for several',
mode: '--mode <mode> Mode such as edit, diff, or both',
model: '--model <id> Provider model id for a new agent launch',
effort: '--effort <level> Reasoning effort for the selected model',
'mouse-button': '--mouse-button <btn> Mouse button: left, right, or middle',
modifiers: '--modifiers <chord> Modifier keys held only for this click',
name: '--name <name> Name for the new worktree or automation',
+4 -1
View File
@@ -5,7 +5,7 @@ export const ORCHESTRATION_WORKER_COMMAND_SPECS: CommandSpec[] = [
path: ['orchestration', 'worker-start'],
summary: 'Start one supervised worker on the Run home or a connected Orca server',
usage:
'orca orchestration worker-start --task <task_id> [--on <saved-environment>] [--worktree <current|selector|new-child|new-top-level>] (--agent <agent> | --terminal <handle>) [--name <name>] [--repo <selector>] [--base-branch <ref>] [--display-name <text>] [--comment <text>] [--setup <run|skip|inherit>] [--retry-of <dispatch_id>] [--timeout-ms <n>] [--run <run_id>] [--from <handle>] [--retry-request <id>] [--json]',
'orca orchestration worker-start --task <task_id> [--on <saved-environment>] [--worktree <current|selector|new-child|new-top-level>] (--agent <agent> | --terminal <handle>) [--model <id>] [--effort <level>] [--name <name>] [--repo <selector>] [--base-branch <ref>] [--display-name <text>] [--comment <text>] [--setup <run|skip|inherit>] [--retry-of <dispatch_id>] [--timeout-ms <n>] [--run <run_id>] [--from <handle>] [--retry-request <id>] [--json]',
allowedFlags: [
...GLOBAL_FLAGS,
'task',
@@ -18,6 +18,8 @@ export const ORCHESTRATION_WORKER_COMMAND_SPECS: CommandSpec[] = [
'comment',
'setup',
'agent',
'model',
'effort',
'terminal',
'retry-of',
'timeout-ms',
@@ -27,6 +29,7 @@ export const ORCHESTRATION_WORKER_COMMAND_SPECS: CommandSpec[] = [
],
notes: [
'Current and existing worktrees never rerun setup; a fresh agent terminal is created unless --terminal is explicit.',
'--model supports Claude, Codex, and Cursor opaque provider model ids; --effort requires --model. Neither can combine with --terminal.',
'New worktrees use agent-first creation and default --setup to run. Repository start-immediately runs setup beside the agent; wait-for-setup gates agent readiness and task input.',
'Creation flags (--name, --repo, --base-branch, --display-name, --comment, --setup) are rejected for current/existing worktrees. Use exact --repo on the selected server; project/host convenience routing remains on worktree create.',
'--on selects only the worker server; the Run and this command remain on the current Orca server.',
+16 -2
View File
@@ -1293,6 +1293,7 @@ type TerminalCreateOptions = {
// 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
launchPreferences?: AgentLaunchPreferences
terminalColorQueryReplies?: TerminalOscColorQueryReplyColors
viewMode?: 'terminal' | 'chat'
startupCommandDelivery?: WorktreeStartupLaunch['startupCommandDelivery']
@@ -20956,7 +20957,8 @@ export class OrcaRuntimeService {
private buildStartupForAgent(
repo: Repo,
agent: TuiAgent,
prompt: string | undefined
prompt: string | undefined,
launchPreferences?: AgentLaunchPreferences
): { agent: TuiAgent; startup: WorktreeStartupLaunch; followup?: WorktreeStartupFollowup } {
if (!this.store) {
throw new Error('runtime_unavailable')
@@ -20974,12 +20976,15 @@ export class OrcaRuntimeService {
isRemote,
terminalWindowsShell: settings.terminalWindowsShell
})
const sessionOptions = this.toAgentSessionOptions(launchPreferences)
const startupPlan = buildAgentStartupPlan({
agent,
prompt: prompt ?? '',
cmdOverrides: settings.agentCmdOverrides ?? {},
agentArgs: resolveTuiAgentLaunchArgs(agent, settings.agentDefaultArgs),
agentEnv: resolveTuiAgentLaunchEnv(agent, settings.agentDefaultEnv),
sessionOptions,
sessionOptionsOverrideAgentArgs: Boolean(sessionOptions),
platform: agentLaunchPlatform,
shell: queuedShell,
isRemote,
@@ -21418,6 +21423,7 @@ export class OrcaRuntimeService {
observeSetupCompletion?: boolean
createdWithAgent?: TuiAgent
startupAgent?: TuiAgent
startupLaunchPreferences?: AgentLaunchPreferences
startupPrompt?: string
pendingFirstAgentMessageRename?: boolean
automationProvenance?: AutomationWorkspaceProvenance
@@ -21450,7 +21456,12 @@ export class OrcaRuntimeService {
}
const agentStartup =
!args.startup && args.startupAgent
? this.buildStartupForAgent(repo, args.startupAgent, args.startupPrompt)
? this.buildStartupForAgent(
repo,
args.startupAgent,
args.startupPrompt,
args.startupLaunchPreferences
)
: null
const draftStartup =
!args.startup && !agentStartup && args.startupDraft
@@ -24619,12 +24630,15 @@ export class OrcaRuntimeService {
return opts
}
const sessionOptions = this.toAgentSessionOptions(opts.launchPreferences)
const startupPlan = buildAgentStartupPlan({
agent,
prompt: '',
cmdOverrides: settings.agentCmdOverrides ?? {},
agentArgs: resolveTuiAgentLaunchArgs(agent, settings.agentDefaultArgs),
agentEnv: resolveTuiAgentLaunchEnv(agent, settings.agentDefaultEnv),
sessionOptions,
sessionOptionsOverrideAgentArgs: Boolean(sessionOptions),
platform,
shell: queuedShell,
isRemote,
@@ -11,6 +11,14 @@ import type { OrcaRuntimeService } from '../../orca-runtime'
import type { OrchestrationDb } from '../../orchestration/db'
import { OrchestrationError } from '../../orchestration/orchestration-error'
import type { WorkerStartInput } from './orchestration-worker-start-schema'
import {
assertWorkerLaunchPreferencesRuntimeSupported,
assertWorkerLaunchPreferencesCreateTerminal,
createPendingWorkerLaunchReceipt,
resolveFederatedWorkerLaunchReceipt,
type OrchestrationWorkerLaunchReceipt
} from './orchestration-worker-launch-preferences'
import { validateFederatedWorkerStartPlacement } from './orchestration-worker-start-validation'
export async function startFederatedWorker(args: {
params: WorkerStartInput
@@ -40,7 +48,13 @@ export async function startFederatedWorker(args: {
)
}
const createsWorktree = worktree === 'new-top-level'
validateRemoteWorkerStart(params, createsWorktree)
assertWorkerLaunchPreferencesCreateTerminal(params)
validateFederatedWorkerStartPlacement(params, createsWorktree)
const requestedLaunch = createPendingWorkerLaunchReceipt({
agent: isTuiAgent(params.agent) ? params.agent : null,
model: params.model,
effort: params.effort
})
const server = runtime.resolveOrchestrationWorkerServer(params.on as string)
const status = (await runtime.callOrchestrationWorkerServer(
@@ -62,6 +76,12 @@ export async function startFederatedWorker(args: {
`Connected server ${server.name} does not support orchestration federation.`
)
}
assertWorkerLaunchPreferencesRuntimeSupported({
model: params.model,
effort: params.effort,
capabilities: status.capabilities,
serverName: server.name
})
const federationProtocolVersion = status.capabilities?.includes(
ORCHESTRATION_FEDERATION_CONTROL_MAIL_RUNTIME_CAPABILITY
)
@@ -81,6 +101,7 @@ export async function startFederatedWorker(args: {
baseBranch: params.baseBranch ?? null,
terminal: params.terminal ?? null,
agent: params.agent ?? null,
launch: requestedLaunch,
timeoutMs: params.timeoutMs ?? 60_000,
setup: setupDecision,
setupSource: createsWorktree
@@ -122,6 +143,8 @@ export async function startFederatedWorker(args: {
: undefined,
terminal: params.terminal,
agent: params.agent,
model: params.model,
effort: params.effort,
timeoutMs: params.timeoutMs,
devMode: params.devMode
},
@@ -134,6 +157,11 @@ export async function startFederatedWorker(args: {
'The worker server returned a different Dispatch attachment.'
)
}
const launch = resolveFederatedWorkerLaunchReceipt(
remote.launch,
requestedLaunch,
remote.state === 'ready'
)
if (remote.state === 'ready' && remote.worktreeId && remote.terminalHandle) {
db.updateFederatedDispatchResources({
dispatchId: started.dispatch.id,
@@ -160,6 +188,7 @@ export async function startFederatedWorker(args: {
stage: readyWorker.stage,
server: { environmentId: server.environmentId, name: server.name },
setup: remote.setup,
launch,
timeoutMs: params.timeoutMs ?? 60_000,
effects: remote.effects ?? [],
residualResources: remote.residualResources ?? []
@@ -171,7 +200,7 @@ export async function startFederatedWorker(args: {
remote.failedStage ?? 'remote_attach',
remote.lastError ?? 'The worker server reported an unknown start outcome.'
)
return federatedUnknownReceipt(worker, task.id, server.name)
return federatedUnknownReceipt(worker, task.id, server.name, launch)
}
const worker = db.failWorkerStart(
started.dispatch.id,
@@ -188,6 +217,7 @@ export async function startFederatedWorker(args: {
failedStage: worker.stage,
lastError: worker.last_error,
setup: remote.setup,
launch,
effects: remote.effects ?? [],
residualResources: remote.residualResources ?? []
}
@@ -204,12 +234,13 @@ export async function startFederatedWorker(args: {
server: { environmentId: server.environmentId, name: server.name },
failedStage: worker.stage,
lastError: worker.last_error,
launch: requestedLaunch,
effects: [],
residualResources: []
}
}
const worker = db.markWorkerStartUnknown(started.dispatch.id, 'remote_attach', reason)
return federatedUnknownReceipt(worker, task.id, server.name)
return federatedUnknownReceipt(worker, task.id, server.name, requestedLaunch)
}
}
@@ -220,45 +251,13 @@ type RemoteStartReceipt = {
worktreeId?: string
terminalHandle?: string
setup?: { state: string }
launch?: OrchestrationWorkerLaunchReceipt
effects?: unknown[]
residualResources?: unknown[]
failedStage?: string
lastError?: string
}
function validateRemoteWorkerStart(params: WorkerStartInput, createsWorktree: boolean): void {
if (createsWorktree && (!params.name || !params.repo)) {
throw new OrchestrationError(
'invalid_argument',
'Remote new-top-level requires --name and an explicit --repo from remote discovery.'
)
}
if (createsWorktree && params.terminal) {
throw new OrchestrationError(
'invalid_argument',
'--terminal cannot combine with remote new-worktree creation.'
)
}
if (!createsWorktree && (params.name || params.repo || params.baseBranch || params.setup)) {
throw new OrchestrationError(
'invalid_argument',
'Creation and setup options apply only to remote new-top-level worktrees.'
)
}
if (params.terminal && params.agent) {
throw new OrchestrationError(
'invalid_argument',
'--terminal reuses an existing agent and cannot combine with --agent.'
)
}
if (!params.terminal && (!params.agent || !isTuiAgent(params.agent))) {
throw new OrchestrationError(
'agent_unconfigured',
'A configured --agent is required when remote worker-start creates a terminal.'
)
}
}
function isKnownRemoteStartFailure(code: string): boolean {
return [
'invalid_argument',
@@ -272,7 +271,8 @@ function isKnownRemoteStartFailure(code: string): boolean {
function federatedUnknownReceipt(
worker: { dispatch_id: string; state: string; stage: string; last_error: string | null },
taskId: string,
serverName: string
serverName: string,
launch: OrchestrationWorkerLaunchReceipt
): unknown {
return {
taskId,
@@ -280,6 +280,7 @@ function federatedUnknownReceipt(
state: 'outcome_unknown',
stage: worker.stage,
server: { name: serverName },
launch,
failedStage: worker.stage,
lastError: worker.last_error,
effects: [],
@@ -61,7 +61,9 @@ describe('federated worker agent launch', () => {
taskSpec: 'remote cursor worker',
protocolVersion: 1,
worktree: 'id:repo::remote-worktree',
agent: 'cursor'
agent: 'cursor',
model: 'gpt-5.3-codex',
effort: 'high'
}),
{
runtime,
@@ -72,14 +74,28 @@ describe('federated worker agent launch', () => {
payloadHash: 'remote_payload'
}
}
)) as { state: string; failedStage?: string; lastError?: string }
)) as {
state: string
failedStage?: string
lastError?: string
launch: unknown
}
// Why: assert the worker actually reached ready — a spy-only assertion would
// stay green even if every stage after terminal_create regressed.
expect(result).toMatchObject({ state: 'ready' })
expect(result).toMatchObject({
state: 'ready',
launch: {
requested: { agent: 'cursor', model: 'gpt-5.3-codex', effort: 'high' },
effective: { agent: 'cursor', model: 'gpt-5.3-codex', effort: 'high' }
}
})
expect(createTerminal).toHaveBeenCalledWith(
'id:repo::remote-worktree',
expect.objectContaining({ startupAgent: 'cursor' })
expect.objectContaining({
startupAgent: 'cursor',
launchPreferences: { model: 'gpt-5.3-codex', effort: 'high' }
})
)
expect(createTerminal).toHaveBeenCalledWith(
'id:repo::remote-worktree',
@@ -1,6 +1,7 @@
import type { OrchestrationDb } from '../../orchestration/db'
import { isFederationEffectUnknown } from './orchestration-federation-effects'
import type { WorkerSetupReceipt } from './orchestration-worker-topology'
import type { OrchestrationWorkerLaunchReceipt } from './orchestration-worker-launch-preferences'
export function failFederatedAttachmentWithReceipt(args: {
db: OrchestrationDb
@@ -9,6 +10,7 @@ export function failFederatedAttachmentWithReceipt(args: {
failedStage: string
error: unknown
setup: WorkerSetupReceipt
launch: OrchestrationWorkerLaunchReceipt
}): unknown {
const reason = args.error instanceof Error ? args.error.message : String(args.error)
const unknown = isFederationEffectUnknown(args.error, args.failedStage)
@@ -26,6 +28,7 @@ export function failFederatedAttachmentWithReceipt(args: {
failedStage: args.failedStage,
lastError: reason,
setup: args.setup,
launch: args.launch,
effects: JSON.parse(attachment.effects) as unknown[],
residualResources: JSON.parse(attachment.residual_resources) as unknown[]
}
@@ -1,5 +1,6 @@
import { z } from 'zod'
import { OptionalFiniteNumber, OptionalString, requiredString } from '../schemas'
import { OptionalWorkerLaunchPreference } from './orchestration-worker-start-schema'
export const FederationAttachStartParams = z.object({
dispatchId: requiredString('Missing Dispatch ID'),
@@ -16,6 +17,10 @@ export const FederationAttachStartParams = z.object({
setupSource: z.enum(['explicit_request', 'orchestration_default']).optional(),
terminal: OptionalString,
agent: OptionalString,
model: OptionalWorkerLaunchPreference,
effort: OptionalWorkerLaunchPreference,
timeoutMs: OptionalFiniteNumber,
devMode: z.boolean().optional()
})
export type FederationAttachStartInput = z.infer<typeof FederationAttachStartParams>
@@ -0,0 +1,25 @@
import { ORCHESTRATION_CONTRACT_VERSION } from '../../../../shared/protocol-version'
import type { RpcRequest } from '../core'
export function createFederationWorkerStartRequest(
taskId: string,
overrides: Record<string, unknown> = {}
): RpcRequest {
return {
id: 'rpc_worker_start',
authToken: 'coordinator-token',
orchestrationContractVersion: ORCHESTRATION_CONTRACT_VERSION,
orchestrationRequestId: 'request_windows_worker',
method: 'orchestration.workerStart',
params: {
task: taskId,
from: 'term_coord',
on: 'windows',
worktree: 'new-top-level',
repo: 'id:windows-repo',
name: 'windows-audit',
agent: 'codex',
...overrides
}
}
}
@@ -8,8 +8,8 @@ import { OrcaRuntimeService } from '../../orca-runtime'
import { OrchestrationDb } from '../../orchestration/db'
import type { OrchestrationEnvironmentTransport } from '../../orchestration/environment-transport'
import { RpcDispatcher } from '../dispatcher'
import type { RpcRequest } from '../core'
import { ORCHESTRATION_METHODS } from './orchestration'
import { createFederationWorkerStartRequest as startRequest } from './orchestration-federation-test-request'
describe('orchestration federation', () => {
const databases: OrchestrationDb[] = []
@@ -97,26 +97,6 @@ describe('orchestration federation', () => {
return homeDb.createTask({ spec: 'Audit Windows behavior', runId: run.id })
}
function startRequest(taskId: string, overrides: Record<string, unknown> = {}): RpcRequest {
return {
id: 'rpc_worker_start',
authToken: 'coordinator-token',
orchestrationContractVersion: ORCHESTRATION_CONTRACT_VERSION,
orchestrationRequestId: 'request_windows_worker',
method: 'orchestration.workerStart',
params: {
task: taskId,
from: 'term_coord',
on: 'windows',
worktree: 'new-top-level',
repo: 'id:windows-repo',
name: 'windows-audit',
agent: 'codex',
...overrides
}
}
}
function configureWorkerRuntime(runtime: OrcaRuntimeService): void {
vi.spyOn(runtime, 'validateOrchestrationAgentLauncher').mockImplementation(() => {})
vi.spyOn(runtime, 'showRepo').mockResolvedValue({
@@ -230,6 +210,25 @@ describe('orchestration federation', () => {
)
})
it('does not report remotely rejected preferences as effective', async () => {
const task = createHomeTask()
const response = await homeDispatcher.dispatch(
startRequest(task.id, { agent: 'grok', model: 'unsupported-model' })
)
expect(response).toMatchObject({
ok: true,
result: {
state: 'failed',
launch: {
requested: { agent: 'grok', model: 'unsupported-model', effort: null },
effective: null
}
}
})
})
it('preserves wait-for-setup gating on the connected worker server', async () => {
vi.mocked(workerRuntime.createManagedWorktree).mockResolvedValueOnce({
worktree: { id: 'repo::windows-worktree', repoId: 'repo' },
@@ -1,4 +1,3 @@
import { isTuiAgent } from '../../../../shared/tui-agent-config'
import type { TuiAgent } from '../../../../shared/types'
import { buildDispatchPreamble } from '../../orchestration/preamble'
import { OrchestrationError } from '../../orchestration/orchestration-error'
@@ -18,6 +17,7 @@ import {
} from './orchestration-federation-setup'
import { FederationAttachStartParams } from './orchestration-federation-start-schema'
import { failFederatedAttachmentWithReceipt } from './orchestration-federation-start-receipt'
import { prepareFederationAttachmentWorkerStart } from './orchestration-worker-start-validation'
export const ORCHESTRATION_FEDERATION_ATTACH_METHODS: RpcMethod[] = [
defineMethod({
@@ -37,43 +37,11 @@ export const ORCHESTRATION_FEDERATION_ATTACH_METHODS: RpcMethod[] = [
)
}
const createsWorktree = params.worktree === 'new-top-level'
if (createsWorktree && (!params.name || !params.repo)) {
throw new OrchestrationError(
'invalid_argument',
'A remote new-top-level worktree requires --name and an explicit --repo.'
)
}
if (createsWorktree && params.terminal) {
throw new OrchestrationError(
'invalid_argument',
'--terminal cannot combine with remote new-worktree creation.'
)
}
if (
!createsWorktree &&
(params.name || params.repo || params.baseBranch || params.setup || params.setupSource)
) {
throw new OrchestrationError(
'invalid_argument',
'Creation and setup options apply only to remote new-top-level worktrees.'
)
}
if (params.terminal && params.agent) {
throw new OrchestrationError(
'invalid_argument',
'--terminal reuses an existing agent and cannot combine with --agent.'
)
}
const agent = params.agent
if (!params.terminal && (!agent || !isTuiAgent(agent))) {
throw new OrchestrationError(
'agent_unconfigured',
'A configured --agent is required when federated worker-start creates a terminal.'
)
}
if (agent) {
runtime.validateOrchestrationAgentLauncher(agent as TuiAgent)
}
const { agent, launch } = prepareFederationAttachmentWorkerStart({
params,
createsWorktree,
runtime
})
if (createsWorktree) {
await assertOrchestrationWorktreeCreationSupported({
runtime,
@@ -126,6 +94,7 @@ export const ORCHESTRATION_FEDERATION_ATTACH_METHODS: RpcMethod[] = [
observeSetupCompletion: true,
createdWithAgent: agent as TuiAgent,
startupAgent: agent as TuiAgent,
...(launch.preferences ? { startupLaunchPreferences: launch.preferences } : {}),
activate: false,
lineage: { noParent: true }
})
@@ -196,6 +165,7 @@ export const ORCHESTRATION_FEDERATION_ATTACH_METHODS: RpcMethod[] = [
// 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,
...(launch.preferences ? { launchPreferences: launch.preferences } : {}),
title: `worker-${params.taskId}`,
presentation: 'background'
})
@@ -284,6 +254,7 @@ export const ORCHESTRATION_FEDERATION_ATTACH_METHODS: RpcMethod[] = [
worktreeId: worktree.id,
terminalHandle,
setup,
launch: launch.receipt,
effects,
residualResources: []
}
@@ -294,7 +265,8 @@ export const ORCHESTRATION_FEDERATION_ATTACH_METHODS: RpcMethod[] = [
runtimeEpoch: runtime.getRuntimeId(),
failedStage,
error,
setup
setup,
launch: launch.receipt
})
}
}
@@ -0,0 +1,135 @@
import { describe, expect, it } from 'vitest'
import { ORCHESTRATION_WORKER_LAUNCH_PREFERENCES_RUNTIME_CAPABILITY } from '../../../../shared/protocol-version'
import {
assertWorkerLaunchPreferencesCreateTerminal,
assertWorkerLaunchPreferencesRuntimeSupported,
createPendingWorkerLaunchReceipt,
resolveFederatedWorkerLaunchReceipt,
resolveWorkerLaunchPreferences
} from './orchestration-worker-launch-preferences'
import { WorkerStartParams } from './orchestration-worker-start-schema'
describe('orchestration worker launch preferences', () => {
it('passes an opaque Claude model and portable effort through the shared catalog', () => {
expect(
resolveWorkerLaunchPreferences({
agent: 'claude',
model: 'aws-bedrock-opus-5',
effort: 'high'
})
).toEqual({
preferences: { model: 'aws-bedrock-opus-5', effort: 'high' },
receipt: {
requested: { agent: 'claude', model: 'aws-bedrock-opus-5', effort: 'high' },
effective: { agent: 'claude', model: 'aws-bedrock-opus-5', effort: 'high' }
}
})
})
it('does not invent an effort when only a model is requested', () => {
expect(
resolveWorkerLaunchPreferences({ agent: 'codex', model: 'gpt-5.6-sol' }).preferences
).toEqual({ model: 'gpt-5.6-sol' })
})
it('rejects model-specific effort combinations the catalog disproves', () => {
expect(() =>
resolveWorkerLaunchPreferences({
agent: 'codex',
model: 'gpt-5.6-luna',
effort: 'xhigh'
})
).toThrow('does not support effort xhigh')
})
it('rejects effort without a model', () => {
expect(() => resolveWorkerLaunchPreferences({ agent: 'codex', effort: 'high' })).toThrow(
'--effort requires --model'
)
})
it('rejects model selection for agents without a launch catalog', () => {
expect(() =>
resolveWorkerLaunchPreferences({ agent: 'grok', model: 'grok-code-fast-1' })
).toThrow('does not support launch-time model selection')
})
it('does not expose deprecated Gemini model selection to worker-start', () => {
expect(() =>
resolveWorkerLaunchPreferences({ agent: 'gemini', model: 'gemini-3-pro-preview' })
).toThrow('does not support launch-time model selection')
})
it('rejects preferences when reusing an existing terminal', () => {
expect(() =>
assertWorkerLaunchPreferencesCreateTerminal({
terminal: 'term_existing',
model: 'gpt-5.6-sol'
})
).toThrow('cannot be applied when reusing an existing terminal')
})
it('requires remote capability support only for explicit preferences', () => {
expect(() =>
assertWorkerLaunchPreferencesRuntimeSupported({
model: 'gpt-5.6-sol',
capabilities: [],
serverName: 'windows'
})
).toThrow('does not support worker model or effort overrides')
expect(() =>
assertWorkerLaunchPreferencesRuntimeSupported({
capabilities: [],
serverName: 'windows'
})
).not.toThrow()
expect(() =>
assertWorkerLaunchPreferencesRuntimeSupported({
model: 'gpt-5.6-sol',
capabilities: [ORCHESTRATION_WORKER_LAUNCH_PREFERENCES_RUNTIME_CAPABILITY],
serverName: 'windows'
})
).not.toThrow()
})
it('uses the requested launch receipt when an older worker omits it', () => {
const requested = createPendingWorkerLaunchReceipt({
agent: 'codex',
model: 'gpt-5.6-sol',
effort: 'high'
})
expect(resolveFederatedWorkerLaunchReceipt(undefined, requested, true)).toEqual({
requested: requested.requested,
effective: requested.requested
})
expect(resolveFederatedWorkerLaunchReceipt(undefined, requested, false)).toBe(requested)
})
it.each([' custom-model', 'custom-model '])(
'rejects model ids with surrounding whitespace: %j',
(model) => {
expect(WorkerStartParams.safeParse({ task: 'task_1', agent: 'codex', model }).success).toBe(
false
)
}
)
it('bounds opaque launch preferences', () => {
expect(
WorkerStartParams.safeParse({
task: 'task_1',
agent: 'codex',
model: 'm'.repeat(513)
}).success
).toBe(false)
expect(
WorkerStartParams.safeParse({
task: 'task_1',
agent: 'codex',
model: 'custom-model',
effort: 'e'.repeat(513)
}).success
).toBe(false)
})
})
@@ -0,0 +1,158 @@
import type { AgentLaunchPreferences } from '../../../../shared/agent-session-host-authority'
import {
findCatalogModel,
findCatalogOption,
getAgentSessionOptionCatalog
} from '../../../../shared/agent-session-option-catalog'
import { resolveAgentSessionOptionLaunch } from '../../../../shared/agent-session-option-launch'
import { ORCHESTRATION_WORKER_LAUNCH_PREFERENCES_RUNTIME_CAPABILITY } from '../../../../shared/protocol-version'
import type { TuiAgent } from '../../../../shared/types'
import { OrchestrationError } from '../../orchestration/orchestration-error'
export type OrchestrationWorkerLaunchSelection = {
agent: TuiAgent | null
model: string | null
effort: string | null
}
export type OrchestrationWorkerLaunchReceipt = {
requested: OrchestrationWorkerLaunchSelection
effective: OrchestrationWorkerLaunchSelection | null
}
export function createWorkerLaunchReceipt(args: {
agent: TuiAgent | null
model?: string
effort?: string
}): OrchestrationWorkerLaunchReceipt {
const selection = {
agent: args.agent,
model: args.model ?? null,
effort: args.effort ?? null
}
return { requested: selection, effective: { ...selection } }
}
export function createPendingWorkerLaunchReceipt(args: {
agent: TuiAgent | null
model?: string
effort?: string
}): OrchestrationWorkerLaunchReceipt {
return {
requested: {
agent: args.agent,
model: args.model ?? null,
effort: args.effort ?? null
},
effective: null
}
}
export function resolveWorkerLaunchPreferences(args: {
agent: TuiAgent
model?: string
effort?: string
}): {
preferences: AgentLaunchPreferences | undefined
receipt: OrchestrationWorkerLaunchReceipt
} {
if (args.effort && !args.model) {
throw new OrchestrationError('invalid_argument', '--effort requires --model.')
}
if (!args.model) {
return {
preferences: undefined,
receipt: createWorkerLaunchReceipt({ agent: args.agent })
}
}
const catalog = getAgentSessionOptionCatalog(args.agent)
if (!catalog?.supportsWorkerLaunchPreferences || !catalog.modelApply.launchArgs) {
throw new OrchestrationError(
'invalid_argument',
`Agent ${args.agent} does not support launch-time model selection.`
)
}
if (args.effort) {
const model = findCatalogModel(catalog, args.model)
const option =
findCatalogOption(model, 'effort') ??
(!model
? catalog.unknownModelOptions?.find((candidate) => candidate.id === 'effort')
: undefined)
if (
option?.kind.type !== 'select' ||
!option.kind.choices.some((choice) => choice.value === args.effort)
) {
throw new OrchestrationError(
'invalid_argument',
`Agent ${args.agent} model ${args.model} does not support effort ${args.effort}.`
)
}
}
const requested = {
model: args.model,
...(args.effort ? { effort: args.effort } : {})
}
const resolved = resolveAgentSessionOptionLaunch(args.agent, requested, [], false)
if (
resolved.appliedValues.model !== args.model ||
resolved.appliedValues.effort !== args.effort
) {
throw new OrchestrationError(
'invalid_argument',
`Agent ${args.agent} cannot apply the requested worker launch preferences.`
)
}
const preferences: AgentLaunchPreferences = requested
return {
preferences,
receipt: createWorkerLaunchReceipt({ agent: args.agent, ...preferences })
}
}
export function assertWorkerLaunchPreferencesCreateTerminal(args: {
terminal?: string
model?: string
effort?: string
}): void {
if (args.terminal && (args.model || args.effort)) {
throw new OrchestrationError(
'invalid_argument',
'--model and --effort cannot be applied when reusing an existing terminal.'
)
}
}
export function assertWorkerLaunchPreferencesRuntimeSupported(args: {
model?: string
effort?: string
capabilities?: readonly string[]
serverName: string
}): void {
if (
(args.model || args.effort) &&
!args.capabilities?.includes(ORCHESTRATION_WORKER_LAUNCH_PREFERENCES_RUNTIME_CAPABILITY)
) {
throw new OrchestrationError(
'capability_unsupported',
`Connected server ${args.serverName} does not support worker model or effort overrides.`
)
}
}
export function resolveFederatedWorkerLaunchReceipt(
remote: OrchestrationWorkerLaunchReceipt | undefined,
requested: OrchestrationWorkerLaunchReceipt,
remoteReady: boolean
): OrchestrationWorkerLaunchReceipt {
if (remote) {
return remote
}
return remoteReady
? { requested: requested.requested, effective: { ...requested.requested } }
: requested
}
@@ -3,6 +3,7 @@ import {
isUnknownWorkerStartOutcome,
type WorkerSetupReceipt
} from './orchestration-worker-topology'
import type { OrchestrationWorkerLaunchReceipt } from './orchestration-worker-launch-preferences'
export function failWorkerStartWithReceipt(args: {
db: OrchestrationDb
@@ -12,6 +13,7 @@ export function failWorkerStartWithReceipt(args: {
failedStage: string
error: unknown
setup: WorkerSetupReceipt
launch: OrchestrationWorkerLaunchReceipt
}): unknown {
const reason = args.error instanceof Error ? args.error.message : String(args.error)
const unknown = isUnknownWorkerStartOutcome(args.error, args.failedStage)
@@ -27,6 +29,7 @@ export function failWorkerStartWithReceipt(args: {
failedStage: args.failedStage,
lastError: reason,
setup: args.setup,
launch: args.launch,
effects: JSON.parse(worker.effects) as unknown[],
residualResources: JSON.parse(worker.residual_resources) as unknown[],
...(unknown
@@ -1,6 +1,13 @@
import { z } from 'zod'
import { OptionalFiniteNumber, OptionalString, requiredString } from '../schemas'
export const OptionalWorkerLaunchPreference = z
.string()
.min(1)
.max(512)
.refine((value) => value === value.trim(), 'Surrounding whitespace is invalid')
.optional()
export const WorkerStartParams = z.object({
task: requiredString('Missing --task'),
on: OptionalString,
@@ -15,6 +22,8 @@ export const WorkerStartParams = z.object({
setup: z.enum(['run', 'skip', 'inherit']).optional(),
terminal: OptionalString,
agent: OptionalString,
model: OptionalWorkerLaunchPreference,
effort: OptionalWorkerLaunchPreference,
retryOf: OptionalString,
timeoutMs: OptionalFiniteNumber,
devMode: z.boolean().optional()
@@ -0,0 +1,164 @@
import { isTuiAgent } from '../../../../shared/tui-agent-config'
import type { TuiAgent } from '../../../../shared/types'
import type { OrcaRuntimeService } from '../../orca-runtime'
import { OrchestrationError } from '../../orchestration/orchestration-error'
import type { FederationAttachStartInput } from './orchestration-federation-start-schema'
import {
assertWorkerLaunchPreferencesCreateTerminal,
createWorkerLaunchReceipt,
resolveWorkerLaunchPreferences
} from './orchestration-worker-launch-preferences'
import type { WorkerStartInput } from './orchestration-worker-start-schema'
type WorkerStartLaunch = ReturnType<typeof resolveWorkerLaunchPreferences>
export function validateFederatedWorkerStartPlacement(
params: WorkerStartInput,
createsWorktree: boolean
): void {
if (createsWorktree && (!params.name || !params.repo)) {
throw new OrchestrationError(
'invalid_argument',
'Remote new-top-level requires --name and an explicit --repo from remote discovery.'
)
}
if (createsWorktree && params.terminal) {
throw new OrchestrationError(
'invalid_argument',
'--terminal cannot combine with remote new-worktree creation.'
)
}
if (!createsWorktree && (params.name || params.repo || params.baseBranch || params.setup)) {
throw new OrchestrationError(
'invalid_argument',
'Creation and setup options apply only to remote new-top-level worktrees.'
)
}
if (params.terminal && params.agent) {
throw new OrchestrationError(
'invalid_argument',
'--terminal reuses an existing agent and cannot combine with --agent.'
)
}
if (!params.terminal && (!params.agent || !isTuiAgent(params.agent))) {
throw new OrchestrationError(
'agent_unconfigured',
'A configured --agent is required when remote worker-start creates a terminal.'
)
}
}
export function prepareLocalWorkerStart(args: {
params: WorkerStartInput
createsWorktree: boolean
runtime: OrcaRuntimeService
}): { agent: TuiAgent | undefined; launch: WorkerStartLaunch } {
const { params, createsWorktree, runtime } = args
assertWorkerLaunchPreferencesCreateTerminal(params)
if (params.terminal && params.agent) {
throw new OrchestrationError(
'invalid_argument',
'--terminal reuses an existing agent and cannot combine with --agent.'
)
}
if (createsWorktree && params.terminal) {
throw new OrchestrationError(
'invalid_argument',
'--terminal cannot combine with new-worktree creation.'
)
}
if (createsWorktree && !params.name) {
throw new OrchestrationError('invalid_argument', 'New worktrees require --name.')
}
if (!createsWorktree && (params.name || params.repo || params.baseBranch || params.setup)) {
throw new OrchestrationError(
'invalid_argument',
'Creation and setup options apply only to new-child or new-top-level worktrees.'
)
}
return resolveWorkerStartAgent({
runtime,
terminal: params.terminal,
agent: params.agent,
model: params.model,
effort: params.effort,
missingAgentMessage: 'A configured --agent is required when worker-start creates a terminal.'
})
}
export function prepareFederationAttachmentWorkerStart(args: {
params: FederationAttachStartInput
createsWorktree: boolean
runtime: OrcaRuntimeService
}): { agent: TuiAgent | undefined; launch: WorkerStartLaunch } {
const { params, createsWorktree, runtime } = args
assertWorkerLaunchPreferencesCreateTerminal(params)
if (createsWorktree && (!params.name || !params.repo)) {
throw new OrchestrationError(
'invalid_argument',
'A remote new-top-level worktree requires --name and an explicit --repo.'
)
}
if (createsWorktree && params.terminal) {
throw new OrchestrationError(
'invalid_argument',
'--terminal cannot combine with remote new-worktree creation.'
)
}
if (
!createsWorktree &&
(params.name || params.repo || params.baseBranch || params.setup || params.setupSource)
) {
throw new OrchestrationError(
'invalid_argument',
'Creation and setup options apply only to remote new-top-level worktrees.'
)
}
if (params.terminal && params.agent) {
throw new OrchestrationError(
'invalid_argument',
'--terminal reuses an existing agent and cannot combine with --agent.'
)
}
return resolveWorkerStartAgent({
runtime,
terminal: params.terminal,
agent: params.agent,
model: params.model,
effort: params.effort,
missingAgentMessage:
'A configured --agent is required when federated worker-start creates a terminal.'
})
}
function resolveWorkerStartAgent(args: {
runtime: OrcaRuntimeService
terminal?: string
agent?: string
model?: string
effort?: string
missingAgentMessage: string
}): { agent: TuiAgent | undefined; launch: WorkerStartLaunch } {
if (!args.terminal && (!args.agent || !isTuiAgent(args.agent))) {
throw new OrchestrationError('agent_unconfigured', args.missingAgentMessage)
}
const agent = args.agent as TuiAgent | undefined
if (agent) {
args.runtime.validateOrchestrationAgentLauncher(agent)
return {
agent,
launch: resolveWorkerLaunchPreferences({
agent,
model: args.model,
effort: args.effort
})
}
}
return {
agent: undefined,
launch: {
preferences: undefined,
receipt: createWorkerLaunchReceipt({ agent: null })
}
}
}
@@ -1,3 +1,4 @@
import type { AgentLaunchPreferences } from '../../../../shared/agent-session-host-authority'
import type { TuiAgent } from '../../../../shared/types'
import type { OrcaRuntimeService } from '../../orca-runtime'
import type { OrchestrationDb } from '../../orchestration/db'
@@ -56,6 +57,7 @@ export async function createExistingWorktreeWorkerTerminal(args: {
runtime: OrcaRuntimeService
worktreeId: string
agent: TuiAgent
launchPreferences?: AgentLaunchPreferences
taskId: string
effects: WorkerEffect[]
}): Promise<{ handle: string; warning?: string }> {
@@ -64,6 +66,7 @@ export async function createExistingWorktreeWorkerTerminal(args: {
// desktop app while its CLI is `cursor-agent`. Let the runtime build the
// configured launcher instead of executing the raw id.
startupAgent: args.agent,
...(args.launchPreferences ? { launchPreferences: args.launchPreferences } : {}),
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.
@@ -117,6 +120,7 @@ export async function createWorkerWorktree(args: {
from: string
}
agent: TuiAgent
launchPreferences?: AgentLaunchPreferences
effects: WorkerEffect[]
}): Promise<{
worktree: Awaited<ReturnType<OrcaRuntimeService['showManagedWorktree']>>
@@ -139,6 +143,7 @@ export async function createWorkerWorktree(args: {
observeSetupCompletion: true,
createdWithAgent: args.agent,
startupAgent: args.agent,
...(args.launchPreferences ? { startupLaunchPreferences: args.launchPreferences } : {}),
activate: false,
lineage: {
parentWorktree: requestedWorktree === 'new-child' ? coordinatorWorktree.id : undefined,
@@ -157,6 +157,29 @@ describe('orchestration new-worktree workers', () => {
expect(runtime.createTerminal).not.toHaveBeenCalled()
})
it('passes launch preferences into agent-first worktree creation', async () => {
mockCreatedWorktree()
const { result } = await startWorker({
model: 'custom-codex-model',
effort: 'high'
})
expect(runtime.createManagedWorktree).toHaveBeenCalledWith(
expect.objectContaining({
startupAgent: 'codex',
startupLaunchPreferences: { model: 'custom-codex-model', effort: 'high' }
})
)
expect(result).toMatchObject({
state: 'ready',
launch: {
requested: { agent: 'codex', model: 'custom-codex-model', effort: 'high' },
effective: { agent: 'codex', model: 'custom-codex-model', effort: 'high' }
}
})
})
it('rejects a new worktree for a folder project before creating effects', async () => {
vi.mocked(runtime.showRepo).mockResolvedValue({
id: 'repo',
@@ -1,4 +1,3 @@
import { isTuiAgent } from '../../../../shared/tui-agent-config'
import type { TuiAgent } from '../../../../shared/types'
import { buildDispatchPreamble } from '../../orchestration/preamble'
import { OrchestrationError } from '../../orchestration/orchestration-error'
@@ -20,6 +19,7 @@ import {
persistWorkerSetupWaitOutcome
} from './orchestration-worker-setup-gate'
import { failWorkerStartWithReceipt } from './orchestration-worker-start-receipt'
import { prepareLocalWorkerStart } from './orchestration-worker-start-validation'
export const ORCHESTRATION_WORKER_START_METHODS: RpcMethod[] = [
defineMethod({
@@ -57,37 +57,7 @@ export const ORCHESTRATION_WORKER_START_METHODS: RpcMethod[] = [
const requestedWorktree = params.worktree ?? 'current'
const createsWorktree =
requestedWorktree === 'new-child' || requestedWorktree === 'new-top-level'
if (params.terminal && params.agent) {
throw new OrchestrationError(
'invalid_argument',
'--terminal reuses an existing agent and cannot combine with --agent.'
)
}
if (createsWorktree && params.terminal) {
throw new OrchestrationError(
'invalid_argument',
'--terminal cannot combine with new-worktree creation.'
)
}
if (createsWorktree && !params.name) {
throw new OrchestrationError('invalid_argument', 'New worktrees require --name.')
}
if (!createsWorktree && (params.name || params.repo || params.baseBranch || params.setup)) {
throw new OrchestrationError(
'invalid_argument',
'Creation and setup options apply only to new-child or new-top-level worktrees.'
)
}
const agent = params.agent
if (!params.terminal && (!agent || !isTuiAgent(agent))) {
throw new OrchestrationError(
'agent_unconfigured',
'A configured --agent is required when worker-start creates a terminal.'
)
}
if (agent) {
runtime.validateOrchestrationAgentLauncher(agent as TuiAgent)
}
const { agent, launch } = prepareLocalWorkerStart({ params, createsWorktree, runtime })
const coordinatorTerminal = await runtime.showTerminal(params.from)
const coordinatorWorktree = await runtime.showManagedWorktree(
@@ -130,6 +100,7 @@ export const ORCHESTRATION_WORKER_START_METHODS: RpcMethod[] = [
baseBranch: params.baseBranch ?? null,
terminal: params.terminal ?? null,
agent: agent ?? null,
launch: launch.receipt,
timeoutMs: params.timeoutMs ?? 60_000,
setup: createsWorktree ? (params.setup ?? 'run') : 'not_applicable',
setupSource: createsWorktree
@@ -174,6 +145,7 @@ export const ORCHESTRATION_WORKER_START_METHODS: RpcMethod[] = [
coordinatorWorktree,
params,
agent: agent as TuiAgent,
launchPreferences: launch.preferences,
effects
})
resolvedWorktree = created.worktree
@@ -190,6 +162,7 @@ export const ORCHESTRATION_WORKER_START_METHODS: RpcMethod[] = [
runtime,
worktreeId: resolvedWorktree!.id,
agent: agent as TuiAgent,
launchPreferences: launch.preferences,
taskId: task.id,
effects
})
@@ -281,6 +254,7 @@ export const ORCHESTRATION_WORKER_START_METHODS: RpcMethod[] = [
state: worker.state,
stage: worker.stage,
setup: setupReceipt,
launch: launch.receipt,
timeoutMs: params.timeoutMs ?? 60_000,
effects,
residualResources: [],
@@ -294,7 +268,8 @@ export const ORCHESTRATION_WORKER_START_METHODS: RpcMethod[] = [
dispatchId: started.dispatch.id,
failedStage,
error,
setup: setupReceipt
setup: setupReceipt,
launch: launch.receipt
})
}
}
@@ -2235,6 +2235,61 @@ describe('orchestration RPC methods', () => {
)
})
it('applies and reports opaque per-invocation model preferences', async () => {
setup()
mockCurrentWorkerStart()
const task = db.createTask({ spec: 'launch a custom model' })
const result = (await call('orchestration.workerStart', {
task: task.id,
from: 'term_coord',
agent: 'claude',
model: 'aws-bedrock-opus-5',
effort: 'high'
})) as {
dispatchId: string
state: string
launch: {
requested: { agent: string; model: string; effort: string }
effective: { agent: string; model: string; effort: string }
}
}
expect(result).toMatchObject({
state: 'ready',
launch: {
requested: { agent: 'claude', model: 'aws-bedrock-opus-5', effort: 'high' },
effective: { agent: 'claude', model: 'aws-bedrock-opus-5', effort: 'high' }
}
})
expect(runtime.createTerminal).toHaveBeenCalledWith(
'id:repo::worktree',
expect.objectContaining({
startupAgent: 'claude',
launchPreferences: { model: 'aws-bedrock-opus-5', effort: 'high' }
})
)
expect(JSON.parse(db.getWorkerDispatch(result.dispatchId)!.start_options)).toMatchObject({
launch: result.launch
})
})
it('rejects launch preferences for an existing terminal before creating a Dispatch', async () => {
setup()
mockCurrentWorkerStart()
const task = db.createTask({ spec: 'reuse exact worker' })
await expect(
call('orchestration.workerStart', {
task: task.id,
from: 'term_coord',
terminal: 'term_worker',
model: 'gpt-5.6-sol'
})
).rejects.toMatchObject({ code: 'invalid_argument' })
expect(db.getDispatchContext(task.id)).toBeUndefined()
})
// 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 () => {
@@ -0,0 +1,35 @@
export function agentArgOptionTokens(tokens: readonly string[]): readonly string[] {
const terminator = tokens.indexOf('--')
return terminator === -1 ? tokens : tokens.slice(0, terminator)
}
export function removeAgentArgOption(
tokens: readonly string[],
aliases: readonly string[]
): string[] {
const result: string[] = []
for (let index = 0; index < tokens.length; index += 1) {
const token = tokens[index]
if (token === '--') {
result.push(...tokens.slice(index))
break
}
const exact = aliases.includes(token)
const matched = aliases.some(
(alias) =>
token.startsWith(`${alias}=`) ||
(alias.startsWith('-') &&
!alias.startsWith('--') &&
token.startsWith(alias) &&
token.length > alias.length)
)
if (!exact && !matched) {
result.push(token)
continue
}
if (exact && tokens[index + 1] && !tokens[index + 1].startsWith('-')) {
index += 1
}
}
return result
}
@@ -3,6 +3,7 @@ import type {
CatalogModel,
CatalogOption
} from './agent-session-option-catalog-types'
import { agentArgOptionTokens, removeAgentArgOption } from './agent-session-option-agent-args'
import {
CLAUDE_MODEL_LIST_ARGS,
CLAUDE_MODEL_LIST_STDIN,
@@ -10,7 +11,7 @@ import {
} from './claude-model-list-probe'
function hasFlag(tokens: readonly string[], flags: readonly string[]): boolean {
return tokens.some((token) =>
return agentArgOptionTokens(tokens).some((token) =>
flags.some(
(flag) =>
token === flag ||
@@ -24,8 +25,9 @@ function hasCodexEffortOverride(tokens: readonly string[]): boolean {
if (hasFlag(tokens, ['--reasoning-effort'])) {
return true
}
return tokens.some((token, index) => {
const previous = tokens[index - 1]
const optionTokens = agentArgOptionTokens(tokens)
return optionTokens.some((token, index) => {
const previous = optionTokens[index - 1]
return (
(token.startsWith('model_reasoning_effort=') &&
(previous === '-c' || previous === '--config')) ||
@@ -36,6 +38,32 @@ function hasCodexEffortOverride(tokens: readonly string[]): boolean {
})
}
function removeCodexEffortOverride(tokens: readonly string[]): string[] {
const withoutFlag = removeAgentArgOption(tokens, ['--reasoning-effort'])
const result: string[] = []
for (let index = 0; index < withoutFlag.length; index += 1) {
const token = withoutFlag[index]
if (token === '--') {
result.push(...withoutFlag.slice(index))
break
}
const next = withoutFlag[index + 1]
if ((token === '-c' || token === '--config') && next?.startsWith('model_reasoning_effort=')) {
index += 1
continue
}
if (
token.startsWith('-cmodel_reasoning_effort=') ||
token.startsWith('-c=model_reasoning_effort=') ||
token.startsWith('--config=model_reasoning_effort=')
) {
continue
}
result.push(token)
}
return result
}
const STANDARD_EFFORT_CHOICES = [
{ value: 'low', label: 'Low' },
{ value: 'medium', label: 'Medium' },
@@ -67,6 +95,7 @@ function claudeEffortWithChoices(choices: typeof EXTENDED_EFFORT_CHOICES): Catal
apply: {
launchArgs: (value) => ['--effort', String(value)],
agentArgsOverride: (tokens) => hasFlag(tokens, ['--effort']),
removeAgentArgs: (tokens) => removeAgentArgOption(tokens, ['--effort']),
midSession: { kind: 'command', build: (value) => `/effort ${String(value)}` }
}
}
@@ -108,6 +137,7 @@ const CLAUDE_FAST_MODE: CatalogOption = {
}
export const CLAUDE_SESSION_OPTION_CATALOG: AgentSessionOptionCatalog = {
supportsWorkerLaunchPreferences: true,
// Why: these ids are Claude CLI aliases that resolve to the newest model of
// each family on the host's CLI (`opus` is Opus 5 on current CLIs, older
// Opus on older CLIs), so pinned version labels lie on part of the fleet.
@@ -143,6 +173,7 @@ export const CLAUDE_SESSION_OPTION_CATALOG: AgentSessionOptionCatalog = {
modelApply: {
launchArgs: (value) => ['--model', String(value)],
agentArgsOverride: (tokens) => hasFlag(tokens, ['--model']),
removeAgentArgs: (tokens) => removeAgentArgOption(tokens, ['--model']),
midSession: {
kind: 'command',
build: (value) => `/model ${String(value)}`,
@@ -152,6 +183,7 @@ export const CLAUDE_SESSION_OPTION_CATALOG: AgentSessionOptionCatalog = {
detectAgentInteraction: 'claude-model-switch-confirmation'
}
},
unknownModelOptions: [claudeEffort(true)],
listModels: {
command: `echo '${CLAUDE_MODEL_LIST_STDIN.trim()}' | claude ${CLAUDE_MODEL_LIST_ARGS.join(' ')}`,
parse: parseClaudeCatalogModels
@@ -181,12 +213,14 @@ function codexEffort(includeExtraHigh: boolean): CatalogOption {
apply: {
launchArgs: (value) => ['-c', `model_reasoning_effort=${String(value)}`],
agentArgsOverride: hasCodexEffortOverride,
removeAgentArgs: removeCodexEffortOverride,
midSession: { kind: 'agent-picker', command: '/model' }
}
}
}
export const CODEX_SESSION_OPTION_CATALOG: AgentSessionOptionCatalog = {
supportsWorkerLaunchPreferences: true,
// Why: Codex model access depends on auth. Keep this seed short and allow
// unknown persisted ids to pass through instead of claiming a complete list.
models: [
@@ -203,11 +237,13 @@ export const CODEX_SESSION_OPTION_CATALOG: AgentSessionOptionCatalog = {
modelApply: {
launchArgs: (value) => ['-m', String(value)],
agentArgsOverride: (tokens) => hasFlag(tokens, ['-m', '--model']),
removeAgentArgs: (tokens) => removeAgentArgOption(tokens, ['-m', '--model']),
// Codex accepts a model argument in its live /model command.
midSession: {
kind: 'command',
build: (value) => `/model ${String(value)}`,
pickerCommand: '/model'
}
}
},
unknownModelOptions: [codexEffort(true)]
}
@@ -3,9 +3,10 @@ import type {
CatalogModel,
CatalogOption
} from './agent-session-option-catalog-types'
import { agentArgOptionTokens, removeAgentArgOption } from './agent-session-option-agent-args'
function hasModelFlag(tokens: readonly string[]): boolean {
return tokens.some(
return agentArgOptionTokens(tokens).some(
(token) =>
token === '-m' ||
token === '--model' ||
@@ -77,6 +78,7 @@ function parseCursorModels(stdout: string): CatalogModel[] {
}
export const CURSOR_SESSION_OPTION_CATALOG: AgentSessionOptionCatalog = {
supportsWorkerLaunchPreferences: true,
models: [
{ id: 'auto', label: 'Auto', isDefault: true, options: [] },
{
@@ -93,6 +95,7 @@ export const CURSOR_SESSION_OPTION_CATALOG: AgentSessionOptionCatalog = {
modelApply: {
launchArgs: (value) => ['--model', String(value)],
agentArgsOverride: hasModelFlag,
removeAgentArgs: (tokens) => removeAgentArgOption(tokens, ['-m', '--model']),
midSession: { kind: 'command', build: (value) => `/model ${String(value)}` }
},
composeModelValue: (modelId, values) => {
@@ -23,6 +23,8 @@ export type CatalogOptionApply = {
/** Why: later free-form args win, so the launch record must discard any
* picker value that those args may have replaced. */
agentArgsOverride?: (tokens: readonly string[]) => boolean
/** Removes conflicting defaults before a more specific launch choice is inserted. */
removeAgentArgs?: (tokens: readonly string[]) => string[]
composedIntoModel?: true
midSession?: CatalogMidSessionApply
}
@@ -53,6 +55,10 @@ export type CatalogModel = {
export type AgentSessionOptionCatalog = {
models: CatalogModel[]
modelApply: CatalogOptionApply
/** Opts this agent into structured per-worker launch overrides. */
supportsWorkerLaunchPreferences?: true
/** Launch-safe options for opaque model ids that are absent from the static catalog. */
unknownModelOptions?: CatalogOption[]
composeModelValue?: (modelId: string, values: Record<string, SessionOptionValue>) => string
listModels?: {
command: string
+41 -11
View File
@@ -7,10 +7,32 @@ export type ResolvedSessionOptionLaunch = {
appliedValues: Record<string, SessionOptionValue>
}
export function removeOverriddenAgentSessionArgs(
agent: AgentType,
values: Record<string, SessionOptionValue> | null | undefined,
tokens: readonly string[]
): string[] {
const catalog = getAgentSessionOptionCatalog(agent)
const modelId = typeof values?.model === 'string' ? values.model : null
if (!catalog || !values || !modelId) {
return [...tokens]
}
let result = catalog.modelApply.removeAgentArgs?.(tokens) ?? [...tokens]
const model = findCatalogModel(catalog, modelId)
const modelOptions = model?.options ?? catalog.unknownModelOptions ?? []
for (const option of modelOptions) {
if (values[option.id] !== undefined && option.apply.removeAgentArgs) {
result = option.apply.removeAgentArgs(result)
}
}
return result
}
export function resolveAgentSessionOptionLaunch(
agent: AgentType,
values: Record<string, SessionOptionValue> | null | undefined,
trailingAgentArgs: readonly string[] = []
trailingAgentArgs: readonly string[] = [],
includeCatalogDefaults = true
): ResolvedSessionOptionLaunch {
const catalog = getAgentSessionOptionCatalog(agent)
const modelId = typeof values?.model === 'string' ? values.model : null
@@ -21,11 +43,23 @@ export function resolveAgentSessionOptionLaunch(
const model = findCatalogModel(catalog, modelId)
const appliedValues: Record<string, SessionOptionValue> = {}
const args: string[] = []
const modelValues = model
? Object.fromEntries(
model.options.map((option) => [option.id, values[option.id] ?? option.kind.defaultValue])
)
: {}
const modelOptions = model?.options ?? catalog.unknownModelOptions ?? []
const modelValues = Object.fromEntries(
modelOptions.flatMap((option) => {
const explicitValue = values[option.id]
if (explicitValue !== undefined) {
if (
!model &&
option.kind.type === 'select' &&
!option.kind.choices.some((choice) => choice.value === explicitValue)
) {
return []
}
return [[option.id, explicitValue]]
}
return model && includeCatalogDefaults ? [[option.id, option.kind.defaultValue]] : []
})
)
const composedModelId = catalog.composeModelValue
? catalog.composeModelValue(modelId, modelValues)
: modelId
@@ -37,11 +71,7 @@ export function resolveAgentSessionOptionLaunch(
appliedValues.model = modelId
}
}
if (!model) {
return { args, appliedValues }
}
for (const option of model.options) {
for (const option of modelOptions) {
const value = modelValues[option.id]
if (value === undefined) {
continue
+3
View File
@@ -32,6 +32,8 @@ export const REMOTE_RUNTIME_SHARED_CONTROL_CAPABILITY = 'remote-runtime.shared-c
export const ORCHESTRATION_FEDERATION_RUNTIME_CAPABILITY = 'orchestration.federation.v1' as const
export const ORCHESTRATION_FEDERATION_CONTROL_MAIL_RUNTIME_CAPABILITY =
'orchestration.federation-control-mail.v1' as const
export const ORCHESTRATION_WORKER_LAUNCH_PREFERENCES_RUNTIME_CAPABILITY =
'orchestration.worker-launch-preferences.v1' as const
export const ORCHESTRATION_FEDERATION_CONTROL_MAIL_PROTOCOL_VERSION = 2 as const
export const ORCHESTRATION_CONTRACT_VERSION = 1 as const
export const ORCHESTRATION_CONTRACT_RUNTIME_CAPABILITY = 'orchestration.contract.v1' as const
@@ -86,6 +88,7 @@ export const RUNTIME_CAPABILITIES = [
REMOTE_RUNTIME_SHARED_CONTROL_CAPABILITY,
ORCHESTRATION_FEDERATION_RUNTIME_CAPABILITY,
ORCHESTRATION_FEDERATION_CONTROL_MAIL_RUNTIME_CAPABILITY,
ORCHESTRATION_WORKER_LAUNCH_PREFERENCES_RUNTIME_CAPABILITY,
ORCHESTRATION_CONTRACT_RUNTIME_CAPABILITY,
'browser.screencast.v1',
'terminal.binary-stream.v1',
+53 -6
View File
@@ -1,4 +1,7 @@
import { resolveAgentSessionOptionLaunch } from './agent-session-option-launch'
import {
removeOverriddenAgentSessionArgs,
resolveAgentSessionOptionLaunch
} from './agent-session-option-launch'
import type { SessionOptionValue } from './native-chat-session-options'
import { getTuiAgentLaunchCommand, TUI_AGENT_CONFIG } from './tui-agent-config'
import {
@@ -25,6 +28,7 @@ export function resolveAgentLaunchCommand(args: {
shell: AgentStartupShell
agentArgs?: string | null
sessionOptions?: Record<string, SessionOptionValue>
sessionOptionsOverrideAgentArgs?: boolean
isRemote?: boolean
}): ResolvedAgentLaunchCommand {
const override = args.cmdOverrides[args.agent]
@@ -46,17 +50,60 @@ export function resolveAgentLaunchCommand(args: {
const resolvedOptions = resolveAgentSessionOptionLaunch(
args.agent,
args.sessionOptions,
trailingTokens.tokens
args.sessionOptionsOverrideAgentArgs ? [] : trailingTokens.tokens,
!args.sessionOptionsOverrideAgentArgs
)
if (override && args.sessionOptionsOverrideAgentArgs) {
const overrideTokens = tokenizeStartupCommand(override, args.shell)
if (!overrideTokens.ok) {
return { ok: false, error: `Agent command override is invalid: ${overrideTokens.error}` }
}
const commandOverrideOptions = resolveAgentSessionOptionLaunch(
args.agent,
args.sessionOptions,
overrideTokens.tokens,
false
)
if (
Object.entries(resolvedOptions.appliedValues).some(
([key, value]) => commandOverrideOptions.appliedValues[key] !== value
)
) {
return {
ok: false,
error:
'Agent command override conflicts with the requested launch preferences. Remove model or effort flags from the command override.'
}
}
}
const optionSuffix = resolvedOptions.args.map((arg) => quoteStartupArg(arg, args.shell)).join(' ')
const commandWithOptions = optionSuffix ? `${command} ${optionSuffix}` : command
const commandWithoutSessionOptions = suffix.suffix ? `${command} ${suffix.suffix}` : command
// Why: session flags precede the free-form suffix so the user's explicit
// repeated flag remains the final, winning occurrence.
const commandWithOptions = optionSuffix ? `${command} ${optionSuffix}` : command
const overrideTokens = args.sessionOptionsOverrideAgentArgs
? insertBeforeTerminator(
removeOverriddenAgentSessionArgs(args.agent, args.sessionOptions, trailingTokens.tokens),
resolvedOptions.args
)
: []
const commandWithOverrides = overrideTokens.length
? `${command} ${overrideTokens.map((token) => quoteStartupArg(token, args.shell)).join(' ')}`
: command
return {
ok: true,
command: suffix.suffix ? `${commandWithOptions} ${suffix.suffix}` : commandWithOptions,
command: args.sessionOptionsOverrideAgentArgs
? commandWithOverrides
: suffix.suffix
? `${commandWithOptions} ${suffix.suffix}`
: commandWithOptions,
commandWithoutSessionOptions,
appliedSessionOptions: resolvedOptions.appliedValues
}
}
function insertBeforeTerminator(tokens: readonly string[], inserted: readonly string[]): string[] {
const terminator = tokens.indexOf('--')
if (terminator === -1) {
return [...tokens, ...inserted]
}
return [...tokens.slice(0, terminator), ...inserted, ...tokens.slice(terminator)]
}
@@ -4,6 +4,7 @@ import {
buildAgentResumeStartupPlan,
buildAgentStartupPlan
} from './tui-agent-startup'
import { resolveAgentLaunchCommand } from './tui-agent-launch-command'
describe('tui agent startup session options', () => {
it('emits catalog options before user arguments without recording an overridden model', () => {
@@ -33,6 +34,59 @@ describe('tui agent startup session options', () => {
expect(plan?.sessionOptions).toEqual({ model: 'opus' })
})
it('lets explicit worker preferences override general agent arguments', () => {
const plan = buildAgentStartupPlan({
agent: 'codex',
prompt: '',
cmdOverrides: {},
platform: 'linux',
allowEmptyPromptLaunch: true,
sessionOptions: { model: 'custom-codex-model', effort: 'high' },
sessionOptionsOverrideAgentArgs: true,
agentArgs: '-m gpt-5.5 -c model_reasoning_effort=low'
})
expect(plan?.launchCommand).toBe(
"codex '-m' 'custom-codex-model' '-c' 'model_reasoning_effort=high'"
)
expect(plan?.launchConfig.agentCommand).toBe(
"codex '-m' 'gpt-5.5' '-c' 'model_reasoning_effort=low'"
)
expect(plan?.sessionOptions).toEqual({ model: 'custom-codex-model', effort: 'high' })
})
it('inserts worker preferences before an argument terminator', () => {
const plan = buildAgentStartupPlan({
agent: 'codex',
prompt: '',
cmdOverrides: {},
platform: 'linux',
allowEmptyPromptLaunch: true,
sessionOptions: { model: 'custom-codex-model', effort: 'high' },
sessionOptionsOverrideAgentArgs: true,
agentArgs: '--dangerously-bypass-approvals-and-sandbox -- literal'
})
expect(plan?.launchCommand).toBe(
"codex '--dangerously-bypass-approvals-and-sandbox' '-m' 'custom-codex-model' '-c' 'model_reasoning_effort=high' '--' 'literal'"
)
})
it('rejects conflicting singleton flags in an agent command override', () => {
expect(
resolveAgentLaunchCommand({
agent: 'codex',
cmdOverrides: { codex: 'codex --profile work -m gpt-5.5' },
platform: 'linux',
shell: 'posix',
sessionOptions: { model: 'custom-codex-model', effort: 'high' },
sessionOptionsOverrideAgentArgs: true
})
).toEqual({
ok: false,
error:
'Agent command override conflicts with the requested launch preferences. Remove model or effort flags from the command override.'
})
})
it('recognizes a long Codex model flag overriding the generated short flag', () => {
const plan = buildAgentStartupPlan({
agent: 'codex',
+3 -3
View File
@@ -36,9 +36,7 @@ export type AgentStartupPlan = {
sessionOptions?: Record<string, SessionOptionValue>
}
function appliedSessionOptionProps(
values: Record<string, SessionOptionValue>
): Pick<AgentStartupPlan, 'sessionOptions'> {
function appliedSessionOptionProps(values: Record<string, SessionOptionValue>) {
return Object.keys(values).length > 0 ? { sessionOptions: { ...values } } : {}
}
@@ -52,6 +50,7 @@ export function buildAgentStartupPlan(args: {
agentArgs?: string | null
agentEnv?: Record<string, string> | null
sessionOptions?: Record<string, SessionOptionValue>
sessionOptionsOverrideAgentArgs?: boolean
/** 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
@@ -68,6 +67,7 @@ export function buildAgentStartupPlan(args: {
shell,
agentArgs: usesQuery ? null : args.agentArgs,
sessionOptions: args.sessionOptions,
sessionOptionsOverrideAgentArgs: args.sessionOptionsOverrideAgentArgs,
isRemote: args.isRemote
})
if (!baseCommand.ok) {