mirror of
https://github.com/stablyai/orca.git
synced 2026-10-29 08:07:18 +00:00
fix(mobile): survive connection-migration cutovers during worktree create
A worktree.create in flight when the mobile transport migrates (relay/direct hand-off on shoddy cellular, relay lease rotation, relay recovery) rejects with "RPC interrupted by connection migration" even though the host completed it — leaving the Create Workspace modal stuck while the worktree exists on desktop. A naive retry hits a name collision and spawns a duplicate. Mirror the existing mobile terminal-create idempotency: worktree.create now accepts an optional clientMutationId that the host dedupes (in-flight + brief post-success TTL), and mobile mints one key per candidate name and re-issues the create on a cutover so the retry reconciles instead of duplicating.
This commit is contained in:
@@ -1387,6 +1387,62 @@ computeWorktreePathMock.mockImplementation(
|
||||
)
|
||||
ensurePathWithinWorkspaceMock.mockImplementation((targetPath: string) => targetPath)
|
||||
|
||||
describe('OrcaRuntimeService.dedupeWorktreeCreate', () => {
|
||||
it('coalesces concurrent creates that share a clientMutationId', async () => {
|
||||
const runtime = new OrcaRuntimeService(store)
|
||||
let calls = 0
|
||||
const factory = (): Promise<{ worktree: { id: string } }> => {
|
||||
calls += 1
|
||||
return Promise.resolve({ worktree: { id: 'wt' } })
|
||||
}
|
||||
const [a, b] = await Promise.all([
|
||||
runtime.dedupeWorktreeCreate('id:r', 'key-1', factory),
|
||||
runtime.dedupeWorktreeCreate('id:r', 'key-1', factory)
|
||||
])
|
||||
expect(calls).toBe(1)
|
||||
expect(a).toBe(b)
|
||||
})
|
||||
|
||||
it('reuses a settled success for a retry whose response was lost in a cutover', async () => {
|
||||
const runtime = new OrcaRuntimeService(store)
|
||||
let calls = 0
|
||||
const factory = (): Promise<{ worktree: { id: string } }> => {
|
||||
calls += 1
|
||||
return Promise.resolve({ worktree: { id: `wt-${calls}` } })
|
||||
}
|
||||
const first = await runtime.dedupeWorktreeCreate('id:r', 'key-1', factory)
|
||||
const retried = await runtime.dedupeWorktreeCreate('id:r', 'key-1', factory)
|
||||
expect(calls).toBe(1)
|
||||
expect(retried).toEqual(first)
|
||||
})
|
||||
|
||||
it('drops a failed create so a genuine retry starts fresh', async () => {
|
||||
const runtime = new OrcaRuntimeService(store)
|
||||
let calls = 0
|
||||
const factory = (): Promise<never> => {
|
||||
calls += 1
|
||||
return Promise.reject(new Error(`boom-${calls}`))
|
||||
}
|
||||
await expect(runtime.dedupeWorktreeCreate('id:r', 'key-1', factory)).rejects.toThrow('boom-1')
|
||||
await expect(runtime.dedupeWorktreeCreate('id:r', 'key-1', factory)).rejects.toThrow('boom-2')
|
||||
expect(calls).toBe(2)
|
||||
})
|
||||
|
||||
it('never dedupes across repos or when no clientMutationId is supplied', async () => {
|
||||
const runtime = new OrcaRuntimeService(store)
|
||||
let calls = 0
|
||||
const factory = (): Promise<{ n: number }> => {
|
||||
calls += 1
|
||||
return Promise.resolve({ n: calls })
|
||||
}
|
||||
await runtime.dedupeWorktreeCreate('id:a', 'key-1', factory)
|
||||
await runtime.dedupeWorktreeCreate('id:b', 'key-1', factory)
|
||||
await runtime.dedupeWorktreeCreate('id:a', undefined, factory)
|
||||
await runtime.dedupeWorktreeCreate('id:a', undefined, factory)
|
||||
expect(calls).toBe(4)
|
||||
})
|
||||
})
|
||||
|
||||
describe('OrcaRuntimeService', () => {
|
||||
it('projects runtime-backed settings to paired clients', () => {
|
||||
const runtime = new OrcaRuntimeService({
|
||||
|
||||
@@ -1287,6 +1287,10 @@ function getAgentLaunchPlatformForRepo(
|
||||
// Why: long enough for a phone to reconnect and retry a create whose response
|
||||
// was lost, short enough that an intentional later re-resume forks fresh.
|
||||
const MOBILE_TERMINAL_CREATE_RESULT_TTL_MS = 60_000
|
||||
// Why: same idempotency window for worktree.create — a phone whose create was
|
||||
// interrupted by a connection migration retries with the same clientMutationId
|
||||
// and reuses the just-created worktree instead of spawning a duplicate.
|
||||
const WORKTREE_CREATE_RESULT_TTL_MS = 60_000
|
||||
const FOREGROUND_AGENT_WRAPPER_RETRY_INTERVAL_MS = 150
|
||||
const FOREGROUND_AGENT_WRAPPER_RETRY_TIMEOUT_MS = 6_500
|
||||
const BRACKETED_PASTE_BEGIN = '\x1b[200~'
|
||||
@@ -2182,6 +2186,10 @@ export class OrcaRuntimeService {
|
||||
string,
|
||||
Promise<RuntimeMobileSessionCreateTerminalResult>
|
||||
>()
|
||||
// Why: idempotency map for worktree.create — a create interrupted by a mobile
|
||||
// connection migration is retried with the same clientMutationId and returns
|
||||
// the in-flight (or just-finished) operation instead of a duplicate worktree.
|
||||
private worktreeCreateByMutationId = new Map<string, Promise<unknown>>()
|
||||
// Why: a mobile create waits for the renderer to publish the new tab's surface
|
||||
// via graph-sync, but a throttled/hidden renderer can park that past the surface
|
||||
// timeout and the create would then destroy the live PTY (#7587). This lets the
|
||||
@@ -18752,6 +18760,38 @@ export class OrcaRuntimeService {
|
||||
})
|
||||
}
|
||||
|
||||
// Why: dedupes a worktree.create whose response was lost when a mobile
|
||||
// connection migration (relay/direct hand-off on shoddy cellular) rejected the
|
||||
// in-flight request. A retry with the same clientMutationId returns the
|
||||
// in-flight or just-finished create instead of a duplicate worktree; failures
|
||||
// drop immediately so a genuine retry starts fresh, and successes linger
|
||||
// briefly so a retry whose response was lost in the cutover still reconciles.
|
||||
dedupeWorktreeCreate<T>(
|
||||
repoSelector: string,
|
||||
clientMutationId: string | undefined,
|
||||
run: () => Promise<T>
|
||||
): Promise<T> {
|
||||
if (!clientMutationId) {
|
||||
return run()
|
||||
}
|
||||
const key = `${repoSelector}\0${clientMutationId}`
|
||||
const inflight = this.worktreeCreateByMutationId.get(key)
|
||||
if (inflight) {
|
||||
return inflight as Promise<T>
|
||||
}
|
||||
const created = run()
|
||||
this.worktreeCreateByMutationId.set(key, created)
|
||||
const drop = (): void => {
|
||||
if (this.worktreeCreateByMutationId.get(key) === created) {
|
||||
this.worktreeCreateByMutationId.delete(key)
|
||||
}
|
||||
}
|
||||
void created.then(() => {
|
||||
setTimeout(drop, WORKTREE_CREATE_RESULT_TTL_MS).unref?.()
|
||||
}, drop)
|
||||
return created
|
||||
}
|
||||
|
||||
async createMobileSessionTerminal(
|
||||
worktreeSelector: string,
|
||||
opts: {
|
||||
|
||||
@@ -19,6 +19,11 @@ import { REMOTE_RUNTIME_SHARED_CONTROL_CAPABILITY } from '../../shared/protocol-
|
||||
const REMOTE_RUNTIME_TEST_TIMEOUT_MS = 15_000
|
||||
const REMOTE_RUNTIME_REQUEST_TIMEOUT_MS = 5_000
|
||||
|
||||
// worktree.create routes through the runtime's clientMutationId idempotency
|
||||
// wrapper; these stubs run the create straight through (no dedupe).
|
||||
const passthroughDedupe = <T>(_repo: string, _id: string | undefined, run: () => Promise<T>) =>
|
||||
run()
|
||||
|
||||
describe('remote runtime request connection integration', () => {
|
||||
it(
|
||||
'fetches repos through the real E2EE WebSocket runtime',
|
||||
@@ -145,6 +150,7 @@ describe('remote runtime request connection integration', () => {
|
||||
source: 'git',
|
||||
worktrees
|
||||
}),
|
||||
dedupeWorktreeCreate: passthroughDedupe,
|
||||
createManagedWorktree: ({ name }: { name?: string }) => {
|
||||
const worktree = {
|
||||
id: `repo-1::${name || 'created'}`,
|
||||
@@ -370,6 +376,7 @@ describe('remote runtime request connection integration', () => {
|
||||
source: 'git',
|
||||
worktrees
|
||||
}),
|
||||
dedupeWorktreeCreate: passthroughDedupe,
|
||||
createManagedWorktree: ({ name }: { name?: string }) => {
|
||||
const worktree = {
|
||||
id: `repo-1::${name || 'created'}`,
|
||||
|
||||
@@ -143,6 +143,9 @@ export const WorktreeCreate = z
|
||||
.unknown()
|
||||
.transform((value) => (isTuiAgent(value) ? value : undefined))
|
||||
.optional(),
|
||||
// Why: mobile retries a create interrupted by a connection migration with the
|
||||
// same key so the host dedupes instead of spawning a duplicate worktree.
|
||||
clientMutationId: z.string().min(1).max(128).optional(),
|
||||
automationProvenanceRequest: AutomationWorkspaceProvenanceRequest.optional()
|
||||
})
|
||||
.superRefine((params, ctx) => {
|
||||
|
||||
@@ -19,10 +19,16 @@ function makeRequest(method: string, params?: unknown): RpcRequest {
|
||||
return { id: 'req-1', authToken: 'tok', method, params }
|
||||
}
|
||||
|
||||
// worktree.create routes through the runtime's clientMutationId idempotency
|
||||
// wrapper; these unit mocks run the create straight through (no dedupe).
|
||||
const passthroughDedupe = <T>(_repo: string, _id: string | undefined, run: () => Promise<T>) =>
|
||||
run()
|
||||
|
||||
describe('worktree RPC methods', () => {
|
||||
it('routes mobile session-only activation without notifying desktop clients', async () => {
|
||||
const runtime = {
|
||||
getRuntimeId: () => 'test-runtime',
|
||||
dedupeWorktreeCreate: passthroughDedupe,
|
||||
activateManagedWorktree: vi
|
||||
.fn()
|
||||
.mockResolvedValue({ repoId: 'repo-1', worktreeId: 'wt-1', activated: true })
|
||||
@@ -46,6 +52,7 @@ describe('worktree RPC methods', () => {
|
||||
it('forwards the mobile clientKind to the runtime on session-only activation', async () => {
|
||||
const runtime = {
|
||||
getRuntimeId: () => 'test-runtime',
|
||||
dedupeWorktreeCreate: passthroughDedupe,
|
||||
activateManagedWorktree: vi
|
||||
.fn()
|
||||
.mockResolvedValue({ repoId: 'repo-1', worktreeId: 'wt-1', activated: true })
|
||||
@@ -70,6 +77,7 @@ describe('worktree RPC methods', () => {
|
||||
it('routes dirty-file force to the runtime server', async () => {
|
||||
const runtime = {
|
||||
getRuntimeId: () => 'test-runtime',
|
||||
dedupeWorktreeCreate: passthroughDedupe,
|
||||
removeManagedWorktree: vi.fn().mockResolvedValue({})
|
||||
} as unknown as OrcaRuntimeService
|
||||
const dispatcher = new RpcDispatcher({ runtime, methods: WORKTREE_METHODS })
|
||||
@@ -89,6 +97,7 @@ describe('worktree RPC methods', () => {
|
||||
it('routes create options to the runtime server', async () => {
|
||||
const runtime = {
|
||||
getRuntimeId: () => 'test-runtime',
|
||||
dedupeWorktreeCreate: passthroughDedupe,
|
||||
showRepo: vi.fn().mockResolvedValue(repo),
|
||||
createManagedWorktree: vi.fn().mockResolvedValue({ worktree: { id: 'wt-1' } })
|
||||
} as unknown as OrcaRuntimeService
|
||||
@@ -158,6 +167,7 @@ describe('worktree RPC methods', () => {
|
||||
const dispatchToken = createAutomationDispatchToken('automation-1', 'run-1')
|
||||
const runtime = {
|
||||
getRuntimeId: () => 'test-runtime',
|
||||
dedupeWorktreeCreate: passthroughDedupe,
|
||||
showRepo: vi.fn().mockResolvedValue(repo),
|
||||
showAutomation: vi.fn(() => ({
|
||||
id: 'automation-1',
|
||||
@@ -246,6 +256,7 @@ describe('worktree RPC methods', () => {
|
||||
}
|
||||
const runtime = {
|
||||
getRuntimeId: () => 'test-runtime',
|
||||
dedupeWorktreeCreate: passthroughDedupe,
|
||||
showRepo: vi.fn().mockResolvedValue(runtimeLocalRepo),
|
||||
showAutomation: vi.fn(() => ({
|
||||
id: 'automation-runtime',
|
||||
@@ -303,6 +314,7 @@ describe('worktree RPC methods', () => {
|
||||
const dispatchToken = createAutomationDispatchToken('automation-edited', 'run-edited')
|
||||
const runtime = {
|
||||
getRuntimeId: () => 'test-runtime',
|
||||
dedupeWorktreeCreate: passthroughDedupe,
|
||||
showRepo: vi.fn().mockResolvedValue(repo),
|
||||
showAutomation: vi.fn(() => ({
|
||||
id: 'automation-edited',
|
||||
@@ -366,6 +378,7 @@ describe('worktree RPC methods', () => {
|
||||
const dispatchToken = createAutomationDispatchToken('automation-retry', 'run-retry')
|
||||
const runtime = {
|
||||
getRuntimeId: () => 'test-runtime',
|
||||
dedupeWorktreeCreate: passthroughDedupe,
|
||||
showRepo: vi.fn().mockResolvedValue(repo),
|
||||
showAutomation: vi.fn(() => ({
|
||||
id: 'automation-retry',
|
||||
@@ -429,6 +442,7 @@ describe('worktree RPC methods', () => {
|
||||
it('rejects forged automation provenance requests on worktree creation', async () => {
|
||||
const runtime = {
|
||||
getRuntimeId: () => 'test-runtime',
|
||||
dedupeWorktreeCreate: passthroughDedupe,
|
||||
showRepo: vi.fn().mockResolvedValue(repo),
|
||||
showAutomation: vi.fn(() => ({
|
||||
id: 'automation-1',
|
||||
@@ -471,6 +485,7 @@ describe('worktree RPC methods', () => {
|
||||
it('forwards startup command and env to runtime worktree creation', async () => {
|
||||
const runtime = {
|
||||
getRuntimeId: () => 'test-runtime',
|
||||
dedupeWorktreeCreate: passthroughDedupe,
|
||||
showRepo: vi.fn().mockResolvedValue(repo),
|
||||
createManagedWorktree: vi.fn().mockResolvedValue({
|
||||
worktree: { id: 'wt-1' },
|
||||
@@ -524,6 +539,7 @@ describe('worktree RPC methods', () => {
|
||||
it('drops invalid startup launch config env at the runtime RPC boundary', async () => {
|
||||
const runtime = {
|
||||
getRuntimeId: () => 'test-runtime',
|
||||
dedupeWorktreeCreate: passthroughDedupe,
|
||||
showRepo: vi.fn().mockResolvedValue(repo),
|
||||
createManagedWorktree: vi.fn().mockResolvedValue({ worktree: { id: 'wt-1' } })
|
||||
} as unknown as OrcaRuntimeService
|
||||
@@ -558,6 +574,7 @@ describe('worktree RPC methods', () => {
|
||||
it('forwards task startup drafts to runtime worktree creation', async () => {
|
||||
const runtime = {
|
||||
getRuntimeId: () => 'test-runtime',
|
||||
dedupeWorktreeCreate: passthroughDedupe,
|
||||
showRepo: vi.fn().mockResolvedValue(repo),
|
||||
createManagedWorktree: vi.fn().mockResolvedValue({ worktree: { id: 'wt-1' } })
|
||||
} as unknown as OrcaRuntimeService
|
||||
@@ -588,6 +605,7 @@ describe('worktree RPC methods', () => {
|
||||
it('routes create-base prefetches to the runtime server', async () => {
|
||||
const runtime = {
|
||||
getRuntimeId: () => 'test-runtime',
|
||||
dedupeWorktreeCreate: passthroughDedupe,
|
||||
prefetchManagedWorktreeCreateBase: vi.fn().mockResolvedValue(undefined)
|
||||
} as unknown as OrcaRuntimeService
|
||||
const dispatcher = new RpcDispatcher({ runtime, methods: WORKTREE_METHODS })
|
||||
@@ -609,6 +627,7 @@ describe('worktree RPC methods', () => {
|
||||
it('maps unknown telemetry sources to the runtime default instead of rejecting create', async () => {
|
||||
const runtime = {
|
||||
getRuntimeId: () => 'test-runtime',
|
||||
dedupeWorktreeCreate: passthroughDedupe,
|
||||
showRepo: vi.fn().mockResolvedValue(repo),
|
||||
createManagedWorktree: vi.fn().mockResolvedValue({ worktree: { id: 'wt-1' } })
|
||||
} as unknown as OrcaRuntimeService
|
||||
@@ -635,6 +654,7 @@ describe('worktree RPC methods', () => {
|
||||
it('rejects worktree.create when both parent and no-parent are supplied', async () => {
|
||||
const runtime = {
|
||||
getRuntimeId: () => 'test-runtime',
|
||||
dedupeWorktreeCreate: passthroughDedupe,
|
||||
createManagedWorktree: vi.fn()
|
||||
} as unknown as OrcaRuntimeService
|
||||
const dispatcher = new RpcDispatcher({ runtime, methods: WORKTREE_METHODS })
|
||||
@@ -656,6 +676,7 @@ describe('worktree RPC methods', () => {
|
||||
it('passes explicit repo selectors to PR base resolution and preserves start-point fields', async () => {
|
||||
const runtime = {
|
||||
getRuntimeId: () => 'test-runtime',
|
||||
dedupeWorktreeCreate: passthroughDedupe,
|
||||
resolveManagedPrBase: vi.fn().mockResolvedValue({
|
||||
baseBranch: 'abc123',
|
||||
headSha: 'abc123',
|
||||
@@ -694,6 +715,7 @@ describe('worktree RPC methods', () => {
|
||||
it('passes explicit repo selectors to MR base resolution', async () => {
|
||||
const runtime = {
|
||||
getRuntimeId: () => 'test-runtime',
|
||||
dedupeWorktreeCreate: passthroughDedupe,
|
||||
resolveManagedMrBase: vi.fn().mockResolvedValue({ baseBranch: 'origin/mr-head' })
|
||||
} as unknown as OrcaRuntimeService
|
||||
const dispatcher = new RpcDispatcher({ runtime, methods: WORKTREE_METHODS })
|
||||
@@ -719,6 +741,7 @@ describe('worktree RPC methods', () => {
|
||||
it('forwards Linear metadata through worktree.set', async () => {
|
||||
const runtime = {
|
||||
getRuntimeId: () => 'test-runtime',
|
||||
dedupeWorktreeCreate: passthroughDedupe,
|
||||
updateManagedWorktreeMeta: vi.fn().mockResolvedValue({ id: 'wt-1' })
|
||||
} as unknown as OrcaRuntimeService
|
||||
const dispatcher = new RpcDispatcher({ runtime, methods: WORKTREE_METHODS })
|
||||
@@ -746,6 +769,7 @@ describe('worktree RPC methods', () => {
|
||||
it('forwards push target clears through worktree.set', async () => {
|
||||
const runtime = {
|
||||
getRuntimeId: () => 'test-runtime',
|
||||
dedupeWorktreeCreate: passthroughDedupe,
|
||||
updateManagedWorktreeMeta: vi.fn().mockResolvedValue({ id: 'wt-1' })
|
||||
} as unknown as OrcaRuntimeService
|
||||
const dispatcher = new RpcDispatcher({ runtime, methods: WORKTREE_METHODS })
|
||||
@@ -771,6 +795,7 @@ describe('worktree RPC methods', () => {
|
||||
it('rejects worktree.set when both parent and no-parent are supplied', async () => {
|
||||
const runtime = {
|
||||
getRuntimeId: () => 'test-runtime',
|
||||
dedupeWorktreeCreate: passthroughDedupe,
|
||||
updateManagedWorktreeMeta: vi.fn()
|
||||
} as unknown as OrcaRuntimeService
|
||||
const dispatcher = new RpcDispatcher({ runtime, methods: WORKTREE_METHODS })
|
||||
@@ -802,6 +827,7 @@ describe('worktree RPC methods', () => {
|
||||
}
|
||||
const runtime = {
|
||||
getRuntimeId: () => 'test-runtime',
|
||||
dedupeWorktreeCreate: passthroughDedupe,
|
||||
listWorktreeLineage: vi.fn().mockResolvedValue(lineage),
|
||||
listWorkspaceLineage: vi.fn().mockResolvedValue({})
|
||||
} as unknown as OrcaRuntimeService
|
||||
@@ -817,6 +843,7 @@ describe('worktree RPC methods', () => {
|
||||
it('persists smart sort order on the runtime server', async () => {
|
||||
const runtime = {
|
||||
getRuntimeId: () => 'test-runtime',
|
||||
dedupeWorktreeCreate: passthroughDedupe,
|
||||
persistManagedWorktreeSortOrder: vi.fn().mockReturnValue({ updated: 2 })
|
||||
} as unknown as OrcaRuntimeService
|
||||
const dispatcher = new RpcDispatcher({ runtime, methods: WORKTREE_METHODS })
|
||||
|
||||
@@ -70,79 +70,85 @@ export const WORKTREE_METHODS: RpcMethod[] = [
|
||||
defineMethod({
|
||||
name: 'worktree.create',
|
||||
params: WorktreeCreate,
|
||||
handler: async (params, { runtime }) => {
|
||||
const repo = await runtime.showRepo(params.repo)
|
||||
const automationProvenance = resolveAutomationWorkspaceProvenance({
|
||||
authority: runtime,
|
||||
repoSelector: params.repo,
|
||||
repo,
|
||||
request: params.automationProvenanceRequest
|
||||
})
|
||||
// Why: provenance tokens are reserved before creation so retries can recover,
|
||||
// but failed create attempts must release the reservation for a safe retry.
|
||||
try {
|
||||
const result = await runtime.createManagedWorktree({
|
||||
handler: async (params, { runtime }) =>
|
||||
// Why: a mobile create interrupted by a connection migration is retried with
|
||||
// the same clientMutationId; dedupe so the host returns the in-flight/created
|
||||
// worktree instead of spawning a duplicate. No key (desktop/CLI) runs plainly.
|
||||
runtime.dedupeWorktreeCreate(params.repo, params.clientMutationId, async () => {
|
||||
const repo = await runtime.showRepo(params.repo)
|
||||
const automationProvenance = resolveAutomationWorkspaceProvenance({
|
||||
authority: runtime,
|
||||
repoSelector: params.repo,
|
||||
name: params.name ?? '',
|
||||
baseBranch: params.baseBranch,
|
||||
compareBaseRef: params.compareBaseRef,
|
||||
branchNameOverride: params.branchNameOverride,
|
||||
linkedIssue: params.linkedIssue,
|
||||
linkedPR: params.linkedPR,
|
||||
linkedLinearIssue: params.linkedLinearIssue,
|
||||
linkedLinearIssueWorkspaceId: params.linkedLinearIssueWorkspaceId,
|
||||
linkedLinearIssueOrganizationUrlKey: params.linkedLinearIssueOrganizationUrlKey,
|
||||
linkedGitLabMR: params.linkedGitLabMR,
|
||||
linkedGitLabIssue: params.linkedGitLabIssue,
|
||||
linkedBitbucketPR: params.linkedBitbucketPR,
|
||||
linkedAzureDevOpsPR: params.linkedAzureDevOpsPR,
|
||||
linkedGiteaPR: params.linkedGiteaPR,
|
||||
comment: params.comment,
|
||||
displayName: params.displayName,
|
||||
telemetrySource: params.telemetrySource,
|
||||
workspaceStatus: params.workspaceStatus,
|
||||
manualOrder: params.manualOrder,
|
||||
sparseCheckout: params.sparseCheckout,
|
||||
pushTarget: params.pushTarget,
|
||||
runHooks: params.runHooks === true,
|
||||
activate: params.activate === true,
|
||||
setupDecision: params.setupDecision,
|
||||
createdWithAgent: params.createdWithAgent ?? params.startupAgent,
|
||||
automationProvenance,
|
||||
startup: params.startupCommand
|
||||
? {
|
||||
command: params.startupCommand,
|
||||
...(params.startupEnv ? { env: params.startupEnv } : {}),
|
||||
...(params.startupLaunchConfig ? { launchConfig: params.startupLaunchConfig } : {}),
|
||||
...(params.startupCommandDelivery
|
||||
? { startupCommandDelivery: params.startupCommandDelivery }
|
||||
: {})
|
||||
}
|
||||
: undefined,
|
||||
...(params.startupAgent ? { startupAgent: params.startupAgent } : {}),
|
||||
...(params.startupPrompt !== undefined ? { startupPrompt: params.startupPrompt } : {}),
|
||||
startupDraft: params.startupDraft,
|
||||
lineage: {
|
||||
parentWorkspace: params.parentWorkspace,
|
||||
envParentWorkspace: params.envParentWorkspace,
|
||||
parentWorktree: params.parentWorktree,
|
||||
...(params.cwdParentWorktree ? { cwdParentWorktree: params.cwdParentWorktree } : {}),
|
||||
noParent: params.noParent === true,
|
||||
callerTerminalHandle: params.callerTerminalHandle,
|
||||
orchestrationContext: params.orchestrationContext
|
||||
}
|
||||
repo,
|
||||
request: params.automationProvenanceRequest
|
||||
})
|
||||
finishAutomationWorkspaceProvenanceRequest(params.automationProvenanceRequest)
|
||||
// Why: agent callers need a stable dispatch target without traversing
|
||||
// terminal-list layout duplicates after creating the worktree.
|
||||
return params.startupAgent && result.startupTerminal?.handle
|
||||
? { ...result, agentTerminalHandle: result.startupTerminal.handle }
|
||||
: result
|
||||
} catch (error) {
|
||||
releaseAutomationWorkspaceProvenanceRequest(params.automationProvenanceRequest)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
// Why: provenance tokens are reserved before creation so retries can recover,
|
||||
// but failed create attempts must release the reservation for a safe retry.
|
||||
try {
|
||||
const result = await runtime.createManagedWorktree({
|
||||
repoSelector: params.repo,
|
||||
name: params.name ?? '',
|
||||
baseBranch: params.baseBranch,
|
||||
compareBaseRef: params.compareBaseRef,
|
||||
branchNameOverride: params.branchNameOverride,
|
||||
linkedIssue: params.linkedIssue,
|
||||
linkedPR: params.linkedPR,
|
||||
linkedLinearIssue: params.linkedLinearIssue,
|
||||
linkedLinearIssueWorkspaceId: params.linkedLinearIssueWorkspaceId,
|
||||
linkedLinearIssueOrganizationUrlKey: params.linkedLinearIssueOrganizationUrlKey,
|
||||
linkedGitLabMR: params.linkedGitLabMR,
|
||||
linkedGitLabIssue: params.linkedGitLabIssue,
|
||||
linkedBitbucketPR: params.linkedBitbucketPR,
|
||||
linkedAzureDevOpsPR: params.linkedAzureDevOpsPR,
|
||||
linkedGiteaPR: params.linkedGiteaPR,
|
||||
comment: params.comment,
|
||||
displayName: params.displayName,
|
||||
telemetrySource: params.telemetrySource,
|
||||
workspaceStatus: params.workspaceStatus,
|
||||
manualOrder: params.manualOrder,
|
||||
sparseCheckout: params.sparseCheckout,
|
||||
pushTarget: params.pushTarget,
|
||||
runHooks: params.runHooks === true,
|
||||
activate: params.activate === true,
|
||||
setupDecision: params.setupDecision,
|
||||
createdWithAgent: params.createdWithAgent ?? params.startupAgent,
|
||||
automationProvenance,
|
||||
startup: params.startupCommand
|
||||
? {
|
||||
command: params.startupCommand,
|
||||
...(params.startupEnv ? { env: params.startupEnv } : {}),
|
||||
...(params.startupLaunchConfig
|
||||
? { launchConfig: params.startupLaunchConfig }
|
||||
: {}),
|
||||
...(params.startupCommandDelivery
|
||||
? { startupCommandDelivery: params.startupCommandDelivery }
|
||||
: {})
|
||||
}
|
||||
: undefined,
|
||||
...(params.startupAgent ? { startupAgent: params.startupAgent } : {}),
|
||||
...(params.startupPrompt !== undefined ? { startupPrompt: params.startupPrompt } : {}),
|
||||
startupDraft: params.startupDraft,
|
||||
lineage: {
|
||||
parentWorkspace: params.parentWorkspace,
|
||||
envParentWorkspace: params.envParentWorkspace,
|
||||
parentWorktree: params.parentWorktree,
|
||||
...(params.cwdParentWorktree ? { cwdParentWorktree: params.cwdParentWorktree } : {}),
|
||||
noParent: params.noParent === true,
|
||||
callerTerminalHandle: params.callerTerminalHandle,
|
||||
orchestrationContext: params.orchestrationContext
|
||||
}
|
||||
})
|
||||
finishAutomationWorkspaceProvenanceRequest(params.automationProvenanceRequest)
|
||||
// Why: agent callers need a stable dispatch target without traversing
|
||||
// terminal-list layout duplicates after creating the worktree.
|
||||
return params.startupAgent && result.startupTerminal?.handle
|
||||
? { ...result, agentTerminalHandle: result.startupTerminal.handle }
|
||||
: result
|
||||
} catch (error) {
|
||||
releaseAutomationWorkspaceProvenanceRequest(params.automationProvenanceRequest)
|
||||
throw error
|
||||
}
|
||||
})
|
||||
}),
|
||||
defineMethod({
|
||||
name: 'worktree.prefetchCreateBase',
|
||||
|
||||
Reference in New Issue
Block a user