mirror of
https://github.com/stablyai/orca.git
synced 2026-10-31 17:17:38 +00:00
refactor(runtime): check workspace removal and recovery ownership (#27624)
This commit is contained in:
@@ -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)) {
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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' }
|
||||
|
||||
+33
-14
@@ -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)
|
||||
+43
-26
@@ -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
|
||||
+75
-35
@@ -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[]
|
||||
|
||||
@@ -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. */
|
||||
|
||||
Reference in New Issue
Block a user