fix(worktree): sequence agent launch on provider setup exit

This commit is contained in:
Merge Sim
2026-08-31 14:08:01 -07:00
parent 3a07d17698
commit d9935c6a77
6 changed files with 386 additions and 228 deletions
+1 -10
View File
@@ -168,16 +168,7 @@ export function spawnDirectPty(params: {
env: Record<string, string>
ptySpawn: typeof pty.spawn
}): ShellSpawnResult {
const {
executable,
argv,
termName = 'xterm-256color',
cols,
rows,
cwd,
env,
ptySpawn
} = params
const { executable, argv, termName = 'xterm-256color', cols, rows, cwd, env, ptySpawn } = params
const process_ = ptySpawn(executable, [...argv], {
name: termName,
cols,
+1 -3
View File
@@ -243,7 +243,5 @@ export type IPtyProvider = {
getProfiles(): Promise<{ name: string; path: string }[]>
onData(callback: (payload: PtyDataEvent) => void): () => void
onReplay(callback: (payload: { id: string; data: string }) => void): () => void
onExit(
callback: (payload: PtyExitPayload) => void
): () => void
onExit(callback: (payload: PtyExitPayload) => void): () => void
}
+79 -106
View File
@@ -127,8 +127,7 @@ import { advertisedUrlWatcher } from '../ports/advertised-url-watcher'
import { makePaneKey } from '../../shared/stable-pane-id'
import {
SETUP_AGENT_SEQUENCE_SETUP_SCRIPT_ENV,
SETUP_AGENT_SEQUENCE_STARTUP_COMMAND_ENV,
SETUP_AGENT_SEQUENCE_STARTUP_SCRIPT_ENV
SETUP_AGENT_SEQUENCE_STARTUP_COMMAND_ENV
} from '../../shared/setup-agent-sequencing'
import type {
AgentSessionExecutionClaim,
@@ -6996,13 +6995,21 @@ describe('OrcaRuntimeService', () => {
vi.mocked(shouldRunSetupForCreate).mockReturnValue(true)
const spawn = vi
.fn()
.mockResolvedValueOnce({ id: 'pty-remote-agent' })
.mockResolvedValueOnce({ id: 'pty-remote-setup' })
.mockResolvedValueOnce({ id: 'pty-remote-agent' })
const revealTerminalSession = vi.fn().mockResolvedValue({ tabId: 'tab-remote' })
registerSshGitProvider('ssh-1', provider as never)
registerSshFilesystemProvider('ssh-1', fsProvider as never)
getActiveMultiplexerMock.mockReturnValue({ request: muxRequestMock, notify: vi.fn() })
const runtime = new OrcaRuntimeService(remoteStore as never)
const waitForSetupTerminalEvidence = vi
.spyOn(runtime, 'waitForSetupTerminalEvidence')
.mockResolvedValue({
ptyId: 'pty-remote-setup',
exitCode: 0,
incarnationId: 'incarnation-setup',
cause: { kind: 'exited', exitCode: 0 }
})
runtime.setPtyController({
spawn,
write: () => true,
@@ -7040,19 +7047,10 @@ describe('OrcaRuntimeService', () => {
`path:${result.worktree.path}`,
expect.objectContaining({ viewMode: 'chat' })
)
expect(waitForSetupTerminalEvidence).toHaveBeenCalledWith(expect.any(String))
await vi.waitFor(() => expect(spawn).toHaveBeenCalledTimes(2))
expect(spawn).toHaveBeenNthCalledWith(
1,
expect.objectContaining({
cwd: '/remote/mobile-setup',
env: expect.objectContaining({
[SETUP_AGENT_SEQUENCE_STARTUP_SCRIPT_ENV]: expect.stringContaining('exec claude')
}),
worktreeId: result.worktree.id
})
)
expect(spawn).toHaveBeenNthCalledWith(
2,
expect.objectContaining({
cwd: '/remote/mobile-setup',
command: expect.stringContaining(
@@ -7061,27 +7059,26 @@ describe('OrcaRuntimeService', () => {
worktreeId: result.worktree.id
})
)
const startup = spawn.mock.calls[0]![0] as {
command: string
env: Record<string, string>
}
const startupCommand = startup.command
const startupScript = startup.env[SETUP_AGENT_SEQUENCE_STARTUP_SCRIPT_ENV]!
expect(spawn).toHaveBeenNthCalledWith(
2,
expect.objectContaining({
cwd: '/remote/mobile-setup',
command: expect.stringContaining('claude'),
worktreeId: result.worktree.id
})
)
const setupSpawn = spawn.mock.calls[1]![0] as {
command: string
env: Record<string, string>
}
const setupCommand = setupSpawn.command
const setupScript = setupSpawn.env[SETUP_AGENT_SEQUENCE_SETUP_SCRIPT_ENV]!
const nonceMatch = startupScript.match(/if \[ "\$seen" = ([0-9a-f-]+) \]/)
expect(nonceMatch?.[1]).toBeTruthy()
const markerPath = `/remote/repo/.git/worktrees/mobile-setup/orca/setup-runner.sh.${nonceMatch![1]}.done`
expect(startupCommand.length).toBeLessThan(256)
expect(setupCommand.length).toBeLessThan(1024)
expect(setupScript).toContain('printf')
expect(setupScript).toContain(`${nonceMatch![1]} "$1"`)
expect(startupScript).toContain(markerPath)
expect(setupScript).toContain(markerPath)
const startup = spawn.mock.calls[0]![0] as {
command: string
env: Record<string, string>
}
expect(startup.command).toContain('claude')
expect(setupSpawn.command).toContain(
'/remote/repo/.git/worktrees/mobile-setup/orca/setup-runner.sh'
)
expect(revealTerminalSession).toHaveBeenLastCalledWith(
result.worktree.id,
expect.objectContaining({
@@ -45906,11 +45903,19 @@ describe('OrcaRuntimeService', () => {
}
const runtime = new OrcaRuntimeService(runtimeStore as never)
const createTerminal = vi.spyOn(runtime, 'createTerminal')
const waitForSetupTerminalEvidence = vi
.spyOn(runtime, 'waitForSetupTerminalEvidence')
.mockResolvedValue({
ptyId: 'pty-headless-setup',
exitCode: 0,
incarnationId: 'incarnation-setup',
cause: { kind: 'exited', exitCode: 0 }
})
const revealTerminalSession = vi.fn().mockResolvedValue({ tabId: 'tab-headless-startup' })
const spawn = vi
.fn()
.mockResolvedValueOnce({ id: 'pty-headless-startup' })
.mockResolvedValueOnce({ id: 'pty-headless-setup' })
.mockResolvedValueOnce({ id: 'pty-headless-startup' })
runtime.setPtyController({
spawn,
write: () => true,
@@ -45963,7 +45968,8 @@ describe('OrcaRuntimeService', () => {
repoSelector: 'id:repo-1',
name: 'runtime-headless-startup-setup',
setupDecision: 'run',
startup: { command: 'claude', viewMode: 'chat' }
startup: { command: 'claude', viewMode: 'chat' },
awaitTerminalProvisioning: true
})
expect(createSetupRunnerScript).toHaveBeenCalled()
@@ -45972,29 +45978,23 @@ describe('OrcaRuntimeService', () => {
`id:${result.worktree.id}`,
expect.objectContaining({ viewMode: 'chat' })
)
// Why: setup is provisioned fire-and-forget; the wait-for-setup guarantee comes from the shell nonce/marker, not JS spawn ordering.
expect(waitForSetupTerminalEvidence).toHaveBeenCalledWith(expect.any(String))
await vi.waitFor(() => expect(spawn).toHaveBeenCalledTimes(2))
const startup = spawn.mock.calls[0]![0] as {
const setupSpawn = spawn.mock.calls[0]![0] as {
command: string
env: Record<string, string>
}
const startupCommand = startup.command
const startupScript = startup.env[SETUP_AGENT_SEQUENCE_STARTUP_SCRIPT_ENV]!
const setupSpawn = spawn.mock.calls[1]![0] as {
const startup = spawn.mock.calls[1]![0] as {
command: string
env: Record<string, string>
}
const setupCommand = setupSpawn.command
const setupScript = setupSpawn.env[SETUP_AGENT_SEQUENCE_SETUP_SCRIPT_ENV]!
const nonceMatch = startupScript.match(/if \[ "\$seen" = ([0-9a-f-]+) \]/)
expect(nonceMatch?.[1]).toBeTruthy()
expect(startupCommand.length).toBeLessThan(256)
expect(startupScript).toContain('exec claude')
expect(startupScript).toContain('/mnt/c/tmp/repo/.git/orca/setup-runner.sh')
expect(setupCommand).toContain('bash /mnt/c/tmp/repo/.git/orca/setup-runner.sh')
expect(setupScript).toContain('printf')
expect(setupScript).toContain(`${nonceMatch![1]} "$1"`)
expect(setupSpawn.command).toContain('bash /mnt/c/tmp/repo/.git/orca/setup-runner.sh')
expect(startup.command).toContain('claude')
expect(result.setup).toBeUndefined()
expect(result.setupReceipt).toMatchObject({
state: 'succeeded',
terminalHandle: expect.stringMatching(/^term_/)
})
})
it('starts setup and startup side by side by default for local headless worktree creates', async () => {
@@ -47261,7 +47261,7 @@ describe('OrcaRuntimeService', () => {
expect(metaById[result.worktree.id]).toMatchObject({ createdWithAgent: 'claude' })
})
it('honors split setup placement for opted-in local startup-draft worktrees', async () => {
it('sequences setup before startup for opted-in local startup-draft worktrees', async () => {
const metaById: Record<string, WorktreeMeta> = {}
const runtimeStore = {
...store,
@@ -47280,8 +47280,16 @@ describe('OrcaRuntimeService', () => {
const runtime = new OrcaRuntimeService(runtimeStore as never)
const spawn = vi
.fn()
.mockResolvedValueOnce({ id: 'pty-startup-split-main' })
.mockResolvedValueOnce({ id: 'pty-startup-split-setup' })
.mockResolvedValueOnce({ id: 'pty-startup-split-main' })
const waitForSetupTerminalEvidence = vi
.spyOn(runtime, 'waitForSetupTerminalEvidence')
.mockResolvedValue({
ptyId: 'pty-startup-split-setup',
exitCode: 0,
incarnationId: 'incarnation-setup',
cause: { kind: 'exited', exitCode: 0 }
})
const revealTerminalSession = vi.fn().mockResolvedValue({ tabId: 'tab-startup-split' })
runtime.setPtyController({
spawn,
@@ -47340,14 +47348,13 @@ describe('OrcaRuntimeService', () => {
awaitTerminalProvisioning: true
})
expect(waitForSetupTerminalEvidence).toHaveBeenCalledWith(expect.any(String))
await vi.waitFor(() => expect(spawn).toHaveBeenCalledTimes(2))
expect(spawn).toHaveBeenNthCalledWith(
1,
expect.objectContaining({
cwd: '/tmp/workspaces/runtime-startup-setup-split',
env: expect.objectContaining({
[SETUP_AGENT_SEQUENCE_STARTUP_COMMAND_ENV]: expect.stringContaining('codex')
}),
command: expect.stringContaining('bash /tmp/repo/.git/orca/setup-runner.sh'),
worktreeId: result.worktree.id
})
)
@@ -47355,65 +47362,25 @@ describe('OrcaRuntimeService', () => {
2,
expect.objectContaining({
cwd: '/tmp/workspaces/runtime-startup-setup-split',
command: expect.stringContaining('bash /tmp/repo/.git/orca/setup-runner.sh'),
env: expect.objectContaining({
ORCA_ROOT_PATH: '/tmp/repo',
ORCA_WORKTREE_PATH: '/tmp/workspaces/runtime-startup-setup-split',
ORCA_WORKTREE_ID: result.worktree.id
}),
command: expect.stringContaining('codex'),
worktreeId: result.worktree.id
})
)
const startup = spawn.mock.calls[0]![0] as {
command: string
env: Record<string, string>
}
const startupCommand = startup.command
const startupScript = startup.env[SETUP_AGENT_SEQUENCE_STARTUP_SCRIPT_ENV]!
const setupSpawn = spawn.mock.calls[1]![0] as {
command: string
env: Record<string, string>
}
const setupScript = setupSpawn.env[SETUP_AGENT_SEQUENCE_SETUP_SCRIPT_ENV]!
const nonceMatch = startupScript.match(/if \[ "\$seen" = ([0-9a-f-]+) \]/)
expect(nonceMatch?.[1]).toBeTruthy()
const markerPath = `/tmp/repo/.git/orca/setup-runner.sh.${nonceMatch![1]}.done`
expect(startupCommand.length).toBeLessThan(256)
expect(setupSpawn.command.length).toBeLessThan(1024)
expect(startupScript).toContain('--dangerously-bypass-approvals-and-sandbox')
expect(setupScript).toContain('printf')
expect(setupScript).toContain(`${nonceMatch![1]} "$1"`)
expect(startupScript).toContain(markerPath)
expect(setupScript).toContain(markerPath)
const mainEnv = (spawn.mock.calls[0]![0] as { env?: Record<string, string> }).env ?? {}
const setupEnv = (spawn.mock.calls[1]![0] as { env?: Record<string, string> }).env ?? {}
const setupCall = spawn.mock.calls[0]![0] as { env?: Record<string, string> }
const startupCall = spawn.mock.calls[1]![0] as { command?: string }
expect(result.setup).toBeUndefined()
expect(result.setupReceipt).toMatchObject({
state: 'running',
state: 'succeeded',
terminalHandle: expect.stringMatching(/^term_/)
})
expect(mainEnv.ORCA_TAB_ID).toBeDefined()
expect(mainEnv.ORCA_PANE_KEY).toBeDefined()
expect(setupEnv.ORCA_TAB_ID).toBe(mainEnv.ORCA_TAB_ID)
const mainLeafId = mainEnv.ORCA_PANE_KEY!.slice(`${mainEnv.ORCA_TAB_ID!}:`.length)
expect(revealTerminalSession).toHaveBeenLastCalledWith(
result.worktree.id,
expect.objectContaining({
ptyId: 'pty-startup-split-setup',
tabId: mainEnv.ORCA_TAB_ID,
activate: false,
splitFromLeafId: mainLeafId,
splitDirection: 'vertical'
})
)
expect(setupCall.env).not.toHaveProperty(SETUP_AGENT_SEQUENCE_STARTUP_COMMAND_ENV)
expect(startupCall.command).toContain('codex')
expect(revealTerminalSession).toHaveBeenCalled()
})
it('passes the wrapped setup command to activation when startup spawned but setup did not', async () => {
it('does not start the agent when setup spawn fails', async () => {
const runtime = new OrcaRuntimeService(store)
const spawn = vi
.fn()
.mockResolvedValueOnce({ id: 'pty-startup-main' })
.mockRejectedValueOnce(new Error('setup spawn failed'))
const spawn = vi.fn().mockRejectedValueOnce(new Error('setup spawn failed'))
const activateWorktree = vi.fn()
runtime.setPtyController({
spawn,
@@ -47472,13 +47439,21 @@ describe('OrcaRuntimeService', () => {
startup: { command: 'claude' }
})
expect(spawn).toHaveBeenCalledTimes(2)
expect(spawn).toHaveBeenCalledTimes(1)
expect(activateWorktree).toHaveBeenCalledWith(
'repo-1',
expect.any(String),
expect.objectContaining({
runnerScriptPath: 'C:\\tmp\\repo\\.git\\orca\\setup-runner.sh',
command: expect.stringContaining('bash /mnt/c/tmp/repo/.git/orca/setup-runner.sh')
shell: {
family: 'posix',
executable: 'wsl.exe'
},
envVars: expect.objectContaining({
ORCA_ROOT_PATH: '/tmp/repo',
ORCA_WORKTREE_PATH: '/tmp/workspaces/runtime-startup-setup-retry'
}),
waitForAgentStartup: true
}),
undefined,
undefined
@@ -47486,9 +47461,7 @@ describe('OrcaRuntimeService', () => {
const activationSetup = activateWorktree.mock.calls[0]?.[2] as
| { command?: string; envVars?: Record<string, string> }
| undefined
// Why: the retry the renderer performs must carry the gate script alongside the command,
// or the Setup tab it opens records no outcome for the waiting agent terminal.
expect(activationSetup?.envVars?.[SETUP_AGENT_SEQUENCE_SETUP_SCRIPT_ENV]).toContain('printf')
expect(activationSetup?.envVars).not.toHaveProperty(SETUP_AGENT_SEQUENCE_SETUP_SCRIPT_ENV)
})
it('lets explicit startup draft agents override the desktop default', async () => {
+250 -107
View File
@@ -418,7 +418,7 @@ import type {
ForceDeleteWorktreeBranchResult,
RemoveWorktreeResult
} from '../../shared/worktree/create-types'
import type { WorktreeStartupLaunch } from '../../shared/worktree/launch-types'
import type { WorktreeSetupLaunch, WorktreeStartupLaunch } from '../../shared/worktree/launch-types'
import type {
WorkspaceLineage,
WorktreeLineage,
@@ -616,11 +616,7 @@ import {
buildSetupRunnerCommand,
getSetupRunnerCommandPlatformForPath
} from '../../shared/setup-runner-command'
import {
applySequencedSetupLaunch,
createSequencedSetupAgentCommands,
SETUP_AGENT_SEQUENCE_STARTUP_COMMAND_ENV
} from '../../shared/setup-agent-sequencing'
import { SETUP_AGENT_SEQUENCE_STARTUP_COMMAND_ENV } from '../../shared/setup-agent-sequencing'
import { TASK_PROVIDERS } from '../../shared/task-providers'
import { FIRST_PANE_ID } from '../../shared/pane-key'
import {
@@ -1717,6 +1713,7 @@ type TerminalAgentStatusSnapshot = {
type TerminalCreateOptions = {
command?: string
directExec?: { executable: string; argv: string[] }
claudeAgentTeamsSourceCommand?: string
cwd?: string
env?: Record<string, string>
@@ -2084,6 +2081,7 @@ type RuntimePtyController = {
rows: number
cwd?: string
command?: string
directExec?: { executable: string; argv: string[] }
launchAgent?: TuiAgent
commandDelivery?: 'renderer' | 'provider'
startupCommandDelivery?: WorktreeStartupLaunch['startupCommandDelivery']
@@ -3239,6 +3237,28 @@ function getSetupRunnerCommandPlatformForLaunch(
return getSetupRunnerCommandPlatformForPath(setup?.runnerScriptPath ?? '', fallbackPlatform)
}
function getDirectSetupExec(
setup: WorktreeSetupLaunch,
platform: 'windows' | 'posix'
): { executable: string; argv: string[] } | undefined {
// WSL and SSH providers need their own host-specific command routing; keep those launches
// shell-mediated until the remote owner advertises the structured-create capability.
if (setup.shell?.executable?.toLowerCase().endsWith('wsl.exe')) {
return undefined
}
if (platform === 'posix' && setup.runnerScriptPath.startsWith('/')) {
return { executable: 'bash', argv: [setup.runnerScriptPath] }
}
if (platform === 'windows' && /\.(cmd|bat)$/i.test(setup.runnerScriptPath)) {
const comspec = process.env.ComSpec || 'cmd.exe'
return {
executable: comspec,
argv: ['/d', '/s', '/v:on', '/c', `""${setup.runnerScriptPath}""`]
}
}
return undefined
}
export type RuntimeRendererReloadFence = Readonly<{
revision: number
recovery: 'renderer' | 'headless' | 'reloading'
@@ -3254,6 +3274,20 @@ type ProviderSnapshotReadOptions = {
visibleScreenOnly?: boolean
}
/** Ordered, provider-backed PTY termination evidence exposed to runtime waiters. */
export type RuntimePtyExitEvent = Readonly<{
ptyId: string
exitCode: number
incarnationId: PtyIncarnationId
cause: TerminalExitCause
}>
/** Setup sequencing admits exactly one terminal outcome; all inferred, signaled, or mismatched
* statuses fail closed so an agent can never start against an unproven environment. */
export function isSuccessfulSetupExitEvidence(event: RuntimePtyExitEvent): boolean {
return event.cause.kind === 'exited' && event.cause.exitCode === 0 && event.exitCode === 0
}
export class OrcaRuntimeService {
private readonly runtimeId = randomUUID()
private readonly startedAt = Date.now()
@@ -3416,7 +3450,7 @@ export class OrcaRuntimeService {
private sessionTabsInventoryPublicationEpoch: number | null = null
private sessionTabsInventoryWaiters = new Set<() => void>()
private waitersByHandle = new Map<string, Set<TerminalWaiter>>()
private ptyExitListenersByPtyId = new Map<string, Set<() => void>>()
private ptyExitListenersByPtyId = new Map<string, Set<(event: RuntimePtyExitEvent) => void>>()
private ptyController: RuntimePtyController | null = null
private notifier: RuntimeNotifier | null = null
private clientEventListeners = new Set<(event: RuntimeClientEvent) => void>()
@@ -17776,7 +17810,6 @@ export class OrcaRuntimeService {
pty?.incarnationId ??
`runtime:${this.runtimeId}:${this.getPtyLifecycleGeneration(ptyId)}`
this.advancePtyLifecycleGeneration(ptyId)
this.notifyPtyExitListeners(ptyId)
const exactSurfaceByKey = new Map<
string,
Pick<RetiredTerminalSurface, 'worktreeId' | 'parentTabId' | 'leafId'>
@@ -17929,6 +17962,14 @@ export class OrcaRuntimeService {
exitedSurfaces.push({ handle: leafHandle, paneKey: `${leaf.tabId}:${leaf.leafId}` })
}
}
// Publish only after the mutable PTY/leaf records carry the same evidence. Late subscribers
// and re-entrant observers therefore see one coherent exit certificate.
this.notifyPtyExitListeners(ptyId, {
ptyId,
exitCode,
incarnationId,
cause: exitCause
})
// Why: an explicit whole-tab close drops the leaf from the graph *before*
// this exit lands, so a leaf-only walk found nothing and left the dispatch
// reading 'dispatched' forever against a dead process. The PTY's own handle
@@ -22529,10 +22570,17 @@ export class OrcaRuntimeService {
})
}
subscribeToPtyExit(ptyId: string, listener: () => void): () => void {
subscribeToPtyExit(ptyId: string, listener: (event: RuntimePtyExitEvent) => void): () => void {
const lifecycleGeneration = this.getPtyLifecycleGeneration(ptyId)
if (this.isPtyKnownExited(ptyId)) {
listener()
const pty = this.ptysById.get(ptyId)
const exitCode = pty?.lastExitCode ?? -1
listener({
ptyId,
exitCode,
incarnationId: pty?.incarnationId ?? `runtime:${this.runtimeId}:${lifecycleGeneration}`,
cause: pty?.lastExitCause ?? resolveUnreportedExitCause(exitCode)
})
return () => {}
}
let listeners = this.ptyExitListenersByPtyId.get(ptyId)
@@ -22557,7 +22605,16 @@ export class OrcaRuntimeService {
this.isPtyKnownExited(ptyId)
) {
unsubscribe()
listener()
const pty = this.ptysById.get(ptyId)
const exitCode = pty?.lastExitCode ?? -1
listener({
ptyId,
exitCode,
incarnationId:
pty?.incarnationId ??
`runtime:${this.runtimeId}:${this.getPtyLifecycleGeneration(ptyId)}`,
cause: pty?.lastExitCause ?? resolveUnreportedExitCause(exitCode)
})
}
return unsubscribe
}
@@ -22618,6 +22675,38 @@ export class OrcaRuntimeService {
})
}
/**
* Returns the provider-owned termination certificate for a Setup PTY. This is intentionally
* separate from the legacy numeric waiter: sequencing must reject inferred/unknown status and
* only accept an explicit normal exit from the matching PTY incarnation.
*/
async waitForSetupTerminalEvidence(handle: string): Promise<RuntimePtyExitEvent> {
const ptyId = this.getLivePtyForHandle(handle)?.pty.ptyId
if (!ptyId) {
throw new Error('terminal_handle_stale')
}
return await new Promise<RuntimePtyExitEvent>((resolve, reject) => {
let settled = false
let unsubscribe: (() => void) | null = null
const finish = (event: RuntimePtyExitEvent): void => {
if (settled) return
settled = true
unsubscribe?.()
resolve(event)
}
const fail = (error: unknown): void => {
if (settled) return
settled = true
unsubscribe?.()
reject(error)
}
unsubscribe = this.subscribeToPtyExit(ptyId, finish)
if (this.isPtyKnownExited(ptyId) && !settled) {
fail(new Error('setup_exit_evidence_unavailable'))
}
})
}
async getWorktreePs(
limit = DEFAULT_WORKTREE_PS_LIMIT,
sourceDefaultsSupported = true
@@ -26812,6 +26901,10 @@ export class OrcaRuntimeService {
// records. Pass the whole sequenced launch record — command plus the env carrying its
// script — so the Setup tab runs the script the agent waits on, not a bare runner.
sequencedSetup?: CreateWorktreeResult['setup']
/** Reserve only the Setup surface. Sequenced agent launches create their agent pane
* after the provider reports a successful Setup exit, so a placeholder shell must not
* occupy the agent surface first. */
reservePrimaryTerminal?: boolean
// Why: a workspace provisioned in the background must not pull the sidebar
// to itself; the user never asked to look at these tabs.
surfaceOwner?: false
@@ -26836,7 +26929,11 @@ export class OrcaRuntimeService {
Pick<GlobalSettings, 'setupScriptLaunchMode'>
>
).setupScriptLaunchMode ?? 'new-tab'
if (!args.hasStartupTerminal && !primaryTerminalHandle) {
if (
!args.hasStartupTerminal &&
!primaryTerminalHandle &&
args.reservePrimaryTerminal !== false
) {
const terminal = await this.createTerminal(args.worktreeSelector, surfacing)
primaryTerminalHandle = terminal.handle
}
@@ -26861,6 +26958,10 @@ export class OrcaRuntimeService {
args.setup.shell
)
const setupEnv = { ...setupLaunch.envVars, ...observedCommand?.env }
const directExec =
args.worktreeSelector.startsWith('id:') && !setupLaunch.command
? getDirectSetupExec(args.setup, args.setupCommandPlatform)
: undefined
const shouldSplitSetup =
primaryTerminalHandle &&
(setupLaunchMode === 'split-vertical' || setupLaunchMode === 'split-horizontal')
@@ -26875,6 +26976,7 @@ export class OrcaRuntimeService {
: this.createTerminal(args.worktreeSelector, {
title: 'Setup',
command: setupCommand,
...(directExec ? { directExec } : {}),
env: setupEnv,
...surfacing
}))
@@ -27942,30 +28044,48 @@ export class OrcaRuntimeService {
let startupTerminalPaneKey: string | null = null
let startupTerminalPtyId: string | null = null
let sequencedStartup = effectiveStartup
let sequencedSetup: CreateWorktreeResult['setup']
if (effectiveStartup && setup?.waitForAgentStartup === true) {
const platform = getSetupRunnerCommandPlatformForLaunch(
setup,
process.platform === 'win32' ? 'windows' : 'posix'
)
const sequenced = createSequencedSetupAgentCommands({
runnerScriptPath: setup.runnerScriptPath,
startupCommand: effectiveStartup.command,
platform,
shell: setup.shell
})
sequencedStartup = {
...effectiveStartup,
command: sequenced.startupCommand,
...(sequenced.startupEnv
? { env: { ...effectiveStartup.env, ...sequenced.startupEnv } }
: {})
const setupMustGateStartup = Boolean(effectiveStartup && setup?.waitForAgentStartup === true)
let setupProvisioned = false
let setupWaitSucceeded = !setupMustGateStartup
// Host-owned sequencing: Setup is a real PTY whose provider exit is the only authorization
// to create the Agent PTY. Unknown/lost setup outcomes fail closed; there is no timer that
// can silently launch an agent without a successful exit certificate.
if (setupMustGateStartup && setup && this.ptyController?.spawn) {
try {
const provisioned = await this.provisionManagedWorktreeTerminals({
worktreeSelector: `id:${worktree.id}`,
worktreeId: worktree.id,
worktreePath,
setup,
...(defaultTabs ? { defaultTabs } : {}),
hasStartupTerminal: false,
reservePrimaryTerminal: false,
setupCommandPlatform: getSetupRunnerCommandPlatformForLaunch(setup, 'posix'),
observeSetupCompletion: false,
...(shouldActivate ? {} : { surfaceOwner: false })
})
didSpawnSetup = provisioned.setupSpawned
setupTerminalHandle = provisioned.setupTerminalHandle
setupProvisioned = provisioned.setupSpawned
if (!setupTerminalHandle) {
throw new Error('setup_spawn_failed')
}
const evidence = await this.waitForSetupTerminalEvidence(setupTerminalHandle)
setupWaitSucceeded = isSuccessfulSetupExitEvidence(evidence)
if (!setupWaitSucceeded) {
warning = `Setup did not complete successfully for ${worktreePath}; the agent was not started.`
}
} catch (err) {
setupWaitSucceeded = false
const message = err instanceof Error ? err.message : String(err)
warning = warning
? `${warning} Also setup sequencing could not be proven for ${worktreePath}: ${message}`
: `Setup sequencing could not be proven for ${worktreePath}: ${message}`
}
sequencedSetup = applySequencedSetupLaunch(setup, sequenced)
}
if (sequencedStartup && this.ptyController?.spawn) {
if (effectiveStartup && setupWaitSucceeded && this.ptyController?.spawn) {
try {
// Why: automation startup must not depend on a renderer TerminalPane
// mounting. Runtime-spawned PTYs run immediately and the UI adopts the
@@ -27975,16 +28095,16 @@ export class OrcaRuntimeService {
await this.markLocalWorkspaceTrustedForAgent(startupTrustAgent, worktreePath)
}
const terminal = await this.createTerminal(`id:${worktree.id}`, {
command: sequencedStartup.command,
command: effectiveStartup.command,
...(setup && effectiveStartup
? { claudeAgentTeamsSourceCommand: effectiveStartup.command }
: {}),
env: sequencedStartup.env,
...(sequencedStartup.launchConfig ? { launchConfig: sequencedStartup.launchConfig } : {}),
env: effectiveStartup.env,
...(effectiveStartup.launchConfig ? { launchConfig: effectiveStartup.launchConfig } : {}),
...(effectiveCreatedWithAgent ? { launchAgent: effectiveCreatedWithAgent } : {}),
...(sequencedStartup.viewMode ? { viewMode: sequencedStartup.viewMode } : {}),
startupCommandDelivery: sequencedStartup.startupCommandDelivery,
telemetry: sequencedStartup.telemetry,
...(effectiveStartup.viewMode ? { viewMode: effectiveStartup.viewMode } : {}),
startupCommandDelivery: effectiveStartup.startupCommandDelivery,
telemetry: effectiveStartup.telemetry,
...ownerSurfacing(shouldActivate)
})
if (effectiveDraftPaste) {
@@ -28010,7 +28130,8 @@ export class OrcaRuntimeService {
// Why: plain CLI creates should not steal the user's current workspace.
// Explicit activation and hook-running still use renderer activation so
// the user can watch prompts/output in a visible pane.
const runtimeWillProvisionTerminals = didSpawnStartup && Boolean(setup || defaultTabs)
const runtimeWillProvisionTerminals =
!setupProvisioned && didSpawnStartup && Boolean(setup || defaultTabs)
if (runtimeWillProvisionTerminals) {
// Why: once runtime spawned the startup PTY, renderer activation may see
// an existing terminal and skip setup/default tabs. Await provisioning so
@@ -28026,10 +28147,7 @@ export class OrcaRuntimeService {
primaryTerminalHandle: startupTerminalHandle,
hasStartupTerminal: didSpawnStartup,
setupCommandPlatform: getSetupRunnerCommandPlatformForLaunch(setup, 'posix'),
observeSetupCompletion: args.observeSetupCompletion,
// Why: carry the wait-for-agent gated setup launch (#6298) so the
// Setup tab runs the same script the sequenced agent waits on.
...(sequencedSetup ? { sequencedSetup } : {})
observeSetupCompletion: args.observeSetupCompletion
})
didSpawnSetup = provisioned.setupSpawned
setupTerminalHandle = provisioned.setupTerminalHandle
@@ -28037,13 +28155,9 @@ export class OrcaRuntimeService {
// Why: when runtime spawned setup, omit it from activation. When setup
// spawn failed, fall through with the wrapped command so renderer
// activation retries it.
const activationSetup = didSpawnSetup
? undefined
: didSpawnStartup && sequencedSetup
? sequencedSetup
: setup
const activationSetup = didSpawnSetup ? undefined : setup
const activationDefaultTabs = runtimeWillProvisionTerminals ? undefined : defaultTabs
if (effectiveStartup && !didSpawnStartup) {
if (effectiveStartup && !didSpawnStartup && !setupMustGateStartup) {
this.notifyActivateWorktree(repo.id, worktree.id, {
setup: activationSetup,
startup: effectiveStartup,
@@ -28057,7 +28171,13 @@ export class OrcaRuntimeService {
navigationTarget: args.navigation
})
}
} else if (this.ptyController?.spawn && (setup || defaultTabs || didSpawnStartup)) {
} else if (
!setupProvisioned &&
this.ptyController?.spawn &&
!didSpawnStartup &&
!setup &&
!defaultTabs
) {
// Why: inactive terminal materialization matches normal worktree creation,
// but setup/default tab failures must not gate automation dispatch.
const provisioning = this.provisionManagedWorktreeTerminals({
@@ -28070,7 +28190,6 @@ export class OrcaRuntimeService {
hasStartupTerminal: didSpawnStartup,
setupCommandPlatform: getSetupRunnerCommandPlatformForLaunch(setup, 'posix'),
observeSetupCompletion: args.observeSetupCompletion,
...(sequencedSetup ? { sequencedSetup } : {}),
surfaceOwner: false
})
// Why: runtime owns setup spawning here, so the RPC result must omit setup
@@ -28085,7 +28204,7 @@ export class OrcaRuntimeService {
didSpawnSetup = true
}
}
} else if (this.ptyController?.spawn) {
} else if (this.ptyController?.spawn && !setupProvisioned && !didSpawnStartup) {
try {
await this.createTerminal(`id:${worktree.id}`, { surfaceOwner: false })
} catch (err) {
@@ -28096,11 +28215,7 @@ export class OrcaRuntimeService {
console.warn(`[worktree-create] ${warning}`)
}
}
const returnedSetup = didSpawnSetup
? undefined
: didSpawnStartup && sequencedSetup
? sequencedSetup
: setup
const returnedSetup = didSpawnSetup ? undefined : setup
this.emitWorktreeLifecycle({
kind: 'created',
worktreeId: worktree.id,
@@ -28132,9 +28247,13 @@ export class OrcaRuntimeService {
? ('skipped' as const)
: // Why: the in-process hook is already executing, so reporting
// spawn_failed would strand callers that retry on it.
didSpawnSetup || didStartInProcessSetupHook
? ('running' as const)
: ('spawn_failed' as const),
setupMustGateStartup && setupProvisioned
? setupWaitSucceeded
? ('succeeded' as const)
: ('failed' as const)
: didSpawnSetup || didStartInProcessSetupHook
? ('running' as const)
: ('spawn_failed' as const),
...(setupTerminalHandle ? { terminalHandle: setupTerminalHandle } : {})
}
}
@@ -28283,25 +28402,48 @@ export class OrcaRuntimeService {
let startupTerminalPaneKey: string | null = null
let startupTerminalPtyId: string | null = null
let sequencedStartup = args.startup
let sequencedSetup: CreateWorktreeResult['setup']
if (args.startup && result.setup?.waitForAgentStartup === true) {
const platform = getSetupRunnerCommandPlatformForLaunch(result.setup, 'posix')
const sequenced = createSequencedSetupAgentCommands({
runnerScriptPath: result.setup.runnerScriptPath,
startupCommand: args.startup.command,
platform,
shell: result.setup.shell
})
sequencedStartup = {
...args.startup,
command: sequenced.startupCommand,
...(sequenced.startupEnv ? { env: { ...args.startup.env, ...sequenced.startupEnv } } : {})
const setupMustGateStartup = Boolean(args.startup && result.setup?.waitForAgentStartup === true)
let setupProvisioned = false
let setupWaitSucceeded = !setupMustGateStartup
// Host-owned sequencing: Setup is a real PTY whose provider exit is the only authorization
// to create the Agent PTY. Unknown/lost setup outcomes fail closed; there is no timer that
// can silently launch an agent without a successful exit certificate.
if (setupMustGateStartup && result.setup && this.ptyController?.spawn) {
try {
const provisioned = await this.provisionManagedWorktreeTerminals({
worktreeSelector: `path:${result.worktree.path}`,
worktreeId: result.worktree.id,
worktreePath: result.worktree.path,
setup: result.setup,
...(result.defaultTabs ? { defaultTabs: result.defaultTabs } : {}),
hasStartupTerminal: false,
reservePrimaryTerminal: false,
setupCommandPlatform: getSetupRunnerCommandPlatformForLaunch(result.setup, 'posix'),
observeSetupCompletion: false,
...(shouldActivate ? {} : { surfaceOwner: false })
})
didSpawnSetup = provisioned.setupSpawned
setupTerminalHandle = provisioned.setupTerminalHandle
setupProvisioned = provisioned.setupSpawned
if (!setupTerminalHandle) {
throw new Error('setup_spawn_failed')
}
const evidence = await this.waitForSetupTerminalEvidence(setupTerminalHandle)
setupWaitSucceeded = isSuccessfulSetupExitEvidence(evidence)
if (!setupWaitSucceeded) {
warning = `Setup did not complete successfully for ${result.worktree.path}; the agent was not started.`
}
} catch (err) {
setupWaitSucceeded = false
const message = err instanceof Error ? err.message : String(err)
warning = warning
? `${warning} Also setup sequencing could not be proven for ${result.worktree.path}: ${message}`
: `Setup sequencing could not be proven for ${result.worktree.path}: ${message}`
}
sequencedSetup = applySequencedSetupLaunch(result.setup, sequenced)
}
if (sequencedStartup && this.ptyController?.spawn) {
if (args.startup && setupWaitSucceeded && this.ptyController?.spawn) {
try {
const startupTrustAgent = args.startupDraftPaste?.agent ?? args.createdWithAgent
if (startupTrustAgent) {
@@ -28312,16 +28454,16 @@ export class OrcaRuntimeService {
)
}
const terminal = await this.createTerminal(`path:${result.worktree.path}`, {
command: sequencedStartup.command,
command: args.startup.command,
...(result.setup && args.startup
? { claudeAgentTeamsSourceCommand: args.startup.command }
: {}),
env: sequencedStartup.env,
...(sequencedStartup.launchConfig ? { launchConfig: sequencedStartup.launchConfig } : {}),
env: args.startup.env,
...(args.startup.launchConfig ? { launchConfig: args.startup.launchConfig } : {}),
...(args.createdWithAgent ? { launchAgent: args.createdWithAgent } : {}),
...(sequencedStartup.viewMode ? { viewMode: sequencedStartup.viewMode } : {}),
startupCommandDelivery: sequencedStartup.startupCommandDelivery,
telemetry: sequencedStartup.telemetry,
...(args.startup.viewMode ? { viewMode: args.startup.viewMode } : {}),
startupCommandDelivery: args.startup.startupCommandDelivery,
telemetry: args.startup.telemetry,
...ownerSurfacing(shouldActivate)
})
if (args.startupDraftPaste) {
@@ -28345,7 +28487,7 @@ export class OrcaRuntimeService {
if (shouldActivate) {
const runtimeWillProvisionTerminals =
didSpawnStartup && Boolean(result.setup || result.defaultTabs)
!setupProvisioned && didSpawnStartup && Boolean(result.setup || result.defaultTabs)
if (runtimeWillProvisionTerminals) {
// Why: remote/mobile task creates spawn the agent terminal in runtime,
// so renderer activation may not materialize setup/default tabs. Await so
@@ -28359,23 +28501,16 @@ export class OrcaRuntimeService {
primaryTerminalHandle: startupTerminalHandle,
hasStartupTerminal: didSpawnStartup,
setupCommandPlatform: getSetupRunnerCommandPlatformForLaunch(result.setup, 'posix'),
observeSetupCompletion: args.observeSetupCompletion,
// Why: carry the wait-for-agent gated setup launch (#6298) so the
// remote Setup tab runs the same script the sequenced agent waits on.
...(sequencedSetup ? { sequencedSetup } : {})
observeSetupCompletion: args.observeSetupCompletion
})
didSpawnSetup = provisioned.setupSpawned
setupTerminalHandle = provisioned.setupTerminalHandle
}
// Why: omit setup from activation when runtime spawned it; on spawn
// failure fall through with the wrapped command so renderer retries.
const activationSetup = didSpawnSetup
? undefined
: didSpawnStartup && sequencedSetup
? sequencedSetup
: result.setup
const activationSetup = didSpawnSetup ? undefined : result.setup
const activationDefaultTabs = runtimeWillProvisionTerminals ? undefined : result.defaultTabs
if (args.startup && !didSpawnStartup) {
if (args.startup && !didSpawnStartup && !setupMustGateStartup) {
this.notifyActivateWorktree(repo.id, result.worktree.id, {
setup: activationSetup,
startup: args.startup,
@@ -28394,7 +28529,10 @@ export class OrcaRuntimeService {
if (
!shouldActivate &&
this.ptyController?.spawn &&
(result.setup || result.defaultTabs || didSpawnStartup)
!setupProvisioned &&
!didSpawnStartup &&
!result.setup &&
!result.defaultTabs
) {
// Why: inactive terminal materialization matches normal worktree creation,
// but setup/default tab failures must not gate automation dispatch.
@@ -28408,7 +28546,6 @@ export class OrcaRuntimeService {
hasStartupTerminal: didSpawnStartup,
setupCommandPlatform: getSetupRunnerCommandPlatformForLaunch(result.setup, 'posix'),
observeSetupCompletion: args.observeSetupCompletion,
...(sequencedSetup ? { sequencedSetup } : {}),
surfaceOwner: false
})
// Why: runtime owns setup spawning here, so omit setup from the RPC result
@@ -28423,7 +28560,12 @@ export class OrcaRuntimeService {
didSpawnSetup = true
}
}
} else if (!shouldActivate && this.ptyController?.spawn) {
} else if (
!shouldActivate &&
this.ptyController?.spawn &&
!setupProvisioned &&
!didSpawnStartup
) {
try {
await this.createTerminal(`path:${result.worktree.path}`, { surfaceOwner: false })
} catch (err) {
@@ -28434,11 +28576,7 @@ export class OrcaRuntimeService {
}
}
const returnedSetup = didSpawnSetup
? undefined
: didSpawnStartup && sequencedSetup
? sequencedSetup
: result.setup
const returnedSetup = didSpawnSetup ? undefined : result.setup
const resultForRenderer = returnedSetup
? { ...result, setup: returnedSetup }
: (() => {
@@ -28473,9 +28611,13 @@ export class OrcaRuntimeService {
? ('skipped' as const)
: !result.setup
? ('not_configured' as const)
: didSpawnSetup
? ('running' as const)
: ('spawn_failed' as const),
: setupMustGateStartup && setupProvisioned
? setupWaitSucceeded
? ('succeeded' as const)
: ('failed' as const)
: didSpawnSetup
? ('running' as const)
: ('spawn_failed' as const),
...(setupTerminalHandle ? { terminalHandle: setupTerminalHandle } : {})
}
const resultWithSetupReceipt = args.awaitTerminalProvisioning
@@ -31021,6 +31163,7 @@ export class OrcaRuntimeService {
command: sequencedStartupCommand
? launchOpts.command
: (agentTeamsPlan?.command ?? launchOpts.command),
...(launchOpts.directExec ? { directExec: launchOpts.directExec } : {}),
launchAgent: launchOpts.launchAgent,
commandDelivery: 'provider',
startupCommandDelivery: launchOpts.startupCommandDelivery,
@@ -38666,13 +38809,13 @@ export class OrcaRuntimeService {
return this.getLeavesForPty(ptyId).some((leaf) => getTerminalState(leaf) === 'exited')
}
private notifyPtyExitListeners(ptyId: string): void {
private notifyPtyExitListeners(ptyId: string, event: RuntimePtyExitEvent): void {
const listeners = this.ptyExitListenersByPtyId.get(ptyId)
if (!listeners) {
return
}
this.ptyExitListenersByPtyId.delete(ptyId)
notifyRuntimeListeners(listeners, (listener) => listener(), 'pty-exit')
notifyRuntimeListeners(listeners, (listener) => listener(event), 'pty-exit')
}
private resolvePtyTuiIdleWaiters(pty: RuntimePtyWorktreeRecord, ptyId: string): void {
@@ -1,5 +1,5 @@
import { describe, expect, it, vi } from 'vitest'
import { OrcaRuntimeService } from './orca-runtime'
import { isSuccessfulSetupExitEvidence, OrcaRuntimeService } from './orca-runtime'
type RuntimeInternals = {
recordPtyWorktree: (ptyId: string, worktreeId: string, state?: { connected?: boolean }) => unknown
@@ -16,6 +16,21 @@ function registerLivePty(runtime: OrcaRuntimeService): void {
}
describe('PTY exit subscription', () => {
it.each([
[{ exitCode: 0, cause: { kind: 'exited', exitCode: 0 } }, true],
[{ exitCode: 1, cause: { kind: 'exited', exitCode: 1 } }, false],
[{ exitCode: 0, cause: { kind: 'signaled', signal: 9 } }, false],
[{ exitCode: -1, cause: { kind: 'unknown', reason: 'transport_lost' } }, false]
])('admits only an explicit normal zero exit as setup success', (partial, expected) => {
expect(
isSuccessfulSetupExitEvidence({
ptyId: 'pty-1',
incarnationId: 'incarnation-1',
...(partial as { exitCode: number; cause: never })
})
).toBe(expected)
})
it('fires on the backing PTY exit', () => {
const runtime = new OrcaRuntimeService()
registerLivePty(runtime)
@@ -31,6 +46,25 @@ describe('PTY exit subscription', () => {
expect(internals(runtime).ptyExitListenersByPtyId.has('pty-1')).toBe(false)
})
it('delivers provider exit evidence instead of requiring a mutable record read', () => {
const runtime = new OrcaRuntimeService()
registerLivePty(runtime)
const listener = vi.fn()
runtime.subscribeToPtyExit('pty-1', listener)
runtime.onPtyExit('pty-1', 0, 'incarnation-1', {
providerExitObserved: true,
cause: { kind: 'exited', exitCode: 0 }
})
expect(listener).toHaveBeenCalledWith({
ptyId: 'pty-1',
exitCode: 0,
incarnationId: 'incarnation-1',
cause: { kind: 'exited', exitCode: 0 }
})
})
it('does not retain listeners across subscription churn', () => {
const runtime = new OrcaRuntimeService()
registerLivePty(runtime)
@@ -109,4 +143,23 @@ describe('PTY exit subscription demands proof of exit, not loss of connection',
expect(listener).toHaveBeenCalledOnce()
})
it('replays the recorded cause for late subscribers', () => {
const runtime = new OrcaRuntimeService()
registerLivePty(runtime)
runtime.onPtyExit('pty-1', 0, 'incarnation-1', {
providerExitObserved: true,
cause: { kind: 'signaled', signal: 9 }
})
const listener = vi.fn()
runtime.subscribeToPtyExit('pty-1', listener)
expect(listener).toHaveBeenCalledWith({
ptyId: 'pty-1',
exitCode: 0,
incarnationId: 'incarnation-1',
cause: { kind: 'signaled', signal: 9 }
})
})
})
+1 -1
View File
@@ -142,7 +142,7 @@ export type CreateWorktreeResult = {
requested: 'run' | 'skip' | 'inherit'
hookFound: boolean
startupPolicy: 'start-immediately' | 'wait-for-setup'
state: 'running' | 'skipped' | 'not_configured' | 'spawn_failed'
state: 'running' | 'succeeded' | 'failed' | 'skipped' | 'not_configured' | 'spawn_failed'
terminalHandle?: string
}
defaultTabs?: WorktreeDefaultTabsLaunch