From 06901f1c7658df9570516a70fc7c76f75e41e0b9 Mon Sep 17 00:00:00 2001 From: mastertyko <11311479+mastertyko@users.noreply.github.com> Date: Mon, 23 Mar 2026 16:52:34 +0100 Subject: [PATCH] fix(search): keep duplicate-search loop guard armed (#2117) --- .../extensions/search-the-web/tool-search.ts | 6 ++-- src/tests/search-loop-guard.test.ts | 33 ++++++++++++++++--- 2 files changed, 32 insertions(+), 7 deletions(-) diff --git a/src/resources/extensions/search-the-web/tool-search.ts b/src/resources/extensions/search-the-web/tool-search.ts index 54dab89b0..399a399df 100644 --- a/src/resources/extensions/search-the-web/tool-search.ts +++ b/src/resources/extensions/search-the-web/tool-search.ts @@ -398,16 +398,16 @@ export function registerSearchTool(pi: ExtensionAPI) { // with brief interruptions every MAX_CONSECUTIVE_DUPES+1 calls. if (cacheKey === lastSearchKey) { consecutiveDupeCount++; - if (consecutiveDupeCount >= MAX_CONSECUTIVE_DUPES) { + if (consecutiveDupeCount > MAX_CONSECUTIVE_DUPES) { return { - content: [{ type: "text" as const, text: `⚠️ Search loop detected: the query "${params.query}" has been searched ${consecutiveDupeCount + 1} times consecutively with identical results. The information you need is already in the previous search results above. Stop searching and use those results to proceed with your task.` }], + content: [{ type: "text" as const, text: `⚠️ Search loop detected: the query "${params.query}" has been searched ${consecutiveDupeCount} times consecutively with identical results. The information you need is already in the previous search results above. Stop searching and use those results to proceed with your task.` }], isError: true, details: { errorKind: "search_loop", error: "Consecutive duplicate search detected" } satisfies Partial, }; } } else { lastSearchKey = cacheKey; - consecutiveDupeCount = 0; + consecutiveDupeCount = 1; } const cached = searchCache.get(cacheKey); diff --git a/src/tests/search-loop-guard.test.ts b/src/tests/search-loop-guard.test.ts index 266b5155a..6413bef32 100644 --- a/src/tests/search-loop-guard.test.ts +++ b/src/tests/search-loop-guard.test.ts @@ -14,6 +14,23 @@ import assert from "node:assert/strict"; import { registerSearchTool } from "../resources/extensions/search-the-web/tool-search.ts"; import searchExtension from "../resources/extensions/search-the-web/index.ts"; +const ORIGINAL_ENV = { + BRAVE_API_KEY: process.env.BRAVE_API_KEY, + TAVILY_API_KEY: process.env.TAVILY_API_KEY, + OLLAMA_API_KEY: process.env.OLLAMA_API_KEY, +}; + +function restoreSearchEnv() { + if (ORIGINAL_ENV.BRAVE_API_KEY === undefined) delete process.env.BRAVE_API_KEY; + else process.env.BRAVE_API_KEY = ORIGINAL_ENV.BRAVE_API_KEY; + + if (ORIGINAL_ENV.TAVILY_API_KEY === undefined) delete process.env.TAVILY_API_KEY; + else process.env.TAVILY_API_KEY = ORIGINAL_ENV.TAVILY_API_KEY; + + if (ORIGINAL_ENV.OLLAMA_API_KEY === undefined) delete process.env.OLLAMA_API_KEY; + else process.env.OLLAMA_API_KEY = ORIGINAL_ENV.OLLAMA_API_KEY; +} + // ============================================================================= // Mock helpers // ============================================================================= @@ -101,6 +118,8 @@ async function callSearch( test("search loop guard fires after MAX_CONSECUTIVE_DUPES duplicates", async () => { process.env.BRAVE_API_KEY = "test-key-loop-guard"; + delete process.env.TAVILY_API_KEY; + delete process.env.OLLAMA_API_KEY; const restoreFetch = mockFetch(makeBraveResponse()); try { @@ -127,12 +146,14 @@ test("search loop guard fires after MAX_CONSECUTIVE_DUPES duplicates", async () ); } finally { restoreFetch(); - delete process.env.BRAVE_API_KEY; + restoreSearchEnv(); } }); test("search loop guard resets at session_start boundary", async () => { process.env.BRAVE_API_KEY = "test-key-loop-guard-session"; + delete process.env.TAVILY_API_KEY; + delete process.env.OLLAMA_API_KEY; const restoreFetch = mockFetch(makeBraveResponse()); const query = "session boundary query"; @@ -167,12 +188,14 @@ test("search loop guard resets at session_start boundary", async () => { ); } finally { restoreFetch(); - delete process.env.BRAVE_API_KEY; + restoreSearchEnv(); } }); test("search loop guard stays armed after firing — subsequent duplicates immediately re-trigger (#1671)", async () => { process.env.BRAVE_API_KEY = "test-key-loop-guard-2"; + delete process.env.TAVILY_API_KEY; + delete process.env.OLLAMA_API_KEY; const restoreFetch = mockFetch(makeBraveResponse()); // Use a unique query so module-level state from previous test doesn't interfere @@ -209,12 +232,14 @@ test("search loop guard stays armed after firing — subsequent duplicates immed ); } finally { restoreFetch(); - delete process.env.BRAVE_API_KEY; + restoreSearchEnv(); } }); test("search loop guard resets cleanly when a different query is issued", async () => { process.env.BRAVE_API_KEY = "test-key-loop-guard-3"; + delete process.env.TAVILY_API_KEY; + delete process.env.OLLAMA_API_KEY; const restoreFetch = mockFetch(makeBraveResponse()); const queryA = "query alpha reset test"; @@ -239,6 +264,6 @@ test("search loop guard resets cleanly when a different query is issued", async ); } finally { restoreFetch(); - delete process.env.BRAVE_API_KEY; + restoreSearchEnv(); } });