Files
orca/src/main/runtime/runtime-folder-worktree-create.ts
T
Brennan Benson a375936c04 feat(agent-launch): let a caller reserve the pane its terminal launch creates (#22291)
* feat(agent-launch): let a caller reserve the pane its terminal launch creates

* fix(agent-launch): refuse a launch whose reserved pane is already live

* fix(agent-launch): refuse a live reserved pane before it is revealed

The live-pane refusal used to fire in the executor, after createTerminal
had already issued a handle, published the mobile snapshot and revealed
the tab. The reveal re-registered a fresh launch config over the running
agent's. agent.launch now passes requireFreshPane with a reserved pane,
and createTerminal throws AgentLaunchPaneAlreadyLiveError as soon as
spawn reports it attached to a live pane. That is before any handle,
snapshot or reveal. The spawn reattach itself is the one terminal.create
already uses, so the live PTY is never killed, and the stable-pane
create claim is still released in finally. The isReattach plumbing
added to the launch factory for the old check is gone.

A replay-safe launch refused this way on an existing workspace now
records a failed ledger row, the same way a name collision does.
Before, the row stayed claimed, so every retry got
agent_session_operation_unknown. agent.launchReplay passes the code
through. On create-worktree the workspace already exists when the
terminal is refused, so the row stays unknown. The code is added to
the runtime passthrough list so callers can branch on it.

The pane key is now in the replay fingerprint, deliberately. It is not
placement: group, anchor and focus still stay out of the request and
out of the ledger. It is identity. It is written into the pane's PTY
environment and names the tab the caller has placed. A retry that
reserved a different pane is therefore a different request. Replaying
the first answer would return a key the new reservation can never
find. This matches terminal.createAgentSession, which also fingerprints
its tab and leaf ids. The key is only folded in when present, so every
existing digest is unchanged, and a test pins that.

The wire schema now refuses a tab id the runtime would not adopt as
sent: one with surrounding whitespace, which the runtime trims, and one
longer than 512 characters, which the spawn reservation does not key
on. It reuses the tab-id schema that Placement uses.

* test(agent-launch): pin that a refused live pane issues no handle

The refusal test named handle issuance but only asserted the reveal, so a
throw moved to just before the reveal would still pass. Assert no terminal
is registered, with the attach test as the positive control.
2026-09-23 10:18:16 -07:00

208 lines
8.4 KiB
TypeScript

import { paneIdentity } from './runtime-terminal-pane-identity'
import { randomUUID } from 'node:crypto'
import { getProjectHostSetupWorktreeMeta } from '../../shared/project-host-setup-lookup'
import { resolveWorktreeCreateDisplayNameRequest } from '../ipc/worktree-logic'
import type { RuntimeTerminalCreate } from '../../shared/runtime-types'
import type { CreateWorktreeResult } from '../../shared/worktree/create-types'
import type { Repo } from '../../shared/repo-types'
import type { TuiAgent } from '../../shared/tui-agent'
import type { WorktreeStartupLaunch } from '../../shared/worktree/launch-types'
import {
getRuntimeFolderWorkspaceInstanceId,
mergeRuntimeFolderWorkspace
} from './runtime-folder-workspace'
import type { RuntimeManagedWorktreeCreateArgs } from './runtime-managed-worktree-create-types'
import type { RuntimeStore } from './runtime-store-contract'
import type { TerminalCreateOptions } from './runtime-terminal-contracts'
import type {
WorktreeStartupDraftPaste,
WorktreeStartupFollowup
} from './runtime-worktree-agent-startup'
type RuntimeFolderWorktreeCreateDeps = {
store: RuntimeStore
ptySpawnAvailable: boolean
createTerminal: (
selector: string,
options: TerminalCreateOptions
) => Promise<RuntimeTerminalCreate>
markTrusted: (agent: TuiAgent, path: string) => Promise<void>
pasteDraft: (handle: string, draft: WorktreeStartupDraftPaste) => void
sendFollowup: (handle: string, followup: WorktreeStartupFollowup) => void
invalidateResolvedWorktrees: () => void
notifyWorktreesChanged: (repoId: string) => void
emitCreated: (event: {
kind: 'created'
worktreeId: string
path: string
branch: string
}) => void
activate: (
repoId: string,
worktreeId: string,
setup?: undefined,
startup?: WorktreeStartupLaunch
) => void
}
export async function createRuntimeFolderWorktree(args: {
request: RuntimeManagedWorktreeCreateArgs
repo: Repo
startup?: WorktreeStartupLaunch
startupFollowup?: WorktreeStartupFollowup
createdWithAgent?: TuiAgent
draftPaste?: WorktreeStartupDraftPaste
deps: RuntimeFolderWorktreeCreateDeps
}): Promise<CreateWorktreeResult> {
const { request, repo, deps } = args
const now = Date.now()
const settings = deps.store.getSettings()
const instanceId = randomUUID()
const worktreeId = getRuntimeFolderWorkspaceInstanceId(repo, instanceId)
const displayNameRequest = resolveWorktreeCreateDisplayNameRequest(
request.displayName,
request.displayNameKind,
request.name,
request.cliProvenance?.kind === 'created-by-cli',
request.nameWasGenerated === true
)
const resolvedFolderDisplayName = displayNameRequest.value
const meta = deps.store.setWorktreeMeta(worktreeId, {
instanceId,
...getProjectHostSetupWorktreeMeta(deps.store.getProjectHostSetups?.() ?? [], repo),
displayName: resolvedFolderDisplayName ?? request.name,
...(displayNameRequest.kind === 'user' && resolvedFolderDisplayName
? { displayNameIsPinned: true }
: {}),
lastActivityAt: now,
createdAt: now,
orcaCreatedAt: now,
orcaCreationSource: 'runtime',
orcaCreationWorkspaceLayout: {
path: settings.workspaceDir,
nestWorkspaces: settings.nestWorkspaces
},
...(request.automationProvenance ? { automationProvenance: request.automationProvenance } : {}),
...(request.cliProvenance ? { cliProvenance: request.cliProvenance } : {}),
creatorProvenance: request.creatorProvenance ?? { kind: 'host' },
...(request.linkedIssue !== undefined ? { linkedIssue: request.linkedIssue } : {}),
...(request.linkedPR !== undefined ? { linkedPR: request.linkedPR } : {}),
...(request.linkedLinearIssue !== undefined
? { linkedLinearIssue: request.linkedLinearIssue }
: {}),
...(request.linkedLinearIssueWorkspaceId !== undefined
? { linkedLinearIssueWorkspaceId: request.linkedLinearIssueWorkspaceId }
: {}),
...(request.linkedLinearIssueOrganizationUrlKey !== undefined
? { linkedLinearIssueOrganizationUrlKey: request.linkedLinearIssueOrganizationUrlKey }
: {}),
...(request.linkedGitLabIssue !== undefined
? { linkedGitLabIssue: request.linkedGitLabIssue }
: {}),
...(request.linkedGitLabMR !== undefined ? { linkedGitLabMR: request.linkedGitLabMR } : {}),
...(request.linkedBitbucketPR !== undefined
? { linkedBitbucketPR: request.linkedBitbucketPR }
: {}),
...(request.linkedAzureDevOpsPR !== undefined
? { linkedAzureDevOpsPR: request.linkedAzureDevOpsPR }
: {}),
...(request.linkedGiteaPR !== undefined ? { linkedGiteaPR: request.linkedGiteaPR } : {}),
...(request.linkedWorkItem !== undefined ? { linkedWorkItem: request.linkedWorkItem } : {}),
...(request.linkedTaskSourceContext !== undefined
? { linkedTaskSourceContext: request.linkedTaskSourceContext }
: {}),
...(args.createdWithAgent ? { createdWithAgent: args.createdWithAgent } : {}),
...(request.comment !== undefined ? { comment: request.comment } : {}),
...(request.manualOrder !== undefined ? { manualOrder: request.manualOrder } : {}),
...(request.workspaceStatus !== undefined ? { workspaceStatus: request.workspaceStatus } : {})
})
const worktree = mergeRuntimeFolderWorkspace(repo, worktreeId, meta)
deps.invalidateResolvedWorktrees()
deps.notifyWorktreesChanged(repo.id)
deps.emitCreated({
kind: 'created',
worktreeId: worktree.id,
path: worktree.path,
branch: worktree.branch
})
const shouldActivate = request.activate === true || request.runHooks === true
let warning: string | undefined
let didSpawnStartup = false
let startupTerminal: CreateWorktreeResult['startupTerminal']
if (args.startup && deps.ptySpawnAvailable) {
try {
const trustAgent = args.draftPaste?.agent ?? args.createdWithAgent
if (trustAgent) {
await deps.markTrusted(trustAgent, worktree.path)
}
const terminal = await deps.createTerminal(`id:${worktree.id}`, {
command: args.startup.command,
...(request.startupCwd ? { cwd: request.startupCwd } : {}),
...paneIdentity(request.startupPaneKey),
env: args.startup.env,
...(args.startup.launchConfig ? { launchConfig: args.startup.launchConfig } : {}),
...(args.createdWithAgent ? { launchAgent: args.createdWithAgent } : {}),
...(args.startup.viewMode ? { viewMode: args.startup.viewMode } : {}),
startupCommandDelivery: args.startup.startupCommandDelivery,
telemetry: args.startup.telemetry,
...(shouldActivate ? {} : { surfaceOwner: false })
})
if (args.draftPaste) {
deps.pasteDraft(terminal.handle, args.draftPaste)
}
if (args.startupFollowup) {
deps.sendFollowup(terminal.handle, args.startupFollowup)
}
didSpawnStartup = true
startupTerminal = {
spawned: true,
handle: terminal.handle,
...(terminal.tabId ? { tabId: terminal.tabId } : {}),
...(terminal.paneKey ? { paneKey: terminal.paneKey } : {}),
...(terminal.ptyId ? { ptyId: terminal.ptyId } : {}),
surface: 'background'
}
} catch (error) {
const message = error instanceof Error ? error.message : String(error)
warning = `Failed to create the startup terminal for ${worktree.path}: ${message}`
console.warn(`[worktree-create] ${warning}`)
}
}
if (shouldActivate) {
deps.activate(
repo.id,
worktree.id,
undefined,
args.startup && !didSpawnStartup ? args.startup : undefined
)
} else if (deps.ptySpawnAvailable && !didSpawnStartup && !args.createdWithAgent) {
try {
await deps.createTerminal(`id:${worktree.id}`, { surfaceOwner: false })
} catch (error) {
const message = error instanceof Error ? error.message : String(error)
const initialWarning = `Failed to create the initial terminal for ${worktree.path}: ${message}`
warning = warning
? `${warning} Also failed to create the initial terminal for ${worktree.path}: ${message}`
: initialWarning
console.warn(`[worktree-create] ${warning}`)
}
}
return {
worktree: {
...worktree,
parentWorktreeId: null,
childWorktreeIds: [],
lineage: null,
git: {
path: worktree.path,
head: worktree.head,
branch: worktree.branch,
isBare: worktree.isBare,
isMainWorktree: worktree.isMainWorktree
}
},
...(startupTerminal ? { startupTerminal } : {}),
...(warning ? { warning } : {})
}
}