fix: preserve native model switches and bound project searches
Server CI and Windows package / verify (push) Failing after 1m40s
Server CI and Windows package / windows-package (push) Skipped

This commit is contained in:
yilsem committed 2026-10-13 16:32:50 +03:00
1 parent db5e876f40
commit e7d3080e0a
14 files changed
+371 -35

No files matched your search

+56 -12
View File
@@ -50,7 +50,8 @@ from agent.file_safety import (
get_write_denied_error,
is_write_denied as _shared_is_write_denied,
)
from agent.search_policy import SEARCH_PRUNE_DIR_NAMES
from agent.search_policy import (SEARCH_PRUNE_DIR_NAMES, SEARCH_RG_THREADS,
SEARCH_MAX_CONCURRENT, local_search_scope)
from tools import interrupt as tool_interrupt
logger = logging.getLogger(__name__)
@@ -73,6 +74,8 @@ _MACOS_TCC_PROTECTED_HOME_DIRS = (
)
_SEARCH_SLOTS = threading.BoundedSemaphore(SEARCH_MAX_CONCURRENT)
_FILENAME_SEARCH_ADMISSION = threading.Condition()
_ACTIVE_FILENAME_SEARCH_ROOTS: set[tuple[str, str, str]] = set()
_FILENAME_SEARCH_WAIT_SECONDS = 0.05
@@ -3432,7 +3435,31 @@ class ShellFileOperations(FileOperations):
def search(self, pattern: str, path: str = ".", target: str = "content",
file_glob: Optional[str] = None, limit: int = 50, offset: int = 0,
output_mode: str = "content", context: int = 0,
order: str = "discovery") -> SearchResult:
order: str = "discovery", allow_broad: bool = False) -> SearchResult:
# One admission slot covers discovery, traversal and zero-match probes.
# It is process-wide across file-ops instances, and interruptible.
while not _SEARCH_SLOTS.acquire(timeout=0.05):
if tool_interrupt.is_interrupted():
return SearchResult(error="Search interrupted while waiting for a search slot.")
try:
if tool_interrupt.is_interrupted():
return SearchResult(error="Search interrupted.")
return self._search_scoped(pattern, path, target, file_glob, limit,
offset, output_mode, context, order, allow_broad)
finally:
_SEARCH_SLOTS.release()
def _project_search_scope(self, path, allow_broad=False):
from tools.environments.local import LocalEnvironment, _msys_to_windows_path
if allow_broad or not isinstance(self.env, LocalEnvironment):
return path, None, None
cwd = getattr(self.env, "cwd", None) or self.cwd
return local_search_scope(_msys_to_windows_path(path), cwd, _HOME)
def _search_scoped(self, pattern: str, path: str = ".", target: str = "content",
file_glob: Optional[str] = None, limit: int = 50, offset: int = 0,
output_mode: str = "content", context: int = 0,
order: str = "discovery", allow_broad: bool = False) -> SearchResult:
"""
Search for content or files.
@@ -3460,6 +3487,9 @@ class ShellFileOperations(FileOperations):
# Expand ~ and other shell paths
path = self._expand_path(path)
path, scope_notice, scope_error = self._project_search_scope(path, allow_broad)
if scope_error:
return SearchResult(error=scope_error)
# Validate that the path exists before searching
check = self._exec(f"test -e {self._escape_shell_arg(path)} && echo exists || echo not_found")
@@ -3470,7 +3500,7 @@ class ShellFileOperations(FileOperations):
# merge the results, and report the skipped parts.
multi = self._try_multi_path_search(
pattern, path, target, file_glob, limit, offset, output_mode, context,
order,
order, allow_broad,
)
if multi is not None:
return multi
@@ -3518,6 +3548,8 @@ class ShellFileOperations(FileOperations):
f"an unattended privacy prompt: {skipped}. Search a protected "
"folder directly when access is intentional."
)
if scope_notice:
result.warning = " ".join(filter(None, [scope_notice, result.warning]))
return result
def _macos_search_exclusions(self, path: str) -> List[str]:
@@ -3587,7 +3619,7 @@ class ShellFileOperations(FileOperations):
def _try_multi_path_search(self, pattern: str, path: str, target: str,
file_glob: Optional[str], limit: int, offset: int,
output_mode: str, context: int,
order: str = "discovery") -> Optional[SearchResult]:
order: str = "discovery", allow_broad: bool = False) -> Optional[SearchResult]:
"""Recover a not-found ``path`` that is really several paths in one string.
Production trajectories show models passing "dir1 dir2 dir3" (or
@@ -3604,9 +3636,14 @@ class ShellFileOperations(FileOperations):
parts = path.split()
if len(parts) < 2:
return None
existing, missing = [], []
existing, missing, scope_notices = [], [], []
for p in parts:
expanded = self._expand_path(p)
expanded, notice, error = self._project_search_scope(expanded, allow_broad)
if error:
return SearchResult(error=error)
if notice:
scope_notices.append(notice)
chk = self._exec(
f"test -e {self._escape_shell_arg(expanded)} && echo exists || echo not_found"
)
@@ -3641,7 +3678,7 @@ class ShellFileOperations(FileOperations):
note += "; skipped missing: " + ", ".join(missing[:3])
if len(missing) > 3:
note += f" (+{len(missing) - 3} more)"
warning_parts = [note]
warning_parts = [note, *scope_notices]
if not merged.error:
protected_paths = [
absolute
@@ -3704,7 +3741,7 @@ class ShellFileOperations(FileOperations):
glob_expr = f" --glob {self._escape_shell_arg(file_glob)}" if file_glob else ""
probe = self._exec(
f"{rg} -i --count-matches{glob_expr} "
f"{rg} --threads {SEARCH_RG_THREADS} -i --count-matches{glob_expr} {self._search_prune_glob_args()} "
f"{self._escape_shell_arg(pattern)} {self._escape_native_tool_arg(path)} "
f"2>/dev/null | head -50",
timeout=30,
@@ -3723,7 +3760,7 @@ class ShellFileOperations(FileOperations):
# files remain diagnosable, but prune heavyweight trees before rg can
# recurse into them.
hidden = self._exec(
f"{rg} --hidden --no-ignore --count-matches{glob_expr}"
f"{rg} --threads {SEARCH_RG_THREADS} --hidden --no-ignore --count-matches{glob_expr}"
f" {self._search_prune_glob_args()} "
f"{self._escape_shell_arg(pattern)} {self._escape_native_tool_arg(path)} "
f"2>/dev/null | head -50",
@@ -3738,7 +3775,7 @@ class ShellFileOperations(FileOperations):
)
if re.search(r"[.\[\](){}?*+^$\\|]", pattern):
fixed = self._exec(
f"{rg} -F --count-matches{glob_expr} "
f"{rg} --threads {SEARCH_RG_THREADS} -F --count-matches{glob_expr} {self._search_prune_glob_args()} "
f"{self._escape_shell_arg(pattern)} {self._escape_native_tool_arg(path)} "
f"2>/dev/null | head -50",
timeout=30,
@@ -3813,7 +3850,7 @@ class ShellFileOperations(FileOperations):
))
try:
return self._search_files_rg(
search_pattern, path, limit, offset, order,
pattern, path, limit, offset, order,
rg_executable=rg_executable,
)
finally:
@@ -3848,6 +3885,9 @@ class ShellFileOperations(FileOperations):
hidden_prune = (
f" \\( -type d -name '.*'{root_exemptions} \\) -prune -o"
)
names = " -o ".join(f"-name {self._escape_shell_arg(name)}"
for name in sorted(SEARCH_PRUNE_DIR_NAMES))
hidden_prune += f" \\( -type d \\( {names} \\){root_exemptions} \\) -prune -o"
protected_paths = [
absolute
for _root, _relative, absolute
@@ -3986,7 +4026,7 @@ class ShellFileOperations(FileOperations):
f"cd {self._escape_shell_arg(scoped_common)} && " if scoped_common else ""
)
cmd = (
f"set -o pipefail; {cd_prefix}{rg} --files{sort_arg} -g {self._escape_shell_arg(glob_pattern)}"
f"set -o pipefail; {cd_prefix}{rg} --files{sort_arg} --threads {SEARCH_RG_THREADS} -g {self._escape_shell_arg(glob_pattern)} {self._search_prune_glob_args()}"
f"{exclusion_args} -- {root_args} 2>/dev/null | head -n {fetch_limit}"
)
result = self._exec(cmd, timeout=60)
@@ -4062,7 +4102,7 @@ class ShellFileOperations(FileOperations):
rg_executable = rg_executable or self._resolve_command("rg")
if not rg_executable:
return SearchResult(error="Content search requires ripgrep (rg).")
cmd_parts = [self._quote_executable(rg_executable), "--line-number", "--no-heading", "--with-filename"]
cmd_parts = [self._quote_executable(rg_executable), "--threads", str(SEARCH_RG_THREADS), "--line-number", "--no-heading", "--with-filename"]
# Giant-single-line containment (ported from cline/cline#13525): a
# match inside a serialized dump (multi-MB single-line JSON/minified
@@ -4100,6 +4140,8 @@ class ShellFileOperations(FileOperations):
if file_glob:
cmd_parts.extend(["--glob", self._escape_shell_arg(file_glob)])
cmd_parts.append(self._search_prune_glob_args())
# Output mode handling
if output_mode == "files_only":
cmd_parts.append("-l") # Files only
@@ -4230,6 +4272,8 @@ class ShellFileOperations(FileOperations):
# Exclude hidden directories (matching ripgrep's default behavior).
# This prevents searching inside .hub/index-cache/, .git/, etc.
cmd_parts.append("--exclude-dir='.*'")
cmd_parts.extend(f"--exclude-dir={self._escape_shell_arg(name)}"
for name in sorted(SEARCH_PRUNE_DIR_NAMES))
# Protected-dir pruning CANNOT use --exclude-dir here: grep matches
# exclude-dir globs against BASENAMES anywhere in the tree, so