fix(search): zero-match probes return the file paths they found, not just counts

The casing/hidden/literal probes already ran the widened search to produce
their counts, then threw away the paths and returned a hint-only warning.
Strong models pivot in one turn; weak models spiral — the A/B eval measured
qwen3-coder-30b going 3.3 -> 9.3 turns on err_case_search, retrying casing
variants the probe had already resolved.

All three probes (case-insensitive, hidden/gitignored, literal-vs-regex) now
include up to 5 matched paths (+N more) in the warning via a shared tally
helper. Fixes the class, not the site.

Closes #80522
This commit is contained in:
Teknium 2026-08-06 11:20:56 -07:00
parent 9d4ef04ed0
commit d273e0f2fa
No known key found for this signature in database
2 changed files with 46 additions and 23 deletions

View File

@ -27,12 +27,21 @@ class TestZeroMatchProbe:
assert r["total_count"] == 0
assert "case-insensitive" in r.get("warning", "")
def test_case_mismatch_hint_names_the_files(self, proj):
# The probe already ran the -i search; it must hand over the paths,
# not just a count (issue #80522: hint-only sent weak models into
# 5-search casing-variant spirals — +6 turns measured on the A/B eval).
r = json.loads(search_tool("token_alpha", path=str(proj / "proj"), task_id="t-zm"))
w = r.get("warning", "")
assert "a.py" in w and "b.py" in w
def test_regex_metachar_literal_hint(self, proj):
d = proj / "proj"
(d / "meta.py").write_text("result = lookup[key+1]\n")
r = json.loads(search_tool("lookup[key+1]", path=str(d), task_id="t-zm"))
assert r["total_count"] == 0
assert "literal match" in r.get("warning", "")
assert "meta.py" in r.get("warning", "")
def test_true_zero_match_no_hint(self, proj):
r = json.loads(search_tool("zzz_totally_absent_zzz", path=str(proj / "proj"), task_id="t-zm"))
@ -46,6 +55,17 @@ class TestZeroMatchProbe:
r = json.loads(search_tool("HIDDEN_ONLY_TOKEN", path=str(d), task_id="t-zm"))
assert r["total_count"] == 0
assert "hidden or gitignored" in r.get("warning", "")
# Same class as the casing probe: the path must be in the hint.
assert "conf.cfg" in r.get("warning", "")
def test_probe_path_list_is_capped(self, proj):
d = proj / "proj"
for i in range(8):
(d / f"cap{i}.txt").write_text("capped_case_token = 1\n")
r = json.loads(search_tool("CAPPED_CASE_TOKEN", path=str(d), task_id="t-zm"))
w = r.get("warning", "")
assert "case-insensitive" in w
assert "+3 more" in w # 8 files, 5 shown
def test_matching_search_unaffected(self, proj):
r = json.loads(search_tool("TOKEN_ALPHA", path=str(proj / "proj"), task_id="t-zm"))

View File

@ -2308,6 +2308,23 @@ class ShellFileOperations(FileOperations):
"""
if not self._has_command('rg'):
return None
def _tally(stdout: str):
"""Parse ``path:count`` lines from rg --count-matches."""
total = 0
per_file = []
for line in (stdout or "").strip().splitlines():
p, _sep, n = line.rpartition(":")
if n.isdigit():
total += int(n)
per_file.append(p)
return total, per_file
def _paths_note(per_file, cap: int = 5) -> str:
shown = ", ".join(per_file[:cap])
extra = len(per_file) - cap
return shown + (f" (+{extra} more)" if extra > 0 else "")
glob_expr = f" --glob {self._escape_shell_arg(file_glob)}" if file_glob else ""
probe = self._exec(
f"rg -i --count-matches{glob_expr} "
@ -2315,17 +2332,12 @@ class ShellFileOperations(FileOperations):
f"2>/dev/null | head -50",
timeout=30,
)
ci_total = 0
ci_files = 0
for line in (probe.stdout or "").strip().splitlines():
_p, _sep, n = line.rpartition(":")
if n.isdigit():
ci_total += int(n)
ci_files += 1
ci_total, ci_paths = _tally(probe.stdout)
if ci_total > 0:
return (
f"0 exact matches, but {ci_total} case-insensitive match(es) "
f"in {ci_files} file(s) — the pattern's casing may be wrong."
f"in {len(ci_paths)} file(s): {_paths_note(ci_paths)}"
"the pattern's casing may be wrong."
)
# Hidden/ignored probe: rg skips dotdirs and .gitignore'd files by
# default. When the pattern exists only there, say so instead of
@ -2337,18 +2349,12 @@ class ShellFileOperations(FileOperations):
f"2>/dev/null | head -50",
timeout=30,
)
h_total = 0
h_files = 0
for line in (hidden.stdout or "").strip().splitlines():
_p, _sep, n = line.rpartition(":")
if n.isdigit():
h_total += int(n)
h_files += 1
h_total, h_paths = _tally(hidden.stdout)
if h_total > 0:
return (
f"0 matches in visible files, but {h_total} match(es) in "
f"{h_files} hidden or gitignored file(s) — these are excluded "
"by default. Search the hidden path explicitly to include them."
f"{len(h_paths)} hidden or gitignored file(s): "
f"{_paths_note(h_paths)} — these are excluded by default."
)
if re.search(r"[.\[\](){}?*+^$\\|]", pattern):
fixed = self._exec(
@ -2357,14 +2363,11 @@ class ShellFileOperations(FileOperations):
f"2>/dev/null | head -50",
timeout=30,
)
f_total = sum(
int(line.rpartition(":")[2])
for line in (fixed.stdout or "").strip().splitlines()
if line.rpartition(":")[2].isdigit()
)
f_total, f_paths = _tally(fixed.stdout)
if f_total > 0:
return (
f"0 regex matches, but {f_total} literal match(es) — the "
f"0 regex matches, but {f_total} literal match(es) in "
f"{len(f_paths)} file(s): {_paths_note(f_paths)} — the "
"pattern contains regex metacharacters that likely need "
"escaping (or pass a simpler substring)."
)