@probelabs/probe 0.6.0-rc302 → 0.6.0-rc303

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -248,8 +248,23 @@ function buildSearchDelegateTask({ searchQuery, searchPath, exact, language, all
248
248
  '- Searching "getUserData" ALREADY matches "get", "user", "data" and their variations.',
249
249
  '- NEVER repeat the same search query — you will get the same results. Changing the path does NOT change this.',
250
250
  '- NEVER search trivial variations of the same keyword (e.g., AllowedIPs then allowedIps then allowed_ips). This is wasteful — probe handles it.',
251
- '- If a search returns no results, the term likely does not exist. Try a genuinely DIFFERENT keyword or concept, not a variation.',
252
- '- If 2-3 searches return no results for a concept, STOP searching for it and move on. Do NOT keep retrying.',
251
+ '',
252
+ 'When a search returns no results:',
253
+ '- If you searched a SUBFOLDER (e.g., path="gateway/"), the term might exist elsewhere.',
254
+ ' Try searching from the workspace root (omit the path parameter) or a different directory.',
255
+ ' But do NOT retry the same subfolder with different quoting — that will not help.',
256
+ '- If you searched the WORKSPACE ROOT and got no results, the term does not exist in this codebase.',
257
+ ' Changing quotes, adding "func " prefix, or switching to method syntax will NOT help.',
258
+ '- These are ALL the same failed search, NOT different searches:',
259
+ ' search("func ctxGetData") → no results',
260
+ ' search("ctxGetData") → no results ← WASTED, same concept, different quoting',
261
+ ' search(ctxGetData) → no results ← WASTED, same concept, no quotes',
262
+ ' search("ctx.GetData") → no results ← WASTED, method syntax of same concept',
263
+ ' After the FIRST "no results" at a given scope, either widen the search path or try',
264
+ ' a fundamentally different approach: search for a broader concept, use listFiles',
265
+ ' to discover actual function names, or extract a known file to read real code.',
266
+ '- If 2 searches return no results for a concept (across different scopes), the code likely',
267
+ ' uses different naming than you expect — discover the real names via extract or listFiles.',
253
268
  '',
254
269
  'When to use exact=true:',
255
270
  '- Use exact=true when searching for a KNOWN symbol name (function, type, variable, struct).',
@@ -302,6 +317,21 @@ function buildSearchDelegateTask({ searchQuery, searchPath, exact, language, all
302
317
  ' → search "ForwardMessage" → search "ForwardMessage" → search "ForwardMessage" (WRONG: repeating the exact same query)',
303
318
  ' → search "authentication" → wait → search "session management" → wait (WRONG: these are independent, run them in parallel)',
304
319
  '',
320
+ ' WORST pattern — retrying a non-existent function with quote/syntax variations (this wastes 30 minutes):',
321
+ ' → search "func ctxGetData" → no results',
322
+ ' → search "ctxGetData" → no results ← WRONG: same term without "func" prefix',
323
+ ' → search "ctx.GetData" → no results ← WRONG: method syntax of same concept',
324
+ ' → search "ctx.SetData" → no results ← WRONG: Set variant of same concept',
325
+ ' → search ctxGetData → no results ← WRONG: unquoted version of same term',
326
+ ' → extract api.go → extract api.go → extract api.go (8 times!) ← WRONG: re-reading same file',
327
+ ' FIX: After "func ctxGetData" returns no results in gateway/:',
328
+ ' Option A: Widen scope — search from the workspace root (omit path) in case the',
329
+ ' function is defined in a different package (e.g., apidef/, user/, config/).',
330
+ ' Option B: Discover real names — extract a file you KNOW uses context (e.g., a',
331
+ ' middleware file) and READ what functions it actually calls.',
332
+ ' Option C: Browse — use listFiles to see what files exist and extract the relevant ones.',
333
+ ' NEVER: retry the same concept with different quoting in the same directory.',
334
+ '',
305
335
  'Keyword tips:',
306
336
  '- Common programming keywords are filtered as stopwords when unquoted: function, class, return, new, struct, impl, var, let, const, etc.',
307
337
  '- Avoid searching for these alone — combine with a specific term (e.g., "middleware function" is fine, "function" alone is too generic).',
@@ -340,7 +370,7 @@ function buildSearchDelegateTask({ searchQuery, searchPath, exact, language, all
340
370
  ' - Type references and imports → include type definitions.',
341
371
  ' - Registered handlers/middleware → include all registered items.',
342
372
  '6. If a search returns results, use extract to verify relevance. Run multiple extracts in parallel too.',
343
- '7. If a search returns NO results, the term does not exist. Do NOT retry with variations. Move on.',
373
+ '7. If a search returns NO results: widen the path scope if you searched a subfolder, or move on. Do NOT retry with quote/syntax variations they search the same index.',
344
374
  '8. Once you have enough targets (typically 5-15), output your final JSON answer immediately.',
345
375
  '',
346
376
  `Query: ${searchQuery}`,
@@ -388,8 +418,30 @@ export const searchTool = (options = {}) => {
388
418
  const dupBlockCounts = new Map();
389
419
  // Track pagination counts per query to cap runaway pagination
390
420
  const paginationCounts = new Map();
421
+ // Track consecutive no-result searches (circuit breaker)
422
+ let consecutiveNoResults = 0;
423
+ const MAX_CONSECUTIVE_NO_RESULTS = 4;
424
+ // Track normalized query concepts for fuzzy dedup (catches quote/syntax variations)
425
+ const failedConcepts = new Map(); // normalizedKey → count
391
426
  const MAX_PAGES_PER_QUERY = 3;
392
427
 
428
+ /**
429
+ * Normalize a search query to detect syntax-level duplicates.
430
+ * Strips quotes, dots, underscores/hyphens, and lowercases.
431
+ * "ctxGetData", "ctx.GetData", "ctx_get_data" all → "ctxgetdata"
432
+ * Note: does NOT strip language keywords (func, type) — those change search
433
+ * semantics and are already handled as stopwords by the Rust search engine.
434
+ */
435
+ function normalizeQueryConcept(query) {
436
+ if (!query) return '';
437
+ return query
438
+ .replace(/^["']|["']$/g, '') // strip outer quotes
439
+ .replace(/\./g, '') // "ctx.GetData" → "ctxGetData"
440
+ .replace(/[_\-\s]+/g, '') // strip underscores/hyphens/spaces
441
+ .toLowerCase()
442
+ .trim();
443
+ }
444
+
393
445
  return tool({
394
446
  name: 'search',
395
447
  description: searchDelegate
@@ -478,6 +530,35 @@ export const searchTool = (options = {}) => {
478
530
  }
479
531
  previousSearches.set(searchKey, { hadResults: false });
480
532
  paginationCounts.set(searchKey, 0);
533
+
534
+ // Fuzzy concept dedup: catch quote/syntax variations of the same failed concept
535
+ // e.g., "func ctxGetData", "ctxGetData", "ctx.GetData" all normalize to "ctxgetdata"
536
+ const normalizedKey = `${searchPath}::${normalizeQueryConcept(searchQuery)}`;
537
+ if (failedConcepts.has(normalizedKey) && failedConcepts.get(normalizedKey) >= 2) {
538
+ const conceptCount = failedConcepts.get(normalizedKey) + 1;
539
+ failedConcepts.set(normalizedKey, conceptCount);
540
+ if (debug) {
541
+ console.error(`[CONCEPT-DEDUP] Blocked variation of failed concept (${conceptCount}x): "${searchQuery}" normalized to "${normalizeQueryConcept(searchQuery)}"`);
542
+ }
543
+ const isSubfolder = path && path !== effectiveSearchCwd && path !== '.';
544
+ const scopeHint = isSubfolder
545
+ ? `\n- Try searching from the workspace root (omit the path parameter) — the term may exist in a different directory`
546
+ : `\n- The term does not exist in this codebase at any path`;
547
+ return `CONCEPT ALREADY FAILED (${conceptCount} variations tried). You already searched for "${normalizeQueryConcept(searchQuery)}" with different quoting/syntax in this path and got NO results each time. Changing quotes, adding "func" prefix, or switching to method syntax will NOT change the results.\n\nChange your strategy:${scopeHint}\n- Use extract on a file you ALREADY found to read actual code and discover real function/type names\n- Use listFiles to browse directories and find what functions actually exist\n- Search for a BROADER concept (e.g., instead of "ctxGetData", try "context" or "middleware data access")\n- If you have enough information from prior searches, provide your final answer NOW`;
548
+ }
549
+
550
+ // Circuit breaker: too many consecutive no-result searches means the model
551
+ // is stuck in a loop guessing names that don't exist
552
+ if (consecutiveNoResults >= MAX_CONSECUTIVE_NO_RESULTS) {
553
+ if (debug) {
554
+ console.error(`[CIRCUIT-BREAKER] ${consecutiveNoResults} consecutive no-result searches, blocking: "${searchQuery}"`);
555
+ }
556
+ const isSubfolderCB = path && path !== effectiveSearchCwd && path !== '.';
557
+ const cbScopeHint = isSubfolderCB
558
+ ? `\n- You have been searching in "${path}" — try searching from the workspace root or a different directory`
559
+ : '';
560
+ return `CIRCUIT BREAKER: Your last ${consecutiveNoResults} searches ALL returned no results. You appear to be guessing function/type names that don't match what's actually in the code.\n\nChange your approach:${cbScopeHint}\n1. Use extract on files you already found — read the actual code to discover real function names\n2. Use listFiles to browse directories and see what files/functions actually exist\n3. If you found some results earlier, those are likely sufficient — provide your final answer\n\nRetrying search query variations will not help. Discover real names from real code instead.`;
561
+ }
481
562
  } else {
482
563
  // Cap pagination to prevent runaway page-through of broad queries
483
564
  const pageCount = (paginationCounts.get(searchKey) || 0) + 1;
@@ -493,11 +574,28 @@ export const searchTool = (options = {}) => {
493
574
  const result = maybeAnnotate(await runRawSearch());
494
575
  // Track whether this search had results for better dedup messages
495
576
  if (typeof result === 'string' && result.includes('No results found')) {
577
+ // Track consecutive no-results and failed concepts for circuit breaker
578
+ consecutiveNoResults++;
579
+ const normalizedKey = `${searchPath}::${normalizeQueryConcept(searchQuery)}`;
580
+ failedConcepts.set(normalizedKey, (failedConcepts.get(normalizedKey) || 0) + 1);
581
+ if (debug) {
582
+ console.error(`[NO-RESULTS] consecutiveNoResults=${consecutiveNoResults}, concept "${normalizeQueryConcept(searchQuery)}" failed ${failedConcepts.get(normalizedKey)}x`);
583
+ }
496
584
  // Append contextual hint for ticket/issue ID queries
497
585
  if (/^[A-Z]+-\d+$/.test(searchQuery.trim()) || /^[A-Z]+-\d+$/.test(searchQuery.replace(/"/g, '').trim())) {
498
586
  return result + '\n\n⚠️ Your query looks like a ticket/issue ID (e.g., JIRA-1234). Ticket IDs are rarely present in source code. Search for the technical concepts described in the ticket instead (e.g., function names, error messages, variable names).';
499
587
  }
588
+ // Add a hint when approaching the circuit breaker threshold
589
+ if (consecutiveNoResults >= MAX_CONSECUTIVE_NO_RESULTS - 1) {
590
+ const isSubfolderWarn = path && path !== effectiveSearchCwd && path !== '.';
591
+ const warnScopeHint = isSubfolderWarn
592
+ ? ` You are searching in "${path}" — consider searching from the workspace root or a different directory.`
593
+ : '';
594
+ return result + `\n\n⚠️ WARNING: ${consecutiveNoResults} consecutive searches returned no results.${warnScopeHint} Before your next action: use extract on a file you already found to read actual code, or use listFiles to discover what functions really exist. One more failed search will trigger the circuit breaker.`;
595
+ }
500
596
  } else if (typeof result === 'string') {
597
+ // Successful search — reset consecutive counter
598
+ consecutiveNoResults = 0;
501
599
  const entry = previousSearches.get(searchKey);
502
600
  if (entry) entry.hadResults = true;
503
601
  }
@@ -27561,8 +27561,23 @@ function buildSearchDelegateTask({ searchQuery, searchPath, exact, language, all
27561
27561
  '- Searching "getUserData" ALREADY matches "get", "user", "data" and their variations.',
27562
27562
  "- NEVER repeat the same search query \u2014 you will get the same results. Changing the path does NOT change this.",
27563
27563
  "- NEVER search trivial variations of the same keyword (e.g., AllowedIPs then allowedIps then allowed_ips). This is wasteful \u2014 probe handles it.",
27564
- "- If a search returns no results, the term likely does not exist. Try a genuinely DIFFERENT keyword or concept, not a variation.",
27565
- "- If 2-3 searches return no results for a concept, STOP searching for it and move on. Do NOT keep retrying.",
27564
+ "",
27565
+ "When a search returns no results:",
27566
+ '- If you searched a SUBFOLDER (e.g., path="gateway/"), the term might exist elsewhere.',
27567
+ " Try searching from the workspace root (omit the path parameter) or a different directory.",
27568
+ " But do NOT retry the same subfolder with different quoting \u2014 that will not help.",
27569
+ "- If you searched the WORKSPACE ROOT and got no results, the term does not exist in this codebase.",
27570
+ ' Changing quotes, adding "func " prefix, or switching to method syntax will NOT help.',
27571
+ "- These are ALL the same failed search, NOT different searches:",
27572
+ ' search("func ctxGetData") \u2192 no results',
27573
+ ' search("ctxGetData") \u2192 no results \u2190 WASTED, same concept, different quoting',
27574
+ " search(ctxGetData) \u2192 no results \u2190 WASTED, same concept, no quotes",
27575
+ ' search("ctx.GetData") \u2192 no results \u2190 WASTED, method syntax of same concept',
27576
+ ' After the FIRST "no results" at a given scope, either widen the search path or try',
27577
+ " a fundamentally different approach: search for a broader concept, use listFiles",
27578
+ " to discover actual function names, or extract a known file to read real code.",
27579
+ "- If 2 searches return no results for a concept (across different scopes), the code likely",
27580
+ " uses different naming than you expect \u2014 discover the real names via extract or listFiles.",
27566
27581
  "",
27567
27582
  "When to use exact=true:",
27568
27583
  "- Use exact=true when searching for a KNOWN symbol name (function, type, variable, struct).",
@@ -27615,6 +27630,21 @@ function buildSearchDelegateTask({ searchQuery, searchPath, exact, language, all
27615
27630
  ' \u2192 search "ForwardMessage" \u2192 search "ForwardMessage" \u2192 search "ForwardMessage" (WRONG: repeating the exact same query)',
27616
27631
  ' \u2192 search "authentication" \u2192 wait \u2192 search "session management" \u2192 wait (WRONG: these are independent, run them in parallel)',
27617
27632
  "",
27633
+ " WORST pattern \u2014 retrying a non-existent function with quote/syntax variations (this wastes 30 minutes):",
27634
+ ' \u2192 search "func ctxGetData" \u2192 no results',
27635
+ ' \u2192 search "ctxGetData" \u2192 no results \u2190 WRONG: same term without "func" prefix',
27636
+ ' \u2192 search "ctx.GetData" \u2192 no results \u2190 WRONG: method syntax of same concept',
27637
+ ' \u2192 search "ctx.SetData" \u2192 no results \u2190 WRONG: Set variant of same concept',
27638
+ " \u2192 search ctxGetData \u2192 no results \u2190 WRONG: unquoted version of same term",
27639
+ " \u2192 extract api.go \u2192 extract api.go \u2192 extract api.go (8 times!) \u2190 WRONG: re-reading same file",
27640
+ ' FIX: After "func ctxGetData" returns no results in gateway/:',
27641
+ " Option A: Widen scope \u2014 search from the workspace root (omit path) in case the",
27642
+ " function is defined in a different package (e.g., apidef/, user/, config/).",
27643
+ " Option B: Discover real names \u2014 extract a file you KNOW uses context (e.g., a",
27644
+ " middleware file) and READ what functions it actually calls.",
27645
+ " Option C: Browse \u2014 use listFiles to see what files exist and extract the relevant ones.",
27646
+ " NEVER: retry the same concept with different quoting in the same directory.",
27647
+ "",
27618
27648
  "Keyword tips:",
27619
27649
  "- Common programming keywords are filtered as stopwords when unquoted: function, class, return, new, struct, impl, var, let, const, etc.",
27620
27650
  '- Avoid searching for these alone \u2014 combine with a specific term (e.g., "middleware function" is fine, "function" alone is too generic).',
@@ -27653,7 +27683,7 @@ function buildSearchDelegateTask({ searchQuery, searchPath, exact, language, all
27653
27683
  " - Type references and imports \u2192 include type definitions.",
27654
27684
  " - Registered handlers/middleware \u2192 include all registered items.",
27655
27685
  "6. If a search returns results, use extract to verify relevance. Run multiple extracts in parallel too.",
27656
- "7. If a search returns NO results, the term does not exist. Do NOT retry with variations. Move on.",
27686
+ "7. If a search returns NO results: widen the path scope if you searched a subfolder, or move on. Do NOT retry with quote/syntax variations \u2014 they search the same index.",
27657
27687
  "8. Once you have enough targets (typically 5-15), output your final JSON answer immediately.",
27658
27688
  "",
27659
27689
  `Query: ${searchQuery}`,
@@ -27713,7 +27743,14 @@ var init_vercel = __esm({
27713
27743
  const previousSearches = /* @__PURE__ */ new Map();
27714
27744
  const dupBlockCounts = /* @__PURE__ */ new Map();
27715
27745
  const paginationCounts = /* @__PURE__ */ new Map();
27746
+ let consecutiveNoResults = 0;
27747
+ const MAX_CONSECUTIVE_NO_RESULTS = 4;
27748
+ const failedConcepts = /* @__PURE__ */ new Map();
27716
27749
  const MAX_PAGES_PER_QUERY = 3;
27750
+ function normalizeQueryConcept(query2) {
27751
+ if (!query2) return "";
27752
+ return query2.replace(/^["']|["']$/g, "").replace(/\./g, "").replace(/[_\-\s]+/g, "").toLowerCase().trim();
27753
+ }
27717
27754
  return (0, import_ai.tool)({
27718
27755
  name: "search",
27719
27756
  description: searchDelegate ? searchDelegateDescription : searchDescription,
@@ -27782,6 +27819,41 @@ var init_vercel = __esm({
27782
27819
  }
27783
27820
  previousSearches.set(searchKey, { hadResults: false });
27784
27821
  paginationCounts.set(searchKey, 0);
27822
+ const normalizedKey = `${searchPath}::${normalizeQueryConcept(searchQuery)}`;
27823
+ if (failedConcepts.has(normalizedKey) && failedConcepts.get(normalizedKey) >= 2) {
27824
+ const conceptCount = failedConcepts.get(normalizedKey) + 1;
27825
+ failedConcepts.set(normalizedKey, conceptCount);
27826
+ if (debug) {
27827
+ console.error(`[CONCEPT-DEDUP] Blocked variation of failed concept (${conceptCount}x): "${searchQuery}" normalized to "${normalizeQueryConcept(searchQuery)}"`);
27828
+ }
27829
+ const isSubfolder = path9 && path9 !== effectiveSearchCwd && path9 !== ".";
27830
+ const scopeHint = isSubfolder ? `
27831
+ - Try searching from the workspace root (omit the path parameter) \u2014 the term may exist in a different directory` : `
27832
+ - The term does not exist in this codebase at any path`;
27833
+ return `CONCEPT ALREADY FAILED (${conceptCount} variations tried). You already searched for "${normalizeQueryConcept(searchQuery)}" with different quoting/syntax in this path and got NO results each time. Changing quotes, adding "func" prefix, or switching to method syntax will NOT change the results.
27834
+
27835
+ Change your strategy:${scopeHint}
27836
+ - Use extract on a file you ALREADY found to read actual code and discover real function/type names
27837
+ - Use listFiles to browse directories and find what functions actually exist
27838
+ - Search for a BROADER concept (e.g., instead of "ctxGetData", try "context" or "middleware data access")
27839
+ - If you have enough information from prior searches, provide your final answer NOW`;
27840
+ }
27841
+ if (consecutiveNoResults >= MAX_CONSECUTIVE_NO_RESULTS) {
27842
+ if (debug) {
27843
+ console.error(`[CIRCUIT-BREAKER] ${consecutiveNoResults} consecutive no-result searches, blocking: "${searchQuery}"`);
27844
+ }
27845
+ const isSubfolderCB = path9 && path9 !== effectiveSearchCwd && path9 !== ".";
27846
+ const cbScopeHint = isSubfolderCB ? `
27847
+ - You have been searching in "${path9}" \u2014 try searching from the workspace root or a different directory` : "";
27848
+ return `CIRCUIT BREAKER: Your last ${consecutiveNoResults} searches ALL returned no results. You appear to be guessing function/type names that don't match what's actually in the code.
27849
+
27850
+ Change your approach:${cbScopeHint}
27851
+ 1. Use extract on files you already found \u2014 read the actual code to discover real function names
27852
+ 2. Use listFiles to browse directories and see what files/functions actually exist
27853
+ 3. If you found some results earlier, those are likely sufficient \u2014 provide your final answer
27854
+
27855
+ Retrying search query variations will not help. Discover real names from real code instead.`;
27856
+ }
27785
27857
  } else {
27786
27858
  const pageCount = (paginationCounts.get(searchKey) || 0) + 1;
27787
27859
  paginationCounts.set(searchKey, pageCount);
@@ -27795,10 +27867,24 @@ var init_vercel = __esm({
27795
27867
  try {
27796
27868
  const result = maybeAnnotate(await runRawSearch());
27797
27869
  if (typeof result === "string" && result.includes("No results found")) {
27870
+ consecutiveNoResults++;
27871
+ const normalizedKey = `${searchPath}::${normalizeQueryConcept(searchQuery)}`;
27872
+ failedConcepts.set(normalizedKey, (failedConcepts.get(normalizedKey) || 0) + 1);
27873
+ if (debug) {
27874
+ console.error(`[NO-RESULTS] consecutiveNoResults=${consecutiveNoResults}, concept "${normalizeQueryConcept(searchQuery)}" failed ${failedConcepts.get(normalizedKey)}x`);
27875
+ }
27798
27876
  if (/^[A-Z]+-\d+$/.test(searchQuery.trim()) || /^[A-Z]+-\d+$/.test(searchQuery.replace(/"/g, "").trim())) {
27799
27877
  return result + "\n\n\u26A0\uFE0F Your query looks like a ticket/issue ID (e.g., JIRA-1234). Ticket IDs are rarely present in source code. Search for the technical concepts described in the ticket instead (e.g., function names, error messages, variable names).";
27800
27878
  }
27879
+ if (consecutiveNoResults >= MAX_CONSECUTIVE_NO_RESULTS - 1) {
27880
+ const isSubfolderWarn = path9 && path9 !== effectiveSearchCwd && path9 !== ".";
27881
+ const warnScopeHint = isSubfolderWarn ? ` You are searching in "${path9}" \u2014 consider searching from the workspace root or a different directory.` : "";
27882
+ return result + `
27883
+
27884
+ \u26A0\uFE0F WARNING: ${consecutiveNoResults} consecutive searches returned no results.${warnScopeHint} Before your next action: use extract on a file you already found to read actual code, or use listFiles to discover what functions really exist. One more failed search will trigger the circuit breaker.`;
27885
+ }
27801
27886
  } else if (typeof result === "string") {
27887
+ consecutiveNoResults = 0;
27802
27888
  const entry = previousSearches.get(searchKey);
27803
27889
  if (entry) entry.hadResults = true;
27804
27890
  }
package/cjs/index.cjs CHANGED
@@ -103080,8 +103080,23 @@ function buildSearchDelegateTask({ searchQuery, searchPath, exact, language, all
103080
103080
  '- Searching "getUserData" ALREADY matches "get", "user", "data" and their variations.',
103081
103081
  "- NEVER repeat the same search query \u2014 you will get the same results. Changing the path does NOT change this.",
103082
103082
  "- NEVER search trivial variations of the same keyword (e.g., AllowedIPs then allowedIps then allowed_ips). This is wasteful \u2014 probe handles it.",
103083
- "- If a search returns no results, the term likely does not exist. Try a genuinely DIFFERENT keyword or concept, not a variation.",
103084
- "- If 2-3 searches return no results for a concept, STOP searching for it and move on. Do NOT keep retrying.",
103083
+ "",
103084
+ "When a search returns no results:",
103085
+ '- If you searched a SUBFOLDER (e.g., path="gateway/"), the term might exist elsewhere.',
103086
+ " Try searching from the workspace root (omit the path parameter) or a different directory.",
103087
+ " But do NOT retry the same subfolder with different quoting \u2014 that will not help.",
103088
+ "- If you searched the WORKSPACE ROOT and got no results, the term does not exist in this codebase.",
103089
+ ' Changing quotes, adding "func " prefix, or switching to method syntax will NOT help.',
103090
+ "- These are ALL the same failed search, NOT different searches:",
103091
+ ' search("func ctxGetData") \u2192 no results',
103092
+ ' search("ctxGetData") \u2192 no results \u2190 WASTED, same concept, different quoting',
103093
+ " search(ctxGetData) \u2192 no results \u2190 WASTED, same concept, no quotes",
103094
+ ' search("ctx.GetData") \u2192 no results \u2190 WASTED, method syntax of same concept',
103095
+ ' After the FIRST "no results" at a given scope, either widen the search path or try',
103096
+ " a fundamentally different approach: search for a broader concept, use listFiles",
103097
+ " to discover actual function names, or extract a known file to read real code.",
103098
+ "- If 2 searches return no results for a concept (across different scopes), the code likely",
103099
+ " uses different naming than you expect \u2014 discover the real names via extract or listFiles.",
103085
103100
  "",
103086
103101
  "When to use exact=true:",
103087
103102
  "- Use exact=true when searching for a KNOWN symbol name (function, type, variable, struct).",
@@ -103134,6 +103149,21 @@ function buildSearchDelegateTask({ searchQuery, searchPath, exact, language, all
103134
103149
  ' \u2192 search "ForwardMessage" \u2192 search "ForwardMessage" \u2192 search "ForwardMessage" (WRONG: repeating the exact same query)',
103135
103150
  ' \u2192 search "authentication" \u2192 wait \u2192 search "session management" \u2192 wait (WRONG: these are independent, run them in parallel)',
103136
103151
  "",
103152
+ " WORST pattern \u2014 retrying a non-existent function with quote/syntax variations (this wastes 30 minutes):",
103153
+ ' \u2192 search "func ctxGetData" \u2192 no results',
103154
+ ' \u2192 search "ctxGetData" \u2192 no results \u2190 WRONG: same term without "func" prefix',
103155
+ ' \u2192 search "ctx.GetData" \u2192 no results \u2190 WRONG: method syntax of same concept',
103156
+ ' \u2192 search "ctx.SetData" \u2192 no results \u2190 WRONG: Set variant of same concept',
103157
+ " \u2192 search ctxGetData \u2192 no results \u2190 WRONG: unquoted version of same term",
103158
+ " \u2192 extract api.go \u2192 extract api.go \u2192 extract api.go (8 times!) \u2190 WRONG: re-reading same file",
103159
+ ' FIX: After "func ctxGetData" returns no results in gateway/:',
103160
+ " Option A: Widen scope \u2014 search from the workspace root (omit path) in case the",
103161
+ " function is defined in a different package (e.g., apidef/, user/, config/).",
103162
+ " Option B: Discover real names \u2014 extract a file you KNOW uses context (e.g., a",
103163
+ " middleware file) and READ what functions it actually calls.",
103164
+ " Option C: Browse \u2014 use listFiles to see what files exist and extract the relevant ones.",
103165
+ " NEVER: retry the same concept with different quoting in the same directory.",
103166
+ "",
103137
103167
  "Keyword tips:",
103138
103168
  "- Common programming keywords are filtered as stopwords when unquoted: function, class, return, new, struct, impl, var, let, const, etc.",
103139
103169
  '- Avoid searching for these alone \u2014 combine with a specific term (e.g., "middleware function" is fine, "function" alone is too generic).',
@@ -103172,7 +103202,7 @@ function buildSearchDelegateTask({ searchQuery, searchPath, exact, language, all
103172
103202
  " - Type references and imports \u2192 include type definitions.",
103173
103203
  " - Registered handlers/middleware \u2192 include all registered items.",
103174
103204
  "6. If a search returns results, use extract to verify relevance. Run multiple extracts in parallel too.",
103175
- "7. If a search returns NO results, the term does not exist. Do NOT retry with variations. Move on.",
103205
+ "7. If a search returns NO results: widen the path scope if you searched a subfolder, or move on. Do NOT retry with quote/syntax variations \u2014 they search the same index.",
103176
103206
  "8. Once you have enough targets (typically 5-15), output your final JSON answer immediately.",
103177
103207
  "",
103178
103208
  `Query: ${searchQuery}`,
@@ -103232,7 +103262,14 @@ var init_vercel = __esm({
103232
103262
  const previousSearches = /* @__PURE__ */ new Map();
103233
103263
  const dupBlockCounts = /* @__PURE__ */ new Map();
103234
103264
  const paginationCounts = /* @__PURE__ */ new Map();
103265
+ let consecutiveNoResults = 0;
103266
+ const MAX_CONSECUTIVE_NO_RESULTS = 4;
103267
+ const failedConcepts = /* @__PURE__ */ new Map();
103235
103268
  const MAX_PAGES_PER_QUERY = 3;
103269
+ function normalizeQueryConcept(query2) {
103270
+ if (!query2) return "";
103271
+ return query2.replace(/^["']|["']$/g, "").replace(/\./g, "").replace(/[_\-\s]+/g, "").toLowerCase().trim();
103272
+ }
103236
103273
  return (0, import_ai5.tool)({
103237
103274
  name: "search",
103238
103275
  description: searchDelegate ? searchDelegateDescription : searchDescription,
@@ -103301,6 +103338,41 @@ var init_vercel = __esm({
103301
103338
  }
103302
103339
  previousSearches.set(searchKey, { hadResults: false });
103303
103340
  paginationCounts.set(searchKey, 0);
103341
+ const normalizedKey = `${searchPath}::${normalizeQueryConcept(searchQuery)}`;
103342
+ if (failedConcepts.has(normalizedKey) && failedConcepts.get(normalizedKey) >= 2) {
103343
+ const conceptCount = failedConcepts.get(normalizedKey) + 1;
103344
+ failedConcepts.set(normalizedKey, conceptCount);
103345
+ if (debug) {
103346
+ console.error(`[CONCEPT-DEDUP] Blocked variation of failed concept (${conceptCount}x): "${searchQuery}" normalized to "${normalizeQueryConcept(searchQuery)}"`);
103347
+ }
103348
+ const isSubfolder = path9 && path9 !== effectiveSearchCwd && path9 !== ".";
103349
+ const scopeHint = isSubfolder ? `
103350
+ - Try searching from the workspace root (omit the path parameter) \u2014 the term may exist in a different directory` : `
103351
+ - The term does not exist in this codebase at any path`;
103352
+ return `CONCEPT ALREADY FAILED (${conceptCount} variations tried). You already searched for "${normalizeQueryConcept(searchQuery)}" with different quoting/syntax in this path and got NO results each time. Changing quotes, adding "func" prefix, or switching to method syntax will NOT change the results.
103353
+
103354
+ Change your strategy:${scopeHint}
103355
+ - Use extract on a file you ALREADY found to read actual code and discover real function/type names
103356
+ - Use listFiles to browse directories and find what functions actually exist
103357
+ - Search for a BROADER concept (e.g., instead of "ctxGetData", try "context" or "middleware data access")
103358
+ - If you have enough information from prior searches, provide your final answer NOW`;
103359
+ }
103360
+ if (consecutiveNoResults >= MAX_CONSECUTIVE_NO_RESULTS) {
103361
+ if (debug) {
103362
+ console.error(`[CIRCUIT-BREAKER] ${consecutiveNoResults} consecutive no-result searches, blocking: "${searchQuery}"`);
103363
+ }
103364
+ const isSubfolderCB = path9 && path9 !== effectiveSearchCwd && path9 !== ".";
103365
+ const cbScopeHint = isSubfolderCB ? `
103366
+ - You have been searching in "${path9}" \u2014 try searching from the workspace root or a different directory` : "";
103367
+ return `CIRCUIT BREAKER: Your last ${consecutiveNoResults} searches ALL returned no results. You appear to be guessing function/type names that don't match what's actually in the code.
103368
+
103369
+ Change your approach:${cbScopeHint}
103370
+ 1. Use extract on files you already found \u2014 read the actual code to discover real function names
103371
+ 2. Use listFiles to browse directories and see what files/functions actually exist
103372
+ 3. If you found some results earlier, those are likely sufficient \u2014 provide your final answer
103373
+
103374
+ Retrying search query variations will not help. Discover real names from real code instead.`;
103375
+ }
103304
103376
  } else {
103305
103377
  const pageCount = (paginationCounts.get(searchKey) || 0) + 1;
103306
103378
  paginationCounts.set(searchKey, pageCount);
@@ -103314,10 +103386,24 @@ var init_vercel = __esm({
103314
103386
  try {
103315
103387
  const result = maybeAnnotate(await runRawSearch());
103316
103388
  if (typeof result === "string" && result.includes("No results found")) {
103389
+ consecutiveNoResults++;
103390
+ const normalizedKey = `${searchPath}::${normalizeQueryConcept(searchQuery)}`;
103391
+ failedConcepts.set(normalizedKey, (failedConcepts.get(normalizedKey) || 0) + 1);
103392
+ if (debug) {
103393
+ console.error(`[NO-RESULTS] consecutiveNoResults=${consecutiveNoResults}, concept "${normalizeQueryConcept(searchQuery)}" failed ${failedConcepts.get(normalizedKey)}x`);
103394
+ }
103317
103395
  if (/^[A-Z]+-\d+$/.test(searchQuery.trim()) || /^[A-Z]+-\d+$/.test(searchQuery.replace(/"/g, "").trim())) {
103318
103396
  return result + "\n\n\u26A0\uFE0F Your query looks like a ticket/issue ID (e.g., JIRA-1234). Ticket IDs are rarely present in source code. Search for the technical concepts described in the ticket instead (e.g., function names, error messages, variable names).";
103319
103397
  }
103398
+ if (consecutiveNoResults >= MAX_CONSECUTIVE_NO_RESULTS - 1) {
103399
+ const isSubfolderWarn = path9 && path9 !== effectiveSearchCwd && path9 !== ".";
103400
+ const warnScopeHint = isSubfolderWarn ? ` You are searching in "${path9}" \u2014 consider searching from the workspace root or a different directory.` : "";
103401
+ return result + `
103402
+
103403
+ \u26A0\uFE0F WARNING: ${consecutiveNoResults} consecutive searches returned no results.${warnScopeHint} Before your next action: use extract on a file you already found to read actual code, or use listFiles to discover what functions really exist. One more failed search will trigger the circuit breaker.`;
103404
+ }
103320
103405
  } else if (typeof result === "string") {
103406
+ consecutiveNoResults = 0;
103321
103407
  const entry = previousSearches.get(searchKey);
103322
103408
  if (entry) entry.hadResults = true;
103323
103409
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@probelabs/probe",
3
- "version": "0.6.0-rc302",
3
+ "version": "0.6.0-rc303",
4
4
  "description": "Node.js wrapper for the probe code search tool",
5
5
  "main": "src/index.js",
6
6
  "module": "src/index.js",
@@ -248,8 +248,23 @@ function buildSearchDelegateTask({ searchQuery, searchPath, exact, language, all
248
248
  '- Searching "getUserData" ALREADY matches "get", "user", "data" and their variations.',
249
249
  '- NEVER repeat the same search query — you will get the same results. Changing the path does NOT change this.',
250
250
  '- NEVER search trivial variations of the same keyword (e.g., AllowedIPs then allowedIps then allowed_ips). This is wasteful — probe handles it.',
251
- '- If a search returns no results, the term likely does not exist. Try a genuinely DIFFERENT keyword or concept, not a variation.',
252
- '- If 2-3 searches return no results for a concept, STOP searching for it and move on. Do NOT keep retrying.',
251
+ '',
252
+ 'When a search returns no results:',
253
+ '- If you searched a SUBFOLDER (e.g., path="gateway/"), the term might exist elsewhere.',
254
+ ' Try searching from the workspace root (omit the path parameter) or a different directory.',
255
+ ' But do NOT retry the same subfolder with different quoting — that will not help.',
256
+ '- If you searched the WORKSPACE ROOT and got no results, the term does not exist in this codebase.',
257
+ ' Changing quotes, adding "func " prefix, or switching to method syntax will NOT help.',
258
+ '- These are ALL the same failed search, NOT different searches:',
259
+ ' search("func ctxGetData") → no results',
260
+ ' search("ctxGetData") → no results ← WASTED, same concept, different quoting',
261
+ ' search(ctxGetData) → no results ← WASTED, same concept, no quotes',
262
+ ' search("ctx.GetData") → no results ← WASTED, method syntax of same concept',
263
+ ' After the FIRST "no results" at a given scope, either widen the search path or try',
264
+ ' a fundamentally different approach: search for a broader concept, use listFiles',
265
+ ' to discover actual function names, or extract a known file to read real code.',
266
+ '- If 2 searches return no results for a concept (across different scopes), the code likely',
267
+ ' uses different naming than you expect — discover the real names via extract or listFiles.',
253
268
  '',
254
269
  'When to use exact=true:',
255
270
  '- Use exact=true when searching for a KNOWN symbol name (function, type, variable, struct).',
@@ -302,6 +317,21 @@ function buildSearchDelegateTask({ searchQuery, searchPath, exact, language, all
302
317
  ' → search "ForwardMessage" → search "ForwardMessage" → search "ForwardMessage" (WRONG: repeating the exact same query)',
303
318
  ' → search "authentication" → wait → search "session management" → wait (WRONG: these are independent, run them in parallel)',
304
319
  '',
320
+ ' WORST pattern — retrying a non-existent function with quote/syntax variations (this wastes 30 minutes):',
321
+ ' → search "func ctxGetData" → no results',
322
+ ' → search "ctxGetData" → no results ← WRONG: same term without "func" prefix',
323
+ ' → search "ctx.GetData" → no results ← WRONG: method syntax of same concept',
324
+ ' → search "ctx.SetData" → no results ← WRONG: Set variant of same concept',
325
+ ' → search ctxGetData → no results ← WRONG: unquoted version of same term',
326
+ ' → extract api.go → extract api.go → extract api.go (8 times!) ← WRONG: re-reading same file',
327
+ ' FIX: After "func ctxGetData" returns no results in gateway/:',
328
+ ' Option A: Widen scope — search from the workspace root (omit path) in case the',
329
+ ' function is defined in a different package (e.g., apidef/, user/, config/).',
330
+ ' Option B: Discover real names — extract a file you KNOW uses context (e.g., a',
331
+ ' middleware file) and READ what functions it actually calls.',
332
+ ' Option C: Browse — use listFiles to see what files exist and extract the relevant ones.',
333
+ ' NEVER: retry the same concept with different quoting in the same directory.',
334
+ '',
305
335
  'Keyword tips:',
306
336
  '- Common programming keywords are filtered as stopwords when unquoted: function, class, return, new, struct, impl, var, let, const, etc.',
307
337
  '- Avoid searching for these alone — combine with a specific term (e.g., "middleware function" is fine, "function" alone is too generic).',
@@ -340,7 +370,7 @@ function buildSearchDelegateTask({ searchQuery, searchPath, exact, language, all
340
370
  ' - Type references and imports → include type definitions.',
341
371
  ' - Registered handlers/middleware → include all registered items.',
342
372
  '6. If a search returns results, use extract to verify relevance. Run multiple extracts in parallel too.',
343
- '7. If a search returns NO results, the term does not exist. Do NOT retry with variations. Move on.',
373
+ '7. If a search returns NO results: widen the path scope if you searched a subfolder, or move on. Do NOT retry with quote/syntax variations they search the same index.',
344
374
  '8. Once you have enough targets (typically 5-15), output your final JSON answer immediately.',
345
375
  '',
346
376
  `Query: ${searchQuery}`,
@@ -388,8 +418,30 @@ export const searchTool = (options = {}) => {
388
418
  const dupBlockCounts = new Map();
389
419
  // Track pagination counts per query to cap runaway pagination
390
420
  const paginationCounts = new Map();
421
+ // Track consecutive no-result searches (circuit breaker)
422
+ let consecutiveNoResults = 0;
423
+ const MAX_CONSECUTIVE_NO_RESULTS = 4;
424
+ // Track normalized query concepts for fuzzy dedup (catches quote/syntax variations)
425
+ const failedConcepts = new Map(); // normalizedKey → count
391
426
  const MAX_PAGES_PER_QUERY = 3;
392
427
 
428
+ /**
429
+ * Normalize a search query to detect syntax-level duplicates.
430
+ * Strips quotes, dots, underscores/hyphens, and lowercases.
431
+ * "ctxGetData", "ctx.GetData", "ctx_get_data" all → "ctxgetdata"
432
+ * Note: does NOT strip language keywords (func, type) — those change search
433
+ * semantics and are already handled as stopwords by the Rust search engine.
434
+ */
435
+ function normalizeQueryConcept(query) {
436
+ if (!query) return '';
437
+ return query
438
+ .replace(/^["']|["']$/g, '') // strip outer quotes
439
+ .replace(/\./g, '') // "ctx.GetData" → "ctxGetData"
440
+ .replace(/[_\-\s]+/g, '') // strip underscores/hyphens/spaces
441
+ .toLowerCase()
442
+ .trim();
443
+ }
444
+
393
445
  return tool({
394
446
  name: 'search',
395
447
  description: searchDelegate
@@ -478,6 +530,35 @@ export const searchTool = (options = {}) => {
478
530
  }
479
531
  previousSearches.set(searchKey, { hadResults: false });
480
532
  paginationCounts.set(searchKey, 0);
533
+
534
+ // Fuzzy concept dedup: catch quote/syntax variations of the same failed concept
535
+ // e.g., "func ctxGetData", "ctxGetData", "ctx.GetData" all normalize to "ctxgetdata"
536
+ const normalizedKey = `${searchPath}::${normalizeQueryConcept(searchQuery)}`;
537
+ if (failedConcepts.has(normalizedKey) && failedConcepts.get(normalizedKey) >= 2) {
538
+ const conceptCount = failedConcepts.get(normalizedKey) + 1;
539
+ failedConcepts.set(normalizedKey, conceptCount);
540
+ if (debug) {
541
+ console.error(`[CONCEPT-DEDUP] Blocked variation of failed concept (${conceptCount}x): "${searchQuery}" normalized to "${normalizeQueryConcept(searchQuery)}"`);
542
+ }
543
+ const isSubfolder = path && path !== effectiveSearchCwd && path !== '.';
544
+ const scopeHint = isSubfolder
545
+ ? `\n- Try searching from the workspace root (omit the path parameter) — the term may exist in a different directory`
546
+ : `\n- The term does not exist in this codebase at any path`;
547
+ return `CONCEPT ALREADY FAILED (${conceptCount} variations tried). You already searched for "${normalizeQueryConcept(searchQuery)}" with different quoting/syntax in this path and got NO results each time. Changing quotes, adding "func" prefix, or switching to method syntax will NOT change the results.\n\nChange your strategy:${scopeHint}\n- Use extract on a file you ALREADY found to read actual code and discover real function/type names\n- Use listFiles to browse directories and find what functions actually exist\n- Search for a BROADER concept (e.g., instead of "ctxGetData", try "context" or "middleware data access")\n- If you have enough information from prior searches, provide your final answer NOW`;
548
+ }
549
+
550
+ // Circuit breaker: too many consecutive no-result searches means the model
551
+ // is stuck in a loop guessing names that don't exist
552
+ if (consecutiveNoResults >= MAX_CONSECUTIVE_NO_RESULTS) {
553
+ if (debug) {
554
+ console.error(`[CIRCUIT-BREAKER] ${consecutiveNoResults} consecutive no-result searches, blocking: "${searchQuery}"`);
555
+ }
556
+ const isSubfolderCB = path && path !== effectiveSearchCwd && path !== '.';
557
+ const cbScopeHint = isSubfolderCB
558
+ ? `\n- You have been searching in "${path}" — try searching from the workspace root or a different directory`
559
+ : '';
560
+ return `CIRCUIT BREAKER: Your last ${consecutiveNoResults} searches ALL returned no results. You appear to be guessing function/type names that don't match what's actually in the code.\n\nChange your approach:${cbScopeHint}\n1. Use extract on files you already found — read the actual code to discover real function names\n2. Use listFiles to browse directories and see what files/functions actually exist\n3. If you found some results earlier, those are likely sufficient — provide your final answer\n\nRetrying search query variations will not help. Discover real names from real code instead.`;
561
+ }
481
562
  } else {
482
563
  // Cap pagination to prevent runaway page-through of broad queries
483
564
  const pageCount = (paginationCounts.get(searchKey) || 0) + 1;
@@ -493,11 +574,28 @@ export const searchTool = (options = {}) => {
493
574
  const result = maybeAnnotate(await runRawSearch());
494
575
  // Track whether this search had results for better dedup messages
495
576
  if (typeof result === 'string' && result.includes('No results found')) {
577
+ // Track consecutive no-results and failed concepts for circuit breaker
578
+ consecutiveNoResults++;
579
+ const normalizedKey = `${searchPath}::${normalizeQueryConcept(searchQuery)}`;
580
+ failedConcepts.set(normalizedKey, (failedConcepts.get(normalizedKey) || 0) + 1);
581
+ if (debug) {
582
+ console.error(`[NO-RESULTS] consecutiveNoResults=${consecutiveNoResults}, concept "${normalizeQueryConcept(searchQuery)}" failed ${failedConcepts.get(normalizedKey)}x`);
583
+ }
496
584
  // Append contextual hint for ticket/issue ID queries
497
585
  if (/^[A-Z]+-\d+$/.test(searchQuery.trim()) || /^[A-Z]+-\d+$/.test(searchQuery.replace(/"/g, '').trim())) {
498
586
  return result + '\n\n⚠️ Your query looks like a ticket/issue ID (e.g., JIRA-1234). Ticket IDs are rarely present in source code. Search for the technical concepts described in the ticket instead (e.g., function names, error messages, variable names).';
499
587
  }
588
+ // Add a hint when approaching the circuit breaker threshold
589
+ if (consecutiveNoResults >= MAX_CONSECUTIVE_NO_RESULTS - 1) {
590
+ const isSubfolderWarn = path && path !== effectiveSearchCwd && path !== '.';
591
+ const warnScopeHint = isSubfolderWarn
592
+ ? ` You are searching in "${path}" — consider searching from the workspace root or a different directory.`
593
+ : '';
594
+ return result + `\n\n⚠️ WARNING: ${consecutiveNoResults} consecutive searches returned no results.${warnScopeHint} Before your next action: use extract on a file you already found to read actual code, or use listFiles to discover what functions really exist. One more failed search will trigger the circuit breaker.`;
595
+ }
500
596
  } else if (typeof result === 'string') {
597
+ // Successful search — reset consecutive counter
598
+ consecutiveNoResults = 0;
501
599
  const entry = previousSearches.get(searchKey);
502
600
  if (entry) entry.hadResults = true;
503
601
  }