fix: preserve native model switches and bound project searches
This commit is contained in:
1 parent
db5e876f40
commit
e7d3080e0a
14 files changed
+371
-35
No files matched your search
+56
-12
@@ -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
|
||||
|
||||
Reference in new issue
Block a user