Being throttled no longer looks like an empty internet
Before, DuckDuckGo's bot-check page parsed into zero results and the tool answered, successfully, “No results found. Try rephrasing your search.” A model that trusts that answer rephrases, narrows, broadens, and keeps going — the PR author reports losing over an hour inside a single agentic fetch this way.
Now the tool returns a failure: “Failed to search: DuckDuckGo is rate-limiting this machine. Do not retry or rephrase; wait a few minutes or fetch known URLs directly.” The instruction not to retry is part of the payload the model reads.
A page that merely quotes the bot-check wording is now reported as a rate limit
Detection is a plain substring scan over the whole response body, so it also matches result snippets. A genuine results page containing the phrase Unfortunately, bots use DuckDuckGo too — the kind of page you get searching for DuckDuckGo's bot detection itself — returned that result before and returns the rate-limit error now.
The PR adds a false-positive guard test, but its sample page contains none of the three markers, so this path is untested.
Not one of 2541 scenarios noticed the change
Running the pre-change tests against the post-change code moved exactly zero results: internal/agent/tools/search.go had no test file at all before this branch. Every assertion about the new behavior comes from the tests the PR itself adds, and from the probes below.
That is not an argument against the change. It is the reason a diff-reading review would have had nothing to check it against.
What a bot-check response produces
How much of the body the markers are matched against
Which HTTP statuses count as a result page
What the suite can tell you about this file
| What DuckDuckGo returns | on base | on head | moved |
|---|---|---|---|
| Bot-check page, HTTP 202probe P1 | No results found. Try rephrasing your search. | Failed to search: DuckDuckGo is rate-limiting this machine… | changed |
| Bot-check page, HTTP 200probe P2 | No results found. Try rephrasing your search. | Failed to search: DuckDuckGo is rate-limiting this machine… | changed |
| Real results, one snippet quotes the marker textprobe P3 | Found 1 search results: Why DuckDuckGo blocks bots | Failed to search: DuckDuckGo is rate-limiting this machine… | changed, unstated |
| Ordinary results pageprobe P4, the control | Found 1 search results: Example Post | Found 1 search results: Example Post | no change |
| The other 2541 scenarios in ./internal/...base and mix runs | as before | as before | no change |
+var errSearchRateLimited = errors.New(+ "DuckDuckGo is rate-limiting this machine. " ++ "Do not retry or rephrase; wait a few minutes or fetch known URLs directly")++var ddgAnomalyMarkers = []string{+ "anomaly-modal",+ "/anomaly.js",+ "Unfortunately, bots use DuckDuckGo too",+}
- if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusAccepted {+ if resp.StatusCode == http.StatusAccepted {+ return nil, errSearchRateLimited+ }+ if resp.StatusCode != http.StatusOK { return nil, fmt.Errorf("search failed with status code: %d", resp.StatusCode) }
- return parseLiteSearchResults(string(body), maxResults)+ content := string(body)+ for _, marker := range ddgAnomalyMarkers {+ if strings.Contains(content, marker) {+ return nil, errSearchRateLimited+ }+ }++ return parseLiteSearchResults(content, maxResults)
+var ddgLiteEndpoint = "https://lite.duckduckgo.com/lite/?q="- searchURL := "https://lite.duckduckgo.com/lite/?q=" + url.QueryEscape(query)+ searchURL := ddgLiteEndpoint + url.QueryEscape(query) Same literal, now reassignable so a test can point at httptest. Ported to base as-is to make the probes measurable on both sides.
+ internal/agent/tools/search_test.go 76 lines, new file+ internal/agent/tools/testdata/ddg_anomaly_202.html 19 lines, captured 2026-07-29
how this was checked
- base 4dd4442a (the commit before the PR's first commit) → head 3c61d9a3. The PR contributed two commits: the fix and, separately, the tests — comparing the merge commit against its parent would have shown only the tests.
- go test ./internal/... -count=1 -json on three builds: base, head, and a mix of head's production code with base's tests and fixtures. 52 packages, 2541 scenarios; head runs 2544.
- One test fails identically on all three and is discarded as pre-existing: internal/cmd/clientserverrace::TestClientServerSpawnRace.
- Four probes, byte-identical on both sides, against a local httptest server. Measuring base required porting the PR's one-line test seam (ddgLiteEndpoint) back to base; it carries no behavior. The fourth probe is a control that passes on both sides — without it the other three would prove nothing.
what this did not cover
- Real DuckDuckGo responses. Every measurement is against a stub serving the captured page; whether those three markers appear in live result pages, and how often, is unknown.
- A 202 response carrying genuine results. The new branch would reject it, but there is no evidence such a response exists, so no probe was written.
- The rest of the search tool: rate-limit backoff timing, header randomization, and URL cleaning were not exercised beyond what the existing suite already runs.