refactor(runtime): check workspace removal and recovery ownership (#27624)

This commit is contained in:
Neil
2026-10-14 01:16:48 -07:00
committed by GitHub
parent d3b17ca9f4
commit fe060f9ba0
17 changed files with 339 additions and 106 deletions
-3
View File
@@ -56,13 +56,10 @@ src/main/runtime/orca-runtime-refresh-pty-worktree-records-from-controller.ts
src/main/runtime/orca-runtime-refresh-pty-worktree-records-with-controller-inventory.ts
src/main/runtime/orca-runtime-refuse-unattributed-mobile-session-tab-close.ts
src/main/runtime/orca-runtime-register-pty.ts
src/main/runtime/orca-runtime-remove-managed-worktree.ts
src/main/runtime/orca-runtime-remove-orphan-or-folder-worktree.ts
src/main/runtime/orca-runtime-resolve-browser-network-execution-host-for-worktree.ts
src/main/runtime/orca-runtime-resolve-known-workspace-file-target.ts
src/main/runtime/orca-runtime-resolve-terminal-pane.ts
src/main/runtime/orca-runtime-resolve-terminal-split-source-authority.ts
src/main/runtime/orca-runtime-resolve-worktree-removal-target.ts
src/main/runtime/orca-runtime-restore-live-paired-renderer-session-owned-mobile-terminals.ts
src/main/runtime/orca-runtime-restore-structured-agent-session-tabs-once.ts
src/main/runtime/orca-runtime-run-create-mobile-session-terminal.ts
@@ -17,6 +17,8 @@ import {
type RegisteredOwner
} from './registered-worktree-root-owner'
type RepoSource = Pick<Store, 'getRepos'>
const registeredWorktreeRoots = new Set<string>()
const registeredOwners = new Map<string, RegisteredOwner>()
const registeredWorktreeRootsRevisionByRepo = new Map<string, number>()
@@ -24,7 +26,7 @@ const CREATED_WORKTREE_ROOTS_MAX = 64
let revisionSequence = 0
let baseRevision = 0
let invalidationGeneration = 0
let registryStore: Store | null = null
let registryStore: RepoSource | null = null
let currentOwners = new Map<string, Repo>()
let registeredWorktreeRootsDirty = true
let registeredWorktreeRootsRefresh: Promise<void> | null = null
@@ -35,7 +37,7 @@ function advanceOwner(owner: RegisteredOwner): void {
registeredWorktreeRootsRevisionByRepo.set(owner.repoId, owner.revision)
}
function resolveOwnerForRepo(store: Store, repo: Repo | string): RegisteredOwner | undefined {
function resolveOwnerForRepo(store: RepoSource, repo: Repo | string): RegisteredOwner | undefined {
const repos = synchronizeOwners(store)
const key = resolveWorktreeRootOwner(repos, repo, currentOwners)
return key === undefined ? undefined : registeredOwners.get(key)
@@ -51,7 +53,7 @@ function relistOwners(owners: Iterable<RegisteredOwner>): void {
registeredWorktreeRootsDirty = true
}
function synchronizeOwners(store: Store): Repo[] {
function synchronizeOwners(store: RepoSource): Repo[] {
const repos = store.getRepos()
const owners = getLocalWorktreeRootOwners(repos)
if (registryStore !== store) {
@@ -147,7 +149,7 @@ export async function rebuildAuthorizedRootsCache(store: Store, onlyDirty = fals
* the global side-effect-token fence, and bumping it would retire in-flight tokens for
* untouched repos. `advanceOwner` still records this repo's new revision.
*/
export function markAuthorizedRootsOwnerDirty(store: Store, repo: Repo | string): boolean {
export function markAuthorizedRootsOwnerDirty(store: RepoSource, repo: Repo | string): boolean {
const owner = resolveOwnerForRepo(store, repo)
if (!owner) {
return false
@@ -20,7 +20,7 @@ import {
* import, folder->git upgrade) must keep using the global form.
*/
export function invalidateAuthorizedRootsCacheForRepo(
store: Store | null | undefined,
store: Pick<Store, 'getRepos'> | null | undefined,
repo: Repo | string
): void {
if (!store || !markAuthorizedRootsOwnerDirty(store, repo)) {
+2 -2
View File
@@ -26,7 +26,7 @@ export type LocalProjectGhExecOptions = LocalProjectWorktreeGitOptions & {
}
export function getLocalProjectGitExecOptions(
store: Store,
store: ProjectRuntimeResolutionStore,
repo: Repo
): LocalProjectGitExecOptions {
// Why: local git must run in the same resolved project runtime as agents,
@@ -56,7 +56,7 @@ function getLocalProjectGitExecOptionsForRuntime(
}
export function getLocalProjectWorktreeGitOptions(
store: Store,
store: ProjectRuntimeResolutionStore,
repo: Repo
): LocalProjectWorktreeGitOptions {
const { wslDistro } = getLocalProjectGitExecOptions(store, repo)
@@ -1,9 +1,8 @@
import { killAllProcessesForWorktree } from './worktree-teardown'
import { killAllProcessesForWorktree, type WorktreeTeardownRuntime } from './worktree-teardown'
import type { IPtyProvider } from '../providers/pty-provider-contract'
import type { OrcaRuntimeService } from './orca-runtime'
export type FolderWorkspacePtyTeardownDeps = {
runtime: OrcaRuntimeService
runtime: WorktreeTeardownRuntime
getLocalProvider: () => IPtyProvider | null
onPtyStopped: ((ptyId: string) => void) | null
}
@@ -1,5 +1,5 @@
// @ts-nocheck -- mechanically split from OrcaRuntimeService; behavior is covered by AST equivalence and characterization tests.
import { OrcaRuntimeWithResolveWorktreeRemovalTarget } from './orca-runtime-resolve-worktree-removal-target'
import { OrcaRuntimeWithResolveWorktreeRemovalTarget } from './workspaces/workspace-removal-target'
import type { TerminalWorkspaceLaunchScope } from './runtime-legacy-worker-terminal-recovery-types'
import type { TuiAgent } from '../../shared/tui-agent'
import { parseWslUncPath } from '../../shared/wsl-paths'
+1 -1
View File
@@ -286,7 +286,7 @@ export abstract class OrcaRuntimeWithGetStatus extends OrcaRuntimeWithGetRuntime
return { tabId, worktreeId: pty.worktreeId }
}
protected notifyWorktreesChanged(
notifyWorktreesChanged(
repoId: string,
options?: { invalidateGitScans?: boolean; sortOrderOnly?: boolean }
): void {
@@ -65,13 +65,13 @@ export abstract class OrcaRuntimeWithPreservedBranchCleanup extends OrcaRuntimeW
createdWorktree?: Worktree
): Promise<TerminalWorkspaceLaunchScope>
protected readonly preservedBranchCleanup = new RuntimePreservedBranchCleanup(() =>
readonly preservedBranchCleanup = new RuntimePreservedBranchCleanup(() =>
this.store ? this.requireStore() : null
)
protected readonly getLocalProviderFn: (() => IPtyProvider) | null
protected readonly onPtyStopped: ((ptyId: string) => void) | null
readonly onPtyStopped: ((ptyId: string) => void) | null
protected readonly onTerminalAgentStatus:
| ((event: RuntimeTerminalAgentStatusEvent) => void)
@@ -1,10 +1,30 @@
import { listWorktrees, listWorktreesStrict, removeWorktree } from '../orca-runtime-test-mocks.spec'
import {
OrcaRuntimeService,
listWorktrees,
listWorktreesStrict,
removeWorktree
} from '../orca-runtime-test-mocks.spec'
import type { WorktreeMeta } from '../orca-runtime-test-mocks.spec'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { TEST_WORKTREE_ID, makeWorktreeMeta, store } from '../orca-runtime-test-fixtures.spec'
import {
TEST_REPO_ID,
TEST_REPO_PATH,
TEST_WORKTREE_ID,
TEST_WORKTREE_PATH,
makeWorktreeMeta,
store
} from '../orca-runtime-test-fixtures.spec'
import { createWorktreeRemovalRuntime } from '../orca-runtime-test-scenario-builders.spec'
import type { ExecutionHostId } from '../../../shared/execution-host'
import type { RuntimeStore } from '../runtime-store-contract'
import type { WorktreeRemovalRecord } from '../../worktree-removal-records'
import { failedWorktreeRemovals, pendingWorktreeRemovals } from '../../worktree-removal-table'
import {
_resetPendingWorktreeRemovalsForTests,
_settlePendingWorktreeRemovalsForTests
} from '../../worktree-background-removal'
import { RuntimeWorktreeRemovalInFlight } from '../runtime-worktree-removal-in-flight'
const REMOTE_REPO_PATH = '/remote/repo'
@@ -109,3 +129,136 @@ describe('OrcaRuntimeService worktree removal execution host', () => {
expect(metaById[TEST_WORKTREE_ID]).toBeDefined()
})
})
class RemovalRuntime extends OrcaRuntimeService {
protected override readonly removeManagedWorktreeInFlight = new RuntimeWorktreeRemovalInFlight()
readonly helperFailure = new Error('runtime helper override')
stopAtHelper = false
stopAtStore = false
readonly purges: {
store: RuntimeStore
worktreeId: string
repoId: string
hostId?: ExecutionHostId
}[] = []
protected override localRemovalJobHost(runtimeStore: RuntimeStore) {
if (this.stopAtHelper) {
throw this.helperFailure
}
return super.localRemovalJobHost(runtimeStore)
}
protected override requireStore(): RuntimeStore {
if (this.stopAtStore) {
throw this.helperFailure
}
return super.requireStore()
}
currentRemovalQueue() {
return this.removeManagedWorktreeInFlight
}
protected override purgeRemovedWorktree(
runtimeStore: RuntimeStore,
worktreeId: string,
repoId: string,
hostId?: ExecutionHostId
): void {
this.purges.push({ store: runtimeStore, worktreeId, repoId, hostId })
}
recoveryJobHost() {
return this.localRemovalJobHost(store)
}
retryRemoval(record: WorktreeRemovalRecord) {
return this.retryFailedLocalRemoval(
{ kind: 'local' },
{ id: record.worktreeId, path: record.worktreePath },
[],
{}
)
}
}
describe('workspace removal runtime dispatch', () => {
let runtime: RemovalRuntime
let record: WorktreeRemovalRecord
beforeEach(() => {
_resetPendingWorktreeRemovalsForTests()
runtime = new RemovalRuntime(store)
record = {
worktreeId: TEST_WORKTREE_ID,
repoId: TEST_REPO_ID,
repoPath: TEST_REPO_PATH,
worktreePath: TEST_WORKTREE_PATH,
branch: 'feature',
head: '',
deleteBranch: false,
force: false,
requestedAt: Date.now()
}
vi.spyOn(console, 'warn').mockImplementation(() => {})
})
afterEach(async () => {
await _settlePendingWorktreeRemovalsForTests()
_resetPendingWorktreeRemovalsForTests()
vi.restoreAllMocks()
})
it('retains runtime purge dispatch in a recovery job', () => {
runtime.recoveryJobHost().purge(record)
expect(runtime.purges).toEqual([
{ store, worktreeId: record.worktreeId, repoId: record.repoId, hostId: 'local' }
])
})
it('uses the runtime helper override before resuming an interrupted removal', () => {
runtime.stopAtHelper = true
pendingWorktreeRemovals.set(record.worktreeId, record)
expect(() => runtime.finishInterruptedWorktreeRemovals()).toThrow(runtime.helperFailure)
})
it('uses the runtime helper override before retrying a failed removal', () => {
runtime.stopAtHelper = true
failedWorktreeRemovals.set(record.worktreeId, record)
expect(() => runtime.retryRemoval(record)).toThrow(runtime.helperFailure)
})
it('captures the watcher callback and retains its job-host receiver', async () => {
const calls: { receiver: unknown; path: string }[] = []
runtime.acquireFileWatcherRemoval = async function (this: unknown, path: string) {
calls.push({ receiver: this, path })
return { finish: async () => {} }
}
const captured = runtime.acquireFileWatcherRemoval
const host = runtime.recoveryJobHost()
runtime.acquireFileWatcherRemoval = async () => {
throw new Error('replacement callback')
}
await host.acquireWatcherRemoval(record.worktreePath)
expect(host.acquireWatcherRemoval).toBe(captured)
expect(calls).toEqual([{ receiver: host, path: record.worktreePath }])
})
it('reads the current runtime removal queue', async () => {
const replacement = runtime.currentRemovalQueue()
const failure = new Error('current removal queue')
const get = vi.spyOn(replacement, 'get').mockImplementation(() => {
throw failure
})
runtime.stopAtStore = true
runtime['resolveWorktreeRemovalTarget'] = async () => ({
id: TEST_WORKTREE_ID,
repoId: TEST_REPO_ID,
path: TEST_WORKTREE_PATH
})
runtime['joinPendingWorktreeRemoval'] = () => undefined
await expect(runtime.removeManagedWorktree(TEST_WORKTREE_ID)).rejects.toBe(failure)
expect(get).toHaveBeenCalledOnce()
})
})
@@ -4,7 +4,7 @@ import type { RemoveWorktreeResult } from '../../shared/worktree/create-types'
import type { GitWorktreeInfo } from '../../shared/worktree/types'
import { assertWorktreeUnlockedForRemoval } from '../../shared/worktree/removal'
import { LOCAL_EXECUTION_HOST_ID } from '../../shared/execution-host'
import type { Store } from '../persistence'
import type { RuntimeStore } from './runtime-store-contract'
import {
releaseStartupRemovalFence,
type BackgroundWorktreeRemovalJob
@@ -32,7 +32,7 @@ import {
} from './runtime-registered-local-worktree-removal'
type InterruptedWorktreeRemovalHost = {
store: Store
store: RuntimeStore
acquireWatcherRemoval: (path: string) => Promise<{ finish: (removed: boolean) => Promise<void> }>
closeWatchers: (path: string) => Promise<void>
preservedBranchCleanup: Pick<RuntimePreservedBranchCleanup, 'preserveHead' | 'remember'>
@@ -94,7 +94,7 @@ type InterruptedLocalWorktreeRemovalArgs = Pick<
RuntimeLocalWorktreeRemovalFinishArgs,
'removedPushTarget' | 'closeWatchers' | 'preserveBranchHead' | 'finishRemoval'
> & {
store: Store
store: RuntimeStore
record: WorktreeRemovalRecord
acquireWatcherRemoval: (path: string) => Promise<{ finish: (removed: boolean) => Promise<void> }>
stopPtys?: () => Promise<void>
@@ -1,5 +1,7 @@
import type { IPtyProvider } from '../providers/pty-provider-contract'
import type { OrcaRuntimeService } from './orca-runtime'
type UnstoppedPtyRuntime = Pick<OrcaRuntimeService, 'getPtyLivenessVerdict'>
import {
UNSTOPPED_PTY_DETAIL_SEPARATOR,
STILL_LIVE_DETAIL_PREFIX,
@@ -65,7 +67,7 @@ export async function verifyUnstoppedPtys(
*/
export function unverifiableStopVerdict(
failedPtyIds: readonly string[],
runtime: OrcaRuntimeService | undefined
runtime: UnstoppedPtyRuntime | undefined
): UnstoppedPtyVerdict | null {
for (const ptyId of failedPtyIds) {
const verdict = runtime?.getPtyLivenessVerdict?.(ptyId)
@@ -81,7 +83,7 @@ export async function resolveUnstoppedPtyVerdict(
provider: IPtyProvider,
sweepBudgetMs: number,
providerObservesOwningHost: boolean,
runtime?: OrcaRuntimeService
runtime?: UnstoppedPtyRuntime
): Promise<UnstoppedPtyVerdict> {
if (failedPtyIds.length === 0) {
return { status: 'exited' }
@@ -1,11 +1,30 @@
// @ts-nocheck -- mechanically split from OrcaRuntimeService.
import { splitWorktreeId, splitWorktreeIdForFilesystem } from '../../shared/worktree/id'
import { parseExecutionHostId } from '../../shared/execution-host'
import { invalidateAuthorizedRootsCache } from '../ipc/filesystem-auth'
import { isFolderRepo } from '../../shared/repo-kind'
import { getRuntimeFolderWorkspaceRootId } from './runtime-folder-workspace'
import { killAllProcessesForWorktree } from './worktree-teardown'
import { teardownFolderWorkspacePtys } from './folder-workspace-pty-teardown'
import type { OrcaRuntimeWithRemoveManagedWorktree } from './workspace-removal'
import type { RuntimeStore } from '../runtime-store-contract'
import type { RuntimeWorktreeRemovalTarget } from '../runtime-worktree-selection'
import type { Repo } from '../../../shared/repo-types'
import type { ExecutionHostId } from '../../../shared/execution-host'
import type { WorktreeTeardownRuntime } from '../worktree-teardown'
import { splitWorktreeId, splitWorktreeIdForFilesystem } from '../../../shared/worktree/id'
import { parseExecutionHostId } from '../../../shared/execution-host'
import { invalidateAuthorizedRootsCache } from '../../ipc/filesystem-auth'
import { isFolderRepo } from '../../../shared/repo-kind'
import { getRuntimeFolderWorkspaceRootId } from '../runtime-folder-workspace'
import { killAllProcessesForWorktree } from '../worktree-teardown'
import { teardownFolderWorkspacePtys } from '../folder-workspace-pty-teardown'
type WorkspaceRegistrationRemovalRuntime = WorktreeTeardownRuntime &
Pick<
OrcaRuntimeWithRemoveManagedWorktree,
| 'getLocalProvider'
| 'onPtyStopped'
| 'acquireFileWatcherRemoval'
| 'clearOptimisticReconcileToken'
| 'removeWorktreeMetadataAndHistory'
| 'preservedBranchCleanup'
| 'invalidateResolvedWorktreeCache'
| 'invalidateWorktreeScanCacheForRepo'
| 'notifyWorktreesChanged'
>
export async function removeOrphanOrFolderWorktree({
runtime,
@@ -15,12 +34,12 @@ export async function removeOrphanOrFolderWorktree({
removalHostId,
repo
}: {
runtime: unknown
store: unknown
removalTarget: unknown
cleanupHostId?: string
removalHostId?: string
repo?: unknown
runtime: WorkspaceRegistrationRemovalRuntime
store: RuntimeStore
removalTarget: RuntimeWorktreeRemovalTarget
cleanupHostId?: ExecutionHostId
removalHostId?: ExecutionHostId
repo?: Repo
}): Promise<{ warning?: string } | undefined> {
if (!repo) {
const orphanHost = parseExecutionHostId(store.getWorktreeMeta(removalTarget.id)?.hostId)
@@ -1,42 +1,59 @@
// @ts-nocheck -- mechanically split from OrcaRuntimeService; behavior is covered by AST equivalence and characterization tests.
import { OrcaRuntimeWithRemoveManagedWorktree } from './orca-runtime-remove-managed-worktree'
import type { ExecutionHostId } from '../../shared/execution-host'
import type { ResolvedWorktree } from '../runtime-worktree-path-identity'
import type { AgentLaunchPreferences } from '../../../shared/agent-session-host-authority'
import { OrcaRuntimeWithRemoveManagedWorktree } from './workspace-removal'
import type {
RemoveManagedWorktreeOptions,
RuntimeWorktreeRemovalTarget
} from './runtime-worktree-selection'
import { resolveRuntimeWorktreeRemovalTarget } from './runtime-worktree-removal-target'
import type { RuntimeStore } from './runtime-store-contract'
import { splitWorktreeId } from '../../shared/worktree/id'
import { runtimeWorktreeIdsEqual } from './runtime-worktree-path-identity'
import { hasWorktreeRemovalRepoOwnerOnOtherHost } from '../worktree-removal-repo-owner'
import { advertisedUrlWatcher } from '../ports/advertised-url-watcher'
import { deleteWorktreeHistoryDir } from '../terminal-history-deletion'
import { forgetFailedAgentLaunches } from '../agent-launch/agent-launch-pane-attachment'
import { closeClientHostedBrowserPagesForWorktree } from './worktree-browser-client-page-close'
} from '../runtime-worktree-selection'
import { resolveRuntimeWorktreeRemovalTarget } from '../runtime-worktree-removal-target'
import type { RuntimeStore } from '../runtime-store-contract'
import { splitWorktreeId } from '../../../shared/worktree/id'
import { runtimeWorktreeIdsEqual } from '../runtime-worktree-path-identity'
import { hasWorktreeRemovalRepoOwnerOnOtherHost } from '../../worktree-removal-repo-owner'
import { advertisedUrlWatcher } from '../../ports/advertised-url-watcher'
import { deleteWorktreeHistoryDir } from '../../terminal-history-deletion'
import { forgetFailedAgentLaunches } from '../../agent-launch/agent-launch-pane-attachment'
import { closeClientHostedBrowserPagesForWorktree } from '../worktree-browser-client-page-close'
import type {
ForceDeleteWorktreeBranchResult,
RemoveWorktreeResult
} from '../../shared/worktree/create-types'
import type { RuntimeTerminalRename } from '../../shared/runtime-types'
import type { TerminalWorkspaceLaunchScope } from './runtime-legacy-worker-terminal-recovery-types'
import type { TerminalCreateOptions } from './runtime-terminal-contracts'
import { LOCAL_EXECUTION_HOST_ID, parseExecutionHostId } from '../../shared/execution-host'
import { invalidateAuthorizedRootsCacheForRepo } from '../ipc/filesystem-auth'
} from '../../../shared/worktree/create-types'
import type { RuntimeTerminalRename } from '../../../shared/runtime-types'
import type { TerminalWorkspaceLaunchScope } from '../runtime-legacy-worker-terminal-recovery-types'
import type { TerminalCreateOptions } from '../runtime-terminal-contracts'
import {
LOCAL_EXECUTION_HOST_ID,
parseExecutionHostId,
type ExecutionHostId
} from '../../../shared/execution-host'
import { invalidateAuthorizedRootsCacheForRepo } from '../../ipc/filesystem-auth'
import {
resumeInterruptedWorktreeRemovals,
retryFailedWorktreeRemoval,
waitForPendingWorktreeRemoval
} from '../worktree-background-removal'
import { interruptedLocalWorktreeRemovalJob } from './runtime-interrupted-local-worktree-removal'
import { retryFailedRemovalUnlessRegistered } from '../worktree-removal-table'
import type { GitWorktreeInfo } from '../../shared/worktree/types'
} from '../../worktree-background-removal'
import { interruptedLocalWorktreeRemovalJob } from '../runtime-interrupted-local-worktree-removal'
import { retryFailedRemovalUnlessRegistered } from '../../worktree-removal-table'
import type { GitWorktreeInfo } from '../../../shared/worktree/types'
import {
resolveAgentTerminalCreateOptions,
withHostAccountLaunchEnv
} from './runtime-agent-terminal-startup'
} from '../runtime-agent-terminal-startup'
export abstract class OrcaRuntimeWithResolveWorktreeRemovalTarget extends OrcaRuntimeWithRemoveManagedWorktree {
protected abstract resolveExplicitWorktreeIdScoped(
worktreeId: string,
requiredHostId?: ExecutionHostId
): Promise<ResolvedWorktree | null>
abstract invalidateResolvedWorktreeCache(): void
abstract invalidateWorktreeScanCacheForRepo(repoId: string): void
protected abstract cancelScheduledMobileSessionTabsChanged(worktreeId: string): void
protected abstract notifyMobileSessionTabsRemoved(worktreeId: string): void
protected abstract assertGraphReady(): void
protected abstract toAgentSessionOptions(
preferences: AgentLaunchPreferences | undefined
): Record<string, string> | undefined
export class OrcaRuntimeWithResolveWorktreeRemovalTarget extends OrcaRuntimeWithRemoveManagedWorktree {
protected async resolveWorktreeRemovalTarget(
worktreeSelector: string,
requiredHostId?: ExecutionHostId
@@ -127,7 +144,7 @@ export class OrcaRuntimeWithResolveWorktreeRemovalTarget extends OrcaRuntimeWith
invalidateAuthorizedRootsCacheForRepo(store, repoId)
}
protected removeWorktreeMetadataAndHistory(
removeWorktreeMetadataAndHistory(
store: RuntimeStore,
worktreeId: string,
hostId?: ExecutionHostId
@@ -1,36 +1,75 @@
// @ts-nocheck -- mechanically split from OrcaRuntimeService; behavior is covered by AST equivalence and characterization tests.
import { OrcaRuntimeWithCreateManagedRemoteWorktree } from './orca-runtime-create-managed-remote-worktree'
import type { RuntimeStore } from '../runtime-store-contract'
import type { OrcaRuntimeWithStopTerminalsForWorktree } from '../orca-runtime-stop-terminals-for-worktree'
import type { GitWorktreeInfo } from '../../../shared/worktree/types'
import { OrcaRuntimeWithCreateManagedRemoteWorktree } from '../orca-runtime-create-managed-remote-worktree'
import {
resolveWorktreeRemovalMetadata,
resolveWorktreeRemovalRepoOwner
} from '../worktree-removal-repo-owner'
import type { RemoveWorktreeResult } from '../../shared/worktree/create-types'
import { getRepoExecutionHostId, parseExecutionHostId } from '../../shared/execution-host'
import { finishAcceptedWorktreeRemoval } from '../worktree-background-removal'
import { preservedBranchCleanupScopeKey } from '../../shared/preserved-branch-cleanup'
} from '../../worktree-removal-repo-owner'
import type { RemoveWorktreeResult } from '../../../shared/worktree/create-types'
import {
getRepoExecutionHostId,
parseExecutionHostId,
type ExecutionHostId
} from '../../../shared/execution-host'
import { finishAcceptedWorktreeRemoval } from '../../worktree-background-removal'
import { preservedBranchCleanupScopeKey } from '../../../shared/preserved-branch-cleanup'
import {
getRuntimeWorktreeRemovalOptionsKey,
type RemoveManagedWorktreeOptions
} from './runtime-worktree-selection'
import { withWorktreeSpan } from '../observability/instrumentation'
type RemoveManagedWorktreeOptions,
type RuntimeWorktreeRemovalTarget
} from '../runtime-worktree-selection'
import { withWorktreeSpan } from '../../observability/instrumentation'
import {
resolveWorktreeRemovalHome,
resolveWorktreeRemovalRoute
} from '../worktree-removal-execution-host-route'
import { listWorktreesOnRemovalRoute } from './worktree-removal-route-listing'
import { isPrunableGitFileWorktree } from '../worktree-prunable-git-file'
import { findRegisteredDeletableWorktree } from '../worktree-removal-safety'
import { removeRuntimeUnregisteredWorktree } from './runtime-unregistered-worktree-removal'
import { assertWorktreeUnlockedForRemoval } from '../../shared/worktree/removal'
import { formatWorktreeRemovalError } from '../ipc/worktree-logic'
import { isWindowsAbsolutePathLike } from '../../shared/cross-platform-path'
import { isLocalRuntimeWorktreePathMissing } from './runtime-worktree-filesystem'
import { removeStaleLocalWorktreeRegistration } from '../local-worktree-removal-recovery'
import { cleanupUnusedWorktreePushTargetRemote } from '../ipc/worktree-remote'
import { removeRuntimeRegisteredLocalWorktree } from './runtime-registered-local-worktree-removal'
import { removeOrphanOrFolderWorktree } from './orca-runtime-remove-orphan-or-folder-worktree'
} from '../../worktree-removal-execution-host-route'
import { listWorktreesOnRemovalRoute } from '../worktree-removal-route-listing'
import { isPrunableGitFileWorktree } from '../../worktree-prunable-git-file'
import { findRegisteredDeletableWorktree } from '../../worktree-removal-safety'
import { removeRuntimeUnregisteredWorktree } from '../runtime-unregistered-worktree-removal'
import { assertWorktreeUnlockedForRemoval } from '../../../shared/worktree/removal'
import { formatWorktreeRemovalError } from '../../ipc/worktree-logic'
import { isWindowsAbsolutePathLike } from '../../../shared/cross-platform-path'
import { isLocalRuntimeWorktreePathMissing } from '../runtime-worktree-filesystem'
import { removeStaleLocalWorktreeRegistration } from '../../local-worktree-removal-recovery'
import { cleanupUnusedWorktreePushTargetRemote } from '../../ipc/worktree-remote'
import { removeRuntimeRegisteredLocalWorktree } from '../runtime-registered-local-worktree-removal'
import { removeOrphanOrFolderWorktree } from './workspace-registration-removal'
export abstract class OrcaRuntimeWithRemoveManagedWorktree extends OrcaRuntimeWithCreateManagedRemoteWorktree {
abstract forgetStructuredSessionMail(sessionId: string): void
abstract stopTerminalsForWorktree(
...args: Parameters<OrcaRuntimeWithStopTerminalsForWorktree['stopTerminalsForWorktree']>
): ReturnType<OrcaRuntimeWithStopTerminalsForWorktree['stopTerminalsForWorktree']>
abstract removeWorktreeMetadataAndHistory(
store: RuntimeStore,
worktreeId: string,
hostId?: ExecutionHostId
): void
abstract invalidateResolvedWorktreeCache(): void
abstract invalidateWorktreeScanCacheForRepo(repoId: string): void
protected abstract resolveWorktreeRemovalTarget(
worktreeSelector: string,
requiredHostId?: ExecutionHostId
): Promise<RuntimeWorktreeRemovalTarget>
protected abstract joinPendingWorktreeRemoval(
worktreeId: string,
options: RemoveManagedWorktreeOptions
): Promise<RemoveWorktreeResult> | undefined
protected abstract purgeRemovedWorktree(
store: RuntimeStore,
worktreeId: string,
repoId: string,
removalHostId?: ExecutionHostId
): void
protected abstract retryFailedLocalRemoval(
route: { kind: string },
target: { id: string; path: string },
registeredWorktrees: readonly GitWorktreeInfo[],
options: RemoveManagedWorktreeOptions
): boolean
export class OrcaRuntimeWithRemoveManagedWorktree extends OrcaRuntimeWithCreateManagedRemoteWorktree {
async removeManagedWorktree(
worktreeSelector: string,
options: RemoveManagedWorktreeOptions = {}
@@ -109,22 +148,23 @@ export class OrcaRuntimeWithRemoveManagedWorktree extends OrcaRuntimeWithCreateM
// One host for the whole removal. Listing on a different host from the one the prune and
// the delete use is how an `executionHostId: 'ssh:*'`-only row got listed remotely and
// deleted here; the route refuses rather than falling back to this machine.
const route = resolveWorktreeRemovalRoute(removalHostId)
// The helper returns for missing/folder repos; this path has a tracked repo and its host.
const route = resolveWorktreeRemovalRoute(removalHostId!)
const { localWorktreeGitOptions, registeredWorktrees } = await listWorktreesOnRemovalRoute(
route,
repo,
repo!,
this.requireStore()
)
const removedMeta = resolveWorktreeRemovalMetadata(
store,
removalTarget.repoId,
removalTarget.id,
removalHostId
removalHostId!
)
const removedPushTarget = removedMeta?.pushTarget ?? removalTarget.pushTarget
const removalHome = resolveWorktreeRemovalHome(route)
const registeredWorktree = findRegisteredDeletableWorktree(
repo.path,
repo!.path,
removalTarget.path,
registeredWorktrees,
removalHome
@@ -134,7 +174,7 @@ export class OrcaRuntimeWithRemoveManagedWorktree extends OrcaRuntimeWithCreateM
}
if (!registeredWorktree) {
return removeRuntimeUnregisteredWorktree({
repo,
repo: repo!,
target: removalTarget,
registeredWorktrees,
removedMeta,
@@ -152,7 +192,7 @@ export class OrcaRuntimeWithRemoveManagedWorktree extends OrcaRuntimeWithCreateM
finishRemoval: () => {
this.preservedBranchCleanup.delete(removalTarget.id, cleanupHostId)
purgeRemovedWorktree()
this.notifyWorktreesChanged(repo.id)
this.notifyWorktreesChanged(repo!.id)
}
})
}
@@ -177,13 +217,13 @@ export class OrcaRuntimeWithRemoveManagedWorktree extends OrcaRuntimeWithCreateM
) {
const removalResult = await removeStaleLocalWorktreeRegistration({
canonicalWorktreePath,
repoPath: repo.path,
repoPath: repo!.path,
localWorktreeGitOptions,
registeredWorktree,
deleteBranch
})
await cleanupUnusedWorktreePushTargetRemote(
repo.path,
repo!.path,
removalTarget.id,
removedPushTarget,
store,
@@ -197,11 +237,11 @@ export class OrcaRuntimeWithRemoveManagedWorktree extends OrcaRuntimeWithCreateM
removedPushTarget
)
purgeRemovedWorktree()
this.notifyWorktreesChanged(repo.id)
this.notifyWorktreesChanged(repo!.id)
return removalResult ?? {}
}
return removeRuntimeRegisteredLocalWorktree({
repo,
repo: repo!,
target: removalTarget,
registeredWorktree,
removedPushTarget,
@@ -236,7 +276,7 @@ export class OrcaRuntimeWithRemoveManagedWorktree extends OrcaRuntimeWithCreateM
purgeRemovedWorktree()
},
onRemoved: emitRemoved,
publish: () => this.publishWorktreeRemovalChange(repo.id)
publish: () => this.publishWorktreeRemovalChange(repo!.id)
})
})
})()
@@ -174,11 +174,11 @@ export abstract class RuntimeWorkspaceRepoScanRefresh extends RuntimeWorkspaceCa
return new Map((await this.listResolvedWorktrees()).map((worktree) => [worktree.id, worktree]))
}
protected invalidateResolvedWorktreeCache(): void {
invalidateResolvedWorktreeCache(): void {
this.resolvedWorktrees.invalidateResolved()
}
protected invalidateWorktreeScanCacheForRepo(repoId: string): void {
invalidateWorktreeScanCacheForRepo(repoId: string): void {
const prefix = `${repoId}\0`
const scopeKeys = new Set(
this.store
@@ -1,5 +1,5 @@
import type { Repo } from '../../shared/repo-types'
import type { Store } from '../persistence'
import type { ProjectRuntimeResolutionStore } from '../local-project-runtime-resolution'
import type { GitWorktreeInfo } from '../../shared/worktree/types'
import type { WorktreeRemovalRoute } from '../worktree-removal-execution-host-route'
import type { LocalProjectWorktreeGitOptions } from '../project-runtime-git-options'
@@ -13,7 +13,7 @@ import { listWorktreesStrict } from '../git/worktree'
export async function listWorktreesOnRemovalRoute(
_route: WorktreeRemovalRoute,
repo: Repo,
store: Store
store: ProjectRuntimeResolutionStore
): Promise<{
localWorktreeGitOptions: LocalProjectWorktreeGitOptions
registeredWorktrees: GitWorktreeInfo[]
+6 -2
View File
@@ -20,7 +20,8 @@ import {
listStructuredSessionsForWorktree,
retireStructuredSessionTabsForWorktree,
unclosedStructuredSessions,
type StructuredSessionInWorkspace
type StructuredSessionInWorkspace,
type StructuredWorktreeSweepRuntime
} from './structured-session-worktree-teardown'
import {
createWorktreeSweepTracker,
@@ -34,8 +35,11 @@ import {
resolveUnstoppedPtyVerdict
} from './unstopped-pty-verification'
export type WorktreeTeardownRuntime = StructuredWorktreeSweepRuntime &
Pick<OrcaRuntimeService, 'stopTerminalsForWorktree' | 'getPtyLivenessVerdict'>
export type WorktreeTeardownDeps = {
runtime?: OrcaRuntimeService
runtime?: WorktreeTeardownRuntime
/** Authoritative id for callers whose selector no longer resolves (orphaned workspace). */
resolvedWorktreeId?: string
/** SSH connection owning `resolvedWorktreeId`; prevents same-id cross-host graph matches. */