Impact & scope
With many workspace windows open (observed at ~6), interactive foreground work across the app stalls:
- Workspace switch can hang indefinitely on the "Switching workspace…" overlay; the overlay's hide path is never reached because the switch's root-load phase is parked behind process-wide concurrency pools that the other windows have saturated.
- Agent Mode dispatches that create a worktree (
agent_run start with worktree_create) stall during worktree preparation; the dispatch hangs and the session never launches.
In both cases the main run loop stays healthy (the app is not deadlocked and remains interactive); the foreground Task is genuinely suspended at an await.
Root mechanism: switch-critical and agent-start git work both run at the foreground admission tier, but the process-wide git admission pool has no reserved lane for foreground work, so sustained background codemap/metadata git demand from multiple windows can keep all slots busy and starve foreground waiters.
Expected vs actual
- Expected: switching workspaces completes promptly (overlay hides within ~1–2s) and agent worktree dispatches start promptly, even with many windows open.
- Actual: at ~6 open windows, a switch sits on the overlay for tens of seconds to effectively forever, and agent worktree dispatches stall in preparation. CPU stays partly busy (background work) while the foreground work waits.
Reproduction
Workspace switch:
- Open N workspace windows (N ≈ 6), including at least one large repo (hundreds of MB, thousands of filesystem entries, git submodules).
- Switch into the large workspace from a window.
- The "Switching workspace…" overlay (with Cancel) appears and does not dismiss.
Agent dispatch:
- Under the same multi-window load, start an agent run that creates a worktree.
- The dispatch hangs in worktree preparation; the session does not launch.
sample <pid> of the running app in either case shows: main thread mostly idle in the run loop; worker threads parked in GitService.runAdmittedGitData at withCheckedThrowingContinuation (admission wait), with WorkspaceCodemapBindingEngine.runProjectionPreload / processProjectionBatch feeding git demand. For the dispatch case, a thread is parked for the whole sampling window in AgentRunMCPToolService.executeStart → AgentMCPStartWorktreeCoordinator.prepare(...); for the switch case, one in WorkspaceFileContextStore.loadRoot.
Frequency: intermittent but reliably triggered once total open windows push shared pool demand past capacity. The largest/noisiest workspace (submodules, many entries) is typically the one that tips over.
Environment
- RepoPrompt CE release app, version 1.0.24 (build 25), arm64, macOS 15.7.1.
Evidence (confirmed vs hypothesis)
Confirmed from code:
GitProcessAdmissionController is a process-wide singleton (Sources/RepoPrompt/Infrastructure/VCS/GitProcessAdmissionController.swift): globalLimit = 8, perRepositoryLimit = 2, weighted service ring (rootBootstrap:userInitiatedAuthority:codemapDemand:background ≈ 8:4:2:1). Grants are non-preemptive and there is no reserved lane for foreground work, so when all slots are busy with background/codemap git, a foreground rootBootstrap/userInitiatedAuthority waiter has priority weight but no slot to run in.
- Both affected flows run git at the foreground tier: workspace-switch root-bootstrap, and agent worktree creation (
GitService.createWorktree → runAdmittedGitData, whose default admissionPriority = .rootBootstrap lives in Sources/RepoPrompt/Infrastructure/VCS/GitService.swift). Agent worktree preparation also calls transitionWorktreeBindings → WorkspaceFileContextStore.loadRoot on the new worktree.
ContentReadAsyncLimiter is a second process-wide actor (Sources/RepoPrompt/Infrastructure/FileSystem/FileSystemService+ContentLoading.swift, capacity max(2, min(4, activeProcessorCount))); each loadRoot holds a foreground permit for the entire directory walk. Unlike the git pool, this limiter already isolates foreground from codemap permits.
loadWorkspaceFolders ends with a while currentCatalogGeneration != indexGeneration { rebuildIndex } convergence loop (Sources/RepoPrompt/Features/Workspaces/ViewModels/WorkspaceManagerViewModel.swift) with no iteration cap or deadline; a noisy watcher on a large/submodule repo can keep advancing the catalog generation so the index rebuild chases a moving target.
Hypothesis: the git admission controller is the binding constraint — it is the one pool that lacks the foreground-priority reservation the content-read limiter already enforces. The per-repository limit (2) compounds this for a single repo that has accumulated background codemap/metadata demand.
Scope / non-goals
- In scope: stop interactive foreground work (workspace switch + agent worktree dispatch) from being starved by background codemap/metadata demand across windows; bound the catalog/index convergence loop.
- Non-goals: removing the admission pools, or changing single-window performance characteristics.
Acceptance criteria
- Switching into a large workspace with ~6 windows open dismisses the overlay and becomes interactive promptly (no multi-tens-of-seconds stall).
- Starting an agent run that creates a worktree, under the same multi-window load, completes worktree preparation and launches the session promptly.
- Under sustained background codemap git demand from other windows, foreground-tier git admission (
rootBootstrap, userInitiatedAuthority) is granted within a bounded queue-wait, observable via GitProcessAdmissionController snapshots.
- The catalog/index convergence loop always terminates within a bounded iteration count or deadline.
Suggested regression coverage
- Admission test: background/codemap leases saturating
globalLimit, then a foreground rootBootstrap acquire(...) (representing either a switch or an agent worktree-create) completes within a bounded time.
- Convergence-loop test: a synthetic catalog that advances every snapshot does not cause unbounded
rebuildIndex iterations.
Impact & scope
With many workspace windows open (observed at ~6), interactive foreground work across the app stalls:
agent_runstart withworktree_create) stall during worktree preparation; the dispatch hangs and the session never launches.In both cases the main run loop stays healthy (the app is not deadlocked and remains interactive); the foreground
Taskis genuinely suspended at anawait.Root mechanism: switch-critical and agent-start git work both run at the foreground admission tier, but the process-wide git admission pool has no reserved lane for foreground work, so sustained background codemap/metadata git demand from multiple windows can keep all slots busy and starve foreground waiters.
Expected vs actual
Reproduction
Workspace switch:
Agent dispatch:
sample <pid>of the running app in either case shows: main thread mostly idle in the run loop; worker threads parked inGitService.runAdmittedGitDataatwithCheckedThrowingContinuation(admission wait), withWorkspaceCodemapBindingEngine.runProjectionPreload/processProjectionBatchfeeding git demand. For the dispatch case, a thread is parked for the whole sampling window inAgentRunMCPToolService.executeStart→AgentMCPStartWorktreeCoordinator.prepare(...); for the switch case, one inWorkspaceFileContextStore.loadRoot.Frequency: intermittent but reliably triggered once total open windows push shared pool demand past capacity. The largest/noisiest workspace (submodules, many entries) is typically the one that tips over.
Environment
Evidence (confirmed vs hypothesis)
Confirmed from code:
GitProcessAdmissionControlleris a process-wide singleton (Sources/RepoPrompt/Infrastructure/VCS/GitProcessAdmissionController.swift):globalLimit = 8,perRepositoryLimit = 2, weighted service ring (rootBootstrap:userInitiatedAuthority:codemapDemand:background ≈ 8:4:2:1). Grants are non-preemptive and there is no reserved lane for foreground work, so when all slots are busy with background/codemap git, a foregroundrootBootstrap/userInitiatedAuthoritywaiter has priority weight but no slot to run in.GitService.createWorktree→runAdmittedGitData, whose defaultadmissionPriority = .rootBootstraplives inSources/RepoPrompt/Infrastructure/VCS/GitService.swift). Agent worktree preparation also callstransitionWorktreeBindings→WorkspaceFileContextStore.loadRooton the new worktree.ContentReadAsyncLimiteris a second process-wide actor (Sources/RepoPrompt/Infrastructure/FileSystem/FileSystemService+ContentLoading.swift, capacitymax(2, min(4, activeProcessorCount))); eachloadRootholds a foreground permit for the entire directory walk. Unlike the git pool, this limiter already isolates foreground from codemap permits.loadWorkspaceFoldersends with awhile currentCatalogGeneration != indexGeneration { rebuildIndex }convergence loop (Sources/RepoPrompt/Features/Workspaces/ViewModels/WorkspaceManagerViewModel.swift) with no iteration cap or deadline; a noisy watcher on a large/submodule repo can keep advancing the catalog generation so the index rebuild chases a moving target.Hypothesis: the git admission controller is the binding constraint — it is the one pool that lacks the foreground-priority reservation the content-read limiter already enforces. The per-repository limit (2) compounds this for a single repo that has accumulated background codemap/metadata demand.
Scope / non-goals
Acceptance criteria
rootBootstrap,userInitiatedAuthority) is granted within a bounded queue-wait, observable viaGitProcessAdmissionControllersnapshots.Suggested regression coverage
globalLimit, then a foregroundrootBootstrapacquire(...)(representing either a switch or an agent worktree-create) completes within a bounded time.rebuildIndexiterations.