@probelabs/probe 0.6.0-rc287 → 0.6.0-rc290

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.
package/src/mcp/index.ts CHANGED
@@ -21,11 +21,13 @@ import { fileURLToPath } from 'url';
21
21
  // Import from parent package
22
22
  import { search, query, extract, grep, getBinaryPath, setBinaryPath } from '../index.js';
23
23
 
24
+ type OutputFormat = 'outline' | 'outline-xml' | 'json';
25
+
24
26
  // Parse command-line arguments
25
- function parseArgs(): { timeout?: number; format?: string } {
27
+ function parseArgs(): { timeout?: number; lsp?: boolean; format?: OutputFormat } {
26
28
  const args = process.argv.slice(2);
27
- const config: { timeout?: number; format?: string } = {};
28
-
29
+ const config: { timeout?: number; lsp?: boolean; format?: OutputFormat } = {};
30
+
29
31
  for (let i = 0; i < args.length; i++) {
30
32
  if ((args[i] === '--timeout' || args[i] === '-t') && i + 1 < args.length) {
31
33
  const timeout = parseInt(args[i + 1], 10);
@@ -36,9 +38,17 @@ function parseArgs(): { timeout?: number; format?: string } {
36
38
  console.error(`Invalid timeout value: ${args[i + 1]}. Using default.`);
37
39
  }
38
40
  i++; // Skip the next argument
41
+ } else if (args[i] === '--lsp') {
42
+ config.lsp = true;
43
+ console.error('LSP mode enabled');
39
44
  } else if (args[i] === '--format' && i + 1 < args.length) {
40
- config.format = args[i + 1];
41
- console.error(`Format set to ${config.format}`);
45
+ const format = args[i + 1] as OutputFormat;
46
+ if (format === 'outline' || format === 'outline-xml' || format === 'json') {
47
+ config.format = format;
48
+ console.error(`Output format set to ${format}`);
49
+ } else {
50
+ console.error(`Invalid format value: ${args[i + 1]}. Using default.`);
51
+ }
42
52
  i++; // Skip the next argument
43
53
  } else if (args[i] === '--help' || args[i] === '-h') {
44
54
  console.error(`
@@ -49,7 +59,9 @@ Usage:
49
59
 
50
60
  Options:
51
61
  --timeout, -t <seconds> Set timeout for search operations (default: 30)
52
- --format <format> Set output format (default: outline)
62
+ --lsp Enable LSP (Language Server Protocol) for enhanced features
63
+ Automatically initializes language servers for the current workspace
64
+ --format <format> Output format for search responses (outline|outline-xml|json)
53
65
  --help, -h Show this help message
54
66
  `);
55
67
  process.exit(0);
@@ -120,17 +132,37 @@ interface SearchCodeArgs {
120
132
  exact?: boolean;
121
133
  strictElasticSyntax?: boolean;
122
134
  session?: string;
123
- nextPage?: boolean;
135
+ timeout?: number;
136
+ noGitignore?: boolean;
137
+ lsp?: boolean;
138
+ }
139
+
140
+ interface QueryCodeArgs {
141
+ path: string;
142
+ pattern: string;
143
+ language?: string;
144
+ ignore?: string[];
145
+ allowTests?: boolean;
146
+ maxResults?: number;
147
+ format?: 'markdown' | 'plain' | 'json' | 'color';
148
+ timeout?: number;
149
+ noGitignore?: boolean;
124
150
  }
125
151
 
126
152
  interface ExtractCodeArgs {
127
153
  path: string;
128
154
  files: string[];
155
+ allowTests?: boolean;
156
+ contextLines?: number;
157
+ format?: 'markdown' | 'plain' | 'json';
158
+ timeout?: number;
159
+ noGitignore?: boolean;
160
+ lsp?: boolean;
129
161
  }
130
162
 
131
163
  interface GrepArgs {
132
164
  pattern: string;
133
- paths: string | string[];
165
+ paths: string[];
134
166
  ignoreCase?: boolean;
135
167
  count?: boolean;
136
168
  context?: number;
@@ -139,11 +171,17 @@ interface GrepArgs {
139
171
  class ProbeServer {
140
172
  private server: Server;
141
173
  private defaultTimeout: number;
142
- private defaultFormat?: string;
143
-
144
- constructor(timeout: number = 30, format?: string) {
174
+ private lspEnabled: boolean;
175
+ private defaultFormat: OutputFormat;
176
+
177
+ constructor(
178
+ timeout: number = 30,
179
+ lspEnabled: boolean = false,
180
+ defaultFormat: OutputFormat = 'outline-xml'
181
+ ) {
145
182
  this.defaultTimeout = timeout;
146
- this.defaultFormat = format;
183
+ this.lspEnabled = lspEnabled;
184
+ this.defaultFormat = defaultFormat;
147
185
  this.server = new Server(
148
186
  {
149
187
  name: '@probelabs/probe',
@@ -201,8 +239,11 @@ class ProbeServer {
201
239
  },
202
240
  nextPage: {
203
241
  type: 'boolean',
204
- description: 'Set to true when requesting the next page of results. Requires passing the same session ID from the previous search output.',
205
- default: false
242
+ description: 'Skip .gitignore files (will use PROBE_NO_GITIGNORE environment variable if not set)',
243
+ },
244
+ lsp: {
245
+ type: 'boolean',
246
+ description: 'Use LSP (Language Server Protocol) for call hierarchy, reference counts, and enhanced symbol information',
206
247
  }
207
248
  },
208
249
  required: ['path', 'query']
@@ -221,7 +262,34 @@ class ProbeServer {
221
262
  files: {
222
263
  type: 'array',
223
264
  items: { type: 'string' },
224
- description: 'Array of file paths to extract from. Formats: "file.js" (entire file), "file.js:42" (code block at line 42), "file.js:10-20" (lines 10-20), "file.js#funcName" (specific symbol). Line numbers and symbols are part of the path string, not separate parameters. Paths can be absolute or relative to the project directory.',
265
+ description: 'Files and lines or sybmbols to extract from: /path/to/file.rs:10, /path/to/file.rs#func_name Path should be absolute.',
266
+ },
267
+ allowTests: {
268
+ type: 'boolean',
269
+ description: 'Allow test files and test code blocks in results (disabled by default)',
270
+ },
271
+ contextLines: {
272
+ type: 'number',
273
+ description: 'Number of context lines to include before and after the extracted block when AST parsing fails to find a suitable node',
274
+ default: 0
275
+ },
276
+ format: {
277
+ type: 'string',
278
+ enum: ['markdown', 'plain', 'json'],
279
+ description: 'Output format for the extracted code',
280
+ default: 'markdown'
281
+ },
282
+ timeout: {
283
+ type: 'number',
284
+ description: 'Timeout for the extract operation in seconds (default: 30)',
285
+ },
286
+ noGitignore: {
287
+ type: 'boolean',
288
+ description: 'Skip .gitignore files (will use PROBE_NO_GITIGNORE environment variable if not set)',
289
+ },
290
+ lsp: {
291
+ type: 'boolean',
292
+ description: 'Use LSP (Language Server Protocol) for call hierarchy, reference counts, and enhanced symbol information',
225
293
  }
226
294
  },
227
295
  required: ['path', 'files'],
@@ -365,7 +433,24 @@ class ProbeServer {
365
433
  } else if (this.defaultFormat === 'json') {
366
434
  options.json = true;
367
435
  }
368
-
436
+ if (args.session !== undefined && args.session.trim() !== '') {
437
+ options.session = args.session;
438
+ } else {
439
+ options.session = "new";
440
+ }
441
+ // Use timeout from args, or fall back to instance default
442
+ if (args.timeout !== undefined) {
443
+ options.timeout = args.timeout;
444
+ } else if (this.defaultTimeout !== undefined) {
445
+ options.timeout = this.defaultTimeout;
446
+ }
447
+ // Pass LSP flag if enabled globally or per-request
448
+ if (args.lsp !== undefined) {
449
+ options.lsp = args.lsp;
450
+ } else if (this.lspEnabled) {
451
+ options.lsp = true;
452
+ }
453
+
369
454
  console.error("Executing search with options:", JSON.stringify(options, null, 2));
370
455
 
371
456
  try {
@@ -405,6 +490,19 @@ class ProbeServer {
405
490
  allowTests: true, // Include test files by default
406
491
  };
407
492
 
493
+ // Use noGitignore from args, or fall back to PROBE_NO_GITIGNORE environment variable
494
+ if (args.noGitignore !== undefined) {
495
+ options.noGitignore = args.noGitignore;
496
+ } else if (process.env.PROBE_NO_GITIGNORE) {
497
+ options.noGitignore = process.env.PROBE_NO_GITIGNORE === 'true';
498
+ }
499
+ // Pass LSP flag if enabled globally or per-request
500
+ if (args.lsp !== undefined) {
501
+ options.lsp = args.lsp;
502
+ } else if (this.lspEnabled) {
503
+ options.lsp = true;
504
+ }
505
+
408
506
  // Call extract with the complete options object
409
507
  try {
410
508
  // Track request size for token usage
@@ -507,6 +605,48 @@ class ProbeServer {
507
605
  // The @probelabs/probe package now handles binary path management internally
508
606
  // We don't need to verify or download the binary in the MCP server anymore
509
607
 
608
+ // Initialize LSP servers for the current workspace if --lsp flag is enabled
609
+ if (this.lspEnabled) {
610
+ const workspaceRoot = process.cwd();
611
+ console.error(`Initializing LSP servers for workspace: ${workspaceRoot}`);
612
+
613
+ try {
614
+ // Execute probe lsp init command to pre-warm language servers
615
+ // Use recursive flag to discover nested projects in monorepos
616
+ const initCmd = process.platform === 'win32'
617
+ ? `probe lsp init -w "${workspaceRoot}" --recursive`
618
+ : `probe lsp init -w '${workspaceRoot}' --recursive`;
619
+
620
+ const { stdout, stderr } = await execAsync(initCmd, {
621
+ timeout: 10000, // 10 second timeout for initialization - don't wait too long
622
+ env: { ...process.env }
623
+ });
624
+
625
+ if (stderr && !stderr.includes('Successfully initialized')) {
626
+ console.error(`LSP initialization warnings: ${stderr}`);
627
+ }
628
+
629
+ console.error(`LSP servers initialized successfully for workspace: ${workspaceRoot}`);
630
+
631
+ // Parse initialization output to show what was initialized
632
+ if (stdout) {
633
+ const lines = stdout.split('\n');
634
+ const initializedServers = lines.filter(line =>
635
+ line.includes('✓') || line.includes('language server')
636
+ );
637
+ if (initializedServers.length > 0) {
638
+ console.error('Initialized language servers:');
639
+ initializedServers.forEach(line => console.error(` ${line.trim()}`));
640
+ }
641
+ }
642
+ } catch (error: any) {
643
+ // Don't fail MCP server startup if LSP initialization fails
644
+ // LSP will still work with cold start on first use
645
+ console.error(`Warning: Failed to initialize LSP servers: ${error.message || error}`);
646
+ console.error('LSP features will still be available but may have slower first-use performance');
647
+ }
648
+ }
649
+
510
650
  // Just connect the server to the transport
511
651
  const transport = new StdioServerTransport();
512
652
  await this.server.connect(transport);
@@ -514,5 +654,10 @@ class ProbeServer {
514
654
  }
515
655
  }
516
656
 
517
- const server = new ProbeServer(cliConfig.timeout, cliConfig.format || 'outline');
657
+ // Instantiate server with (timeout, lspEnabled, format)
658
+ const server = new ProbeServer(
659
+ cliConfig.timeout ?? 30,
660
+ cliConfig.lsp ?? false,
661
+ cliConfig.format || 'outline-xml'
662
+ );
518
663
  server.run().catch(console.error);
package/src/search.js CHANGED
@@ -32,7 +32,8 @@ const SEARCH_FLAG_MAP = {
32
32
  session: '--session',
33
33
  timeout: '--timeout',
34
34
  language: '--language',
35
- format: '--format'
35
+ format: '--format',
36
+ lsp: '--lsp'
36
37
  };
37
38
 
38
39
  /**
@@ -58,7 +59,7 @@ const SEARCH_FLAG_MAP = {
58
59
  * @param {string} [options.session] - Session ID for caching results
59
60
  * @param {number} [options.timeout] - Timeout in seconds (default: 30)
60
61
  * @param {string} [options.language] - Limit search to files of a specific programming language
61
- * @param {string} [options.format] - Output format ('json', 'outline-xml', etc.)
62
+ * @param {boolean} [options.lsp] - Use LSP (Language Server Protocol) for enhanced symbol information
62
63
  * @param {Object} [options.binaryOptions] - Options for getting the binary
63
64
  * @param {boolean} [options.binaryOptions.forceDownload] - Force download even if binary exists
64
65
  * @param {string} [options.binaryOptions.version] - Specific version to download
@@ -85,8 +86,8 @@ export async function search(options) {
85
86
  if (options.json && !options.format) {
86
87
  cliArgs.push('--format', 'json');
87
88
  } else if (options.format) {
88
- // Format is already handled by buildCliArgs through SEARCH_FLAG_MAP
89
- // but we need to ensure json parsing for json format
89
+ // Format is handled by buildCliArgs through SEARCH_FLAG_MAP.
90
+ // Ensure json parsing is enabled for json format.
90
91
  if (options.format === 'json') {
91
92
  options.json = true;
92
93
  }
@@ -257,4 +258,4 @@ export async function search(options) {
257
258
  };
258
259
  throw structuredError;
259
260
  }
260
- }
261
+ }
@@ -69,12 +69,12 @@ function autoQuoteSearchTerms(query) {
69
69
  if (token.startsWith('"')) return token;
70
70
  // Boolean operator
71
71
  if (operators.has(token)) return token;
72
- // Check if token needs quoting: has mixed case (upper+lower) or underscores
73
- const hasUpper = /[A-Z]/.test(token);
74
- const hasLower = /[a-z]/.test(token);
72
+ // Check if token needs quoting: has camelCase/PascalCase transitions or underscores
73
+ // Simple capitalized words like "Redis" or "Limiter" should NOT be quoted —
74
+ // only quote when there's an actual case transition (e.g., "getUserData", "NewSlidingLog")
75
75
  const hasUnderscore = token.includes('_');
76
- const hasMixedCase = hasUpper && hasLower;
77
- if (hasMixedCase || hasUnderscore) {
76
+ const hasCaseTransition = /[a-z][A-Z]/.test(token) || /[A-Z]{2,}[a-z]/.test(token);
77
+ if (hasCaseTransition || hasUnderscore) {
78
78
  return `"${token}"`;
79
79
  }
80
80
  return token;
@@ -237,7 +237,7 @@ function buildSearchDelegateTask({ searchQuery, searchPath, exact, language, all
237
237
  'Break down complex queries into multiple searches to cover all aspects.',
238
238
  '',
239
239
  'Available tools:',
240
- '- search: Find code matching keywords or patterns. Run multiple searches for different aspects of complex queries.',
240
+ '- search: Find code matching keywords or patterns. Results are paginated — use nextPage=true when results are relevant to get more. Run multiple searches for different aspects.',
241
241
  '- extract: Verify code snippets to ensure targets are actually relevant before including them.',
242
242
  '- listFiles: Understand directory structure to find where relevant code might live.',
243
243
  '',
@@ -258,13 +258,14 @@ function buildSearchDelegateTask({ searchQuery, searchPath, exact, language, all
258
258
  '',
259
259
  'Combining searches with OR:',
260
260
  '- Multiple unquoted words use OR logic: rate limit matches files containing EITHER "rate" OR "limit".',
261
- '- For known symbol names, quote each term to prevent splitting: \'"limitDRL" "limitRedis"\' matches either exact symbol.',
261
+ '- IMPORTANT: Multiple quoted terms use AND logic by default: \'"RateLimit" "middleware"\' requires BOTH in the same file.',
262
+ '- To search for ANY of several quoted symbols, use the explicit OR operator: \'"ForwardMessage" OR "SessionLimiter"\'.',
262
263
  '- Without quotes, camelCase like limitDRL gets split into "limit" + "DRL" — not what you want for symbol lookup.',
263
264
  '- Use OR to search for multiple related symbols in ONE search instead of separate searches.',
264
265
  '- This is much faster than running separate searches sequentially.',
265
- '- Example: search \'"ForwardMessage" "SessionLimiter"\' finds files with either exact symbol in one call.',
266
- '- Example: search \'"limitDRL" "doRollingWindowWrite"\' finds both rate limiting functions at once.',
267
- '- Use AND only when you need both terms to appear in the same file: "rate AND limit".',
266
+ '- Example: search \'"ForwardMessage" OR "SessionLimiter"\' finds files with either exact symbol in one call.',
267
+ '- Example: search \'"limitDRL" OR "doRollingWindowWrite"\' finds both rate limiting functions at once.',
268
+ '- Use AND (or just put quoted terms together) when you need both terms in the same file.',
268
269
  '',
269
270
  'Parallel tool calls:',
270
271
  '- When you need to search for INDEPENDENT concepts, call multiple search tools IN PARALLEL (same response).',
@@ -278,10 +279,10 @@ function buildSearchDelegateTask({ searchQuery, searchPath, exact, language, all
278
279
  ' Query: "Find the IP allowlist middleware"',
279
280
  ' → search "allowlist middleware" (one search, probe handles IP/ip/Ip variations)',
280
281
  ' Query: "Find ForwardMessage and SessionLimiter"',
281
- ' → search \'"ForwardMessage" "SessionLimiter"\' (one OR search finds both exact symbols)',
282
+ ' → search \'"ForwardMessage" OR "SessionLimiter"\' (one OR search finds both exact symbols)',
282
283
  ' OR: search exact=true "ForwardMessage" + search exact=true "SessionLimiter" IN PARALLEL',
283
284
  ' Query: "Find limitDRL and limitRedis functions"',
284
- ' → search \'"limitDRL" "limitRedis"\' (one OR search, quoted to prevent camelCase splitting)',
285
+ ' → search \'"limitDRL" OR "limitRedis"\' (one OR search, quoted to prevent camelCase splitting)',
285
286
  ' Query: "Find ThrottleRetryLimit usage"',
286
287
  ' → search exact=true "ThrottleRetryLimit" (one search, if no results the symbol does not exist — stop)',
287
288
  ' Query: "How does BM25 scoring work with SIMD optimization?"',
@@ -289,7 +290,7 @@ function buildSearchDelegateTask({ searchQuery, searchPath, exact, language, all
289
290
  '',
290
291
  'BAD search strategy (never do this):',
291
292
  ' → search "AllowedIPs" → search "allowedIps" → search "allowed_ips" (WRONG: case/style variations, probe handles them)',
292
- ' → search "limitDRL" → search "LimitDRL" (WRONG: case variation — combine with OR: \'"limitDRL" "limitRedis"\')',
293
+ ' → search "limitDRL" → search "LimitDRL" (WRONG: case variation — combine with OR: \'"limitDRL" OR "limitRedis"\')',
293
294
  ' → search "throttle_retry_limit" after searching "ThrottleRetryLimit" (WRONG: snake_case variation, probe handles it)',
294
295
  ' → search "ThrottleRetryLimit" path=tyk → search "ThrottleRetryLimit" path=gateway → search "ThrottleRetryLimit" path=apidef (WRONG: same query on different paths — probe searches recursively)',
295
296
  ' → search "func (k *RateLimitAndQuotaCheck) handleRateLimitFailure" (WRONG: do not search full function signatures, just use exact=true "handleRateLimitFailure")',
@@ -302,15 +303,34 @@ function buildSearchDelegateTask({ searchQuery, searchPath, exact, language, all
302
303
  '- To bypass stopword filtering: wrap terms in quotes ("return", "struct") or set exact=true. Both disable stemming and splitting too.',
303
304
  '- camelCase terms are split: getUserData becomes "get", "user", "data" — so one search covers all naming styles.',
304
305
  '- Do NOT search for full function signatures like "func (r *Type) Method(args)". Just search for the method name with exact=true.',
306
+ '- Do NOT search for file names (e.g., "sliding_log.go"). Use listFiles to discover files by name.',
307
+ '',
308
+ 'PAGINATION:',
309
+ '- Search results are paginated (~20k tokens per page).',
310
+ '- If your search returned relevant files, call the same query with nextPage=true to check for more.',
311
+ '- Keep paginating while results stay relevant. Stop when results are off-topic or "All results retrieved".',
312
+ '',
313
+ 'WHEN TO STOP:',
314
+ '- After you have explored the main concept AND related subsystems.',
315
+ '- Once you have 5-15 targets covering different aspects of the query.',
316
+ '- If you get a "DUPLICATE SEARCH BLOCKED" message, move on.',
305
317
  '',
306
318
  'Strategy:',
307
- '1. Analyze the query - identify key concepts and group related symbols',
308
- '2. Combine related symbols into OR searches: \'"symbolA" "symbolB"\' finds files with either (quote to prevent splitting)',
309
- '3. Run INDEPENDENT searches in PARALLEL do not wait for one to finish before starting another',
319
+ '1. Analyze the query identify key concepts, then brainstorm SYNONYMS and alternative terms for each.',
320
+ ' Code naming often differs from the concept: "authentication" verify, credentials, login, auth;',
321
+ ' "rate limiting" throttle, quota, limiter, bucket; "error handling" catch, recover, panic.',
322
+ ' Think about what a developer would NAME the function/struct/variable, not just the concept.',
323
+ '2. Run INDEPENDENT searches in PARALLEL — search for the main concept AND synonyms simultaneously.',
324
+ ' After each search, check if results are relevant. If yes, call nextPage=true for more results.',
325
+ '3. Combine related symbols into OR searches: \'"symbolA" OR "symbolB"\' finds files with either.',
310
326
  '4. For known symbol names use exact=true. For concepts use default (exact=false).',
311
- '5. If a search returns results, use extract to verify relevance. Run multiple extracts in parallel too.',
312
- '6. If a search returns NO results, the term does not exist. Do NOT retry with variations, different paths, or longer strings. Move on.',
313
- '7. Combine all relevant targets in your final response',
327
+ '5. After your first round of searches, READ the extracted code and look for connected code:',
328
+ ' - Function calls to other important functions include those targets.',
329
+ ' - Type references and imports include type definitions.',
330
+ ' - Registered handlers/middleware → include all registered items.',
331
+ '6. If a search returns results, use extract to verify relevance. Run multiple extracts in parallel too.',
332
+ '7. If a search returns NO results, the term does not exist. Do NOT retry with variations. Move on.',
333
+ '8. Once you have enough targets (typically 5-15), output your final JSON answer immediately.',
314
334
  '',
315
335
  `Query: ${searchQuery}`,
316
336
  `Search path(s): ${searchPath}`,
@@ -319,7 +339,9 @@ function buildSearchDelegateTask({ searchQuery, searchPath, exact, language, all
319
339
  'Return ONLY valid JSON: {"targets": ["path/to/file.ext#Symbol", "path/to/file.ext:line", "path/to/file.ext:start-end"]}',
320
340
  'IMPORTANT: Use ABSOLUTE file paths in targets (e.g., "/full/path/to/file.ext#Symbol"). If you only have relative paths, make them relative to the search path above.',
321
341
  'Prefer #Symbol when a function/class name is clear; otherwise use line numbers.',
322
- 'Deduplicate targets. Do NOT explain or answer - ONLY return the JSON targets.'
342
+ 'Deduplicate targets. Do NOT explain or answer - ONLY return the JSON targets.',
343
+ '',
344
+ 'Remember: if your search returned relevant results, use nextPage=true to check for more before outputting.'
323
345
  ].join('\n');
324
346
  }
325
347
 
@@ -351,6 +373,8 @@ export const searchTool = (options = {}) => {
351
373
 
352
374
  // Track previous non-paginated searches to detect and block duplicates
353
375
  const previousSearches = new Set();
376
+ // Track how many times a duplicate search has been blocked (for escalating messages)
377
+ let consecutiveDupBlocks = 0;
354
378
  // Track pagination counts per query to cap runaway pagination
355
379
  const paginationCounts = new Map();
356
380
  const MAX_PAGES_PER_QUERY = 3;
@@ -422,12 +446,17 @@ export const searchTool = (options = {}) => {
422
446
  const searchKey = `${searchQuery}::${exact || false}`;
423
447
  if (!nextPage) {
424
448
  if (previousSearches.has(searchKey)) {
449
+ consecutiveDupBlocks++;
425
450
  if (debug) {
426
- console.error(`[DEDUP] Blocked duplicate search: "${searchQuery}" (path: "${searchPath}")`);
451
+ console.error(`[DEDUP] Blocked duplicate search (${consecutiveDupBlocks}x): "${searchQuery}" (path: "${searchPath}")`);
452
+ }
453
+ if (consecutiveDupBlocks >= 3) {
454
+ return 'STOP. You have been blocked ' + consecutiveDupBlocks + ' times for repeating searches. You MUST output your final JSON answer NOW with whatever targets you have found. Do NOT call any more tools.';
427
455
  }
428
- return 'DUPLICATE SEARCH BLOCKED: You already searched for this exact query. Changing the path does NOT give different results — probe searches recursively. Do NOT repeat the same search. Try a genuinely different keyword, use extract to examine results you already found, or provide your final answer if you have enough information.';
456
+ return 'DUPLICATE SEARCH BLOCKED (' + consecutiveDupBlocks + 'x). You already searched for this. Do NOT repeat — probe searches recursively across all paths. Either: (1) use extract on results you already found, (2) try a COMPLETELY different keyword, or (3) output your final answer NOW.';
429
457
  }
430
458
  previousSearches.add(searchKey);
459
+ consecutiveDupBlocks = 0; // Reset on successful new search
431
460
  paginationCounts.set(searchKey, 0);
432
461
  } else {
433
462
  // Cap pagination to prevent runaway page-through of broad queries